From d7dbe7fee362d587dfc334135d9581b6216a4295 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 23 Nov 2025 04:13:56 +0000
Subject: [PATCH] =Timeline block, and feed block updated. DataStore.js refactored to not block rendering

---
 base/_setup.php                         |    6 
 assets/css/icons.css                    |    1 
 assets/js/dash/TaxonomyCreator.js       |  242 
 inc/blocks/SummaryBlock.php             |    2 
 assets/css/dash.min.css                 |    2 
 jvb.php                                 |   90 
 assets/js/min/modal.min.js              |    2 
 inc/helpers/formatting.php              |   10 
 inc/rest/routes/TermRoutes.php          |  196 
 inc/managers/OperationQueue.php         |    8 
 assets/js/min/form.min.js               |    2 
 inc/managers/DashboardManager.php       |  121 
 inc/rest/routes/ContentRoutes.php       |  292 
 src/feed/edit.js                        |  346 
 assets/js/min/referral.min.js           |    2 
 build/feed/block.json                   |    7 
 inc/rest/routes/FeedRoutes.php          | 2026 +++--
 assets/js/concise/Swiper.js             |  154 
 build/timeline/style-index-rtl.css      |    1 
 assets/js/min/favourites.min.js         |    2 
 inc/integrations/_setup.php             |    5 
 assets/js/concise/Gallery.js            |  463 +
 build/gmbreviews/render.php             |   34 
 build/timeline/block.json               |   26 
 JVBase.php                              |    5 
 inc/managers/LoginManager.php           |  112 
 inc/meta/MetaManager.php                |    7 
 assets/js/concise/Queue.js              |  152 
 inc/integrations/Integrations.php       |    8 
 src/timeline/edit.js                    |   38 
 src/feed/style.scss                     | 1262 +-
 inc/helpers/breadcrumbs.php             |   37 
 src/timeline/view.js                    |    0 
 build/feed/style-index.css              |    2 
 build/feed/index.asset.php              |    2 
 assets/js/concise/UploadManager.js      | 3920 +++++-----
 src/feed/view.js                        | 1174 +-
 src/timeline/editor.scss                |    0 
 build/timeline/index.js                 |    1 
 assets/css/forms.min.css                |    2 
 assets/js/concise/View.js               |  460 
 inc/blocks/_setup.php                   |    4 
 src/gmbreviews/style.scss               |   64 
 build/timeline/style-index.css          |    1 
 inc/meta/MetaForm.php                   |   14 
 inc/rest/routes/UploadRoutes.php        |    2 
 assets/js/min/populate.min.js           |    2 
 build/feed/index.js                     |    2 
 src/timeline/index.js                   |   33 
 inc/integrations/GoogleMyBusiness.php   |   24 
 inc/blocks/GlossaryBlock.php            |    2 
 build/gmbreviews/style-index.css        |    2 
 build/timeline/index.css                |    1 
 assets/js/concise/TaxonomySelector.js   |  521 
 assets/js/Gallery.js                    |    2 
 inc/utility/Image.php                   |   88 
 assets/js/min/view.min.js               |    2 
 inc/integrations/PostMark.php           |    3 
 assets/js/concise/DataStore.js          | 1966 ++--
 webpack.jvb.js                          |    3 
 inc/utility/setup.php                   |    1 
 assets/js/min/dataStore.min.js          |    2 
 build/feed/view.js                      |    2 
 inc/rest/routes/MagicLinkRoutes.php     |   22 
 inc/rest/routes/LoginRoutes.php         |   21 
 assets/js/min/a11y.min.js               |    2 
 src/feed/block.json                     |    4 
 build/timeline/index.asset.php          |    1 
 inc/managers/CacheManager.php           |  682 +
 assets/js/dash/Modal.js                 |    1 
 assets/js/dash/UtilityFunctions.js      |   35 
 assets/js/concise/Media.js              |    4 
 build/timeline/index-rtl.css            |    1 
 inc/helpers/ui.php                      |   24 
 assets/js/dash/A11yHelper.js            |    4 
 inc/forms/TaxonomySelector.php          |   20 
 build/timeline/view.js                  |    0 
 inc/registry/TaxonomyRegistrar.php      |   20 
 inc/managers/CRUDManager.php            |  174 
 assets/js/min/queue.min.js              |    2 
 inc/blocks/CustomBlocks.php             |  151 
 assets/js/min/utility.min.js            |    2 
 src/timeline/style.scss                 |  135 
 src/timeline/render.php                 |    8 
 assets/js/concise/DataStoreOld.js       | 1621 ++-
 inc/blocks/RegisterBlocks.php           |   45 
 assets/js/min/media.min.js              |    2 
 assets/js/concise/FrontendVotes.js      |   14 
 build/gmbreviews/style-index-rtl.css    |    2 
 inc/blocks/TimelineBlock.php            |  181 
 assets/js/min/swiper.min.js             |    1 
 assets/js/min/gallery.min.js            |    2 
 inc/managers/IconsManager.php           |  301 
 build/feed/style-index-rtl.css          |    2 
 build/timeline/view.asset.php           |    1 
 src/timeline/index.php                  |    0 
 inc/helpers/time.php                    |    2 
 assets/js/concise/Referral.js           |    5 
 assets/js/min/selector.min.js           |    2 
 inc/rest/RegisterRoutes.php             |    5 
 src/gmbreviews/render.php               |   34 
 assets/js/concise/FormController.js     |  254 
 inc/blocks/FeedBlock.php                |  565 
 inc/registry/PostTypeRegistrar.php      |   39 
 inc/meta/MetaValidator.php              |    1 
 assets/js/min/crud.min.js               |    2 
 assets/js/concise/PopulateForm.js       |  221 
 assets/js/dash/CRUD.js                  |  263 
 inc/helpers/renderFields.php            |   89 
 assets/js/concise/FrontendFavourites.js |   54 
 inc/helpers/media.php                   |   20 
 inc/helpers/members.php                 |    8 
 inc/blocks/MenuBlock.php                |    2 
 inc/rest/routes/QueueRoutes.php         |   87 
 inc/utility/Features.php                |    1 
 inc/managers/AdminPages.php             |  660 +
 assets/js/min/uploader.min.js           |    2 
 assets/js/min/creator.min.js            |    2 
 build/feed/view.asset.php               |    2 
 src/timeline/block.json                 |   23 
 /dev/null                               |  252 
 src/feed/viewOld.js                     | 1996 +---
 inc/rest/RestRouteManager.php           |   16 
 123 files changed, 12,167 insertions(+), 9,888 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 8894d0c..04bb699 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -109,14 +109,14 @@
 			'login'			=> new LoginRoutes(),
 			'integrations'	=> new IntegrationsRoutes(),
 		];
-        if (jvbSiteUsesFeedBlock()) {
+        if (Features::forSite()->has('feed_block')) {
             $this->routes['feed'] = new FeedRoutes();
         }
         if (jvbSiteHasNotifications()) {
 			$this->managers['notifications'] = new NotificationManager();
             $this->routes['notifications'] = new NotificationsRoutes();
         }
-        if (jvbSiteUsesFeedBlock() || jvbSiteHasDashboard()) {
+        if (Features::forSite()->has('feed_block') || jvbSiteHasDashboard()) {
             $this->routes['term'] = new TermRoutes();
         }
 
@@ -263,7 +263,6 @@
 
     public function activate():void
     {
-        error_log('Activate!');
         // Activate roles - will be properly initialized after post types are registered
         $this->roles()->activate();
     }
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 751536b..e3d33d7 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-: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}
\ No newline at end of file
+: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,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.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}
\ No newline at end of file
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index 4bd1dd5..031ce4c 100644
--- a/assets/css/forms.min.css
+++ b/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.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)}
\ No newline at end of file
+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{margin-bottom: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.preview summary span{display:none}.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 .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{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))}.upload-group .selected .field{margin:0}.upload-group .group-actions button{aspect-ratio:unset}.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{order:-1;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)}
\ No newline at end of file
diff --git a/assets/css/icons.css b/assets/css/icons.css
index 429920e..e69de29 100644
--- a/assets/css/icons.css
+++ b/assets/css/icons.css
@@ -1 +0,0 @@
-.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=');}
\ No newline at end of file
diff --git a/assets/js/Gallery.js b/assets/js/Gallery.js
index dfc8069..9c596c4 100644
--- a/assets/js/Gallery.js
+++ b/assets/js/Gallery.js
@@ -165,6 +165,8 @@
 			newIndex = this.items.length - 1;
 		} else if (newIndex >= this.items.length) {
 			newIndex = 0;
+		} else if (this.items.length - newIndex === 3) {
+			this.notify('load-more');
 		}
 
 		this.index = newIndex;
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index fe9b22e..47f72e8 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -1,314 +1,986 @@
 /**
- * ExtendedDataStore - A flexible IndexedDB wrapper with HTTP caching
+ * DataStore - Singleton pattern managing multiple store namespaces
  *
- * Configuration-based approach for different storage needs:
- * - Configurable endpoint, keyPath, and indexes
- * - Built-in ETag and If-Modified-Since support
- * - Automatic DOM reference stripping
- * - TTL-based cache invalidation
- *
- * All notifications:
- *
-  		this.store.subscribe((event, data) => {
-  			switch (event) {
-  				case 'data-loaded':
-  					break;
-  				case 'item-saved':
-  					break;
-  				case 'items-saved':
-  					break;
-  				case 'item-deleted':
-  					break;
-  				case 'data-cleared':
-  					break;
-  				case 'filters-changed':
-  					break;
-  				case 'filters-cleared':
-  					break;
-  				case 'cache-cleared':
-  					break;
-  			}
-  		});
+ * Usage:
+ *   window.jvbStore = new DataStore();
+ *   this.store = window.jvbStore.register('feed', { config });
  */
 class DataStore {
-	constructor(config = {}) {
-		// Core configuration with sensible defaults
-		this.config = {
-			// Storage configuration
-			name: 'default',
-			version: 1,
-			storeName: 'items',
-			keyPath: 'id',
-			indexes: [], // Array of {name, keyPath, unique}
+	constructor() {
+		// Singleton pattern
+		if (DataStore.instance) {
+			return DataStore.instance;
+		}
+		DataStore.instance = this;
 
-			// API configuration
-			endpoint: null,
-			saveToServer: false,
-			apiBase: jvbSettings.api,
-			headers: {},
-			filters: {},
-			required:  null, //any required filters before fetching
-			icon: null,
-			getBlobs: null,
+		// Shared resources
+		this.databases = new Map();      // Shared IndexedDB connections
+		this.stores = new Map();         // Registered store namespaces
+		this.subscribers = new Map();    // Per-store event subscribers
+		this.pendingInits = new Map();   // Track initialization promises
+		this.fetchQueue = [];
 
-			// Cache configuration
-			TTL: 3600000, // 1 hour default
-			useHttpCaching: true, // ETag and If-Modified-Since
-			cacheKeyStrategy: 'filters', // How to generate cache keys
-
-			// UI configuration
-			showLoading: true,
-
-			// Features
-			stripDOMReferences: true,
-			storeBlobs: false,
-
-			...config
-		};
-
-		// Initialize base properties
-		this.db = null;
-		this.data = new Map();
-		this.cache = new Map();
-		this.isFetching = false;
-		this.pendingFetch = null;
-		this.httpHeaders = new Map();
-		this.subscribers = new Set();
-		this.currentRequest = null;
-		this.filters = this.config.filters??{};
-
-		// Set up headers
-		this.headers = {
-			'X-WP-Nonce': jvbSettings?.nonce,
-			...this.config.headers
-		};
-
+		// Global state
+		this._initialized = false;
 		this.body = document.body;
 		this.loading = document.querySelector('dialog.loading');
 
-		// Auto-initialize
-		this.initDB();
+		this.init();
 
-		// Cleanup on page unload
 		window.addEventListener('beforeunload', () => this.destroy());
 	}
 
-	/**
-	 * Initialize IndexedDB with configurable schema
-	 */
-	async initDB() {
+	async init() {
+		if (this._initialized) return;
+		this._initialized = true;
+
 		if (!('indexedDB' in window)) {
 			console.warn('IndexedDB not supported');
-			return;
 		}
-
-		const dbName = `jvb_${this.config.name}_db`;
-		const request = indexedDB.open(dbName, this.config.version);
-
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-
-			// Create main store with configurable keyPath
-			if (!db.objectStoreNames.contains(this.config.storeName)) {
-				const store = db.createObjectStore(this.config.storeName, {
-					keyPath: this.config.keyPath
-				});
-
-				// Add configured indexes
-				this.config.indexes.forEach(index => {
-					store.createIndex(
-						index.name,
-						index.keyPath || index.name,
-						{ unique: index.unique || false }
-					);
-				});
-			}
-
-			// Cache store for HTTP responses
-			if (this.config.endpoint && !db.objectStoreNames.contains('cache')) {
-				const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
-				cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
-				cacheStore.createIndex('endpoint', 'endpoint', { unique: false });
-				cacheStore.createIndex('filters', 'filters', { unique: false });
-			}
-
-			// HTTP headers store for ETag/If-Modified-Since
-			if (this.config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
-				db.createObjectStore('headers', { keyPath: 'key' });
-			}
-
-			if (this.config.storeBlobs && !db.objectStoreNames.contains('blobs')) {
-				db.createObjectStore('blobs', { keyPath: 'uploadId' });
-			}
-
-			// Call optional schema extension
-			if (this.config.onUpgrade) {
-				this.config.onUpgrade(db, e.oldVersion, e.newVersion);
-			}
-		};
-
-		request.onsuccess = async (e) => {
-			this.db = e.target.result;
-
-			// Load cache and headers BEFORE fetching (only if stores exist)
-			const loadTasks = [this.loadFromDB()];
-
-			if (this.db.objectStoreNames.contains('cache')) {
-				loadTasks.push(this.loadCache());
-			}
-
-			if (this.config.useHttpCaching && this.db.objectStoreNames.contains('headers')) {
-				loadTasks.push(this.loadHeaders());
-			}
-
-			await Promise.all(loadTasks);
-
-			this.notify('db-init');
-
-			// Now fetch if needed (cache might already have data)
-			if (this.config.endpoint) {
-				this.fetch();
-			}
-		};
-
-		request.onerror = (e) => {
-			console.error(`IndexedDB error for ${dbName}:`, e);
-			if (this.config.onError) {
-				this.config.onError(e);
-			}
-		};
 	}
 
 	/**
-	 * Load all data from IndexedDB
+	 * Register a new store namespace
 	 */
-	async loadFromDB() {
-		if (!this.db) return;
+	register(name, config = {}) {
+		if (this.stores.has(name)) {
+			console.warn(`Store "${name}" already registered`);
+			return this.getStoreAPI(name);
+		}
 
-		return new Promise(async (resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.getAll();
+		if (!config.keyPath) {
+			throw new Error(`Store "${name}" requires a keyPath`);
+		}
 
-			request.onsuccess = async (e) => {
-				const items = e.target.result;
+		const store = {
+			name,
+			config: {
+				// Storage
+				dbName: `jvb_${name}_db`,
+				version: 1,
+				storeName: 'items',
+				keyPath: 'id',
+				indexes: [],
 
-				// Restore FormData for ALL items on startup
-				for (const item of items) {
-					if (item.data?._isFormData && this.config.getBlobs) {
-						item.data = await this.objectToFormData(item.data);
-					}
-					const key = this.getItemKey(item);
-					this.data.set(key, item);
+				// API
+				endpoint: null,
+				apiBase: jvbSettings.api,
+				filters: {},
+				required: null,
+
+				// Cache
+				TTL: 3600000, // 1 hour
+				useHttpCaching: true,
+
+				// Behavior
+				showLoading: false,
+				delayFetch: true,
+				validateData: true, // Validate data is serializable
+
+				...config
+			},
+
+			// State
+			db: null,
+			data: new Map(),
+			cache: new Map(),
+			httpHeaders: new Map(),
+			filters: { ...config.filters },
+			isFetching: false,
+			currentRequest: null,
+			lastResponse: null,
+			_initialized: false
+		};
+
+		store.config.headers = {
+			'X-WP-Nonce': jvbSettings?.nonce,
+			...store.config.headers
+		};
+
+		this.stores.set(name, store);
+		this.subscribers.set(name, new Set());
+
+		// Initialize database asynchronously
+		this.initStoreDB(name).catch(error => {
+			console.error(`Failed to initialize store "${name}":`, error);
+		});
+
+		return this.getStoreAPI(name);
+	}
+
+	/**
+	 * Get the API object for a registered store
+	 */
+	getStoreAPI(name) {
+		const api = {
+			// Data methods
+			fetch: () => this.fetch(name),
+			save: (item) => this.save(name, item),
+			delete: (id) => this.delete(name, id),
+			get: (id) => this.get(name, id),
+			getAll: () => this.getAll(name),
+			getFiltered: () => this.getFiltered(name),
+			clear: () => this.clear(name),
+
+			// Filter methods
+			setFilter: (key, value) => this.setFilter(name, key, value),
+			setFilters: (filters) => this.setFilters(name, filters),
+			removeFilter: (key) => this.removeFilter(name, key),
+			clearFilters: () => this.clearFilters(name),
+
+			// Cache methods
+			clearCache: () => this.clearCache(name),
+			clearHttpHeaders: (key) => this.clearHttpHeaders(name, key),
+
+			// Event methods
+			subscribe: (callback) => this.subscribe(name, callback),
+
+			// Utility
+			ensureInitialized: () => this.ensureStoreInitialized(name),
+
+			// Exposed properties (read-only)
+			get filters() {
+				return { ...api.getStore().filters };
+			},
+			get lastResponse() {
+				return api.getStore().lastResponse;
+			},
+			get data() {
+				return api.getStore().data;
+			},
+
+			getStore: () => this.stores.get(name)
+		};
+
+		return api;
+	}
+
+	/**
+	 * Normalize data before saving - convert Sets/Maps automatically
+	 */
+	normalizeForStorage(obj) {
+		if (obj === null || obj === undefined) return obj;
+
+		// Convert Set to Array
+		if (obj instanceof Set) {
+			return Array.from(obj);
+		}
+
+		// Convert Map to Object
+		if (obj instanceof Map) {
+			return Object.fromEntries(obj);
+		}
+
+		// Handle Arrays
+		if (Array.isArray(obj)) {
+			return obj.map(item => this.normalizeForStorage(item));
+		}
+
+		// Handle Objects
+		if (typeof obj === 'object') {
+			const normalized = {};
+			for (const [key, value] of Object.entries(obj)) {
+				normalized[key] = this.normalizeForStorage(value);
+			}
+			return normalized;
+		}
+
+		return obj;
+	}
+
+	/**
+	 * Strip DOM references from object
+	 */
+	stripDOMReferences(obj, visited = new WeakSet()) {
+		if (obj === null || obj === undefined) return obj;
+
+		const type = typeof obj;
+		if (type === 'string' || type === 'number' || type === 'boolean') {
+			return obj;
+		}
+
+		// Prevent circular references
+		if (type === 'object' && visited.has(obj)) {
+			return '[Circular]';
+		}
+
+		// Remove DOM elements
+		if (obj instanceof HTMLElement ||
+			obj instanceof NodeList ||
+			obj instanceof HTMLCollection ||
+			obj.nodeType !== undefined) {
+			return null;
+		}
+
+		// Handle Date
+		if (obj instanceof Date) {
+			return obj;
+		}
+
+		// Handle Arrays
+		if (Array.isArray(obj)) {
+			visited.add(obj);
+			return obj.map(item => this.stripDOMReferences(item, visited)).filter(v => v !== null);
+		}
+
+		// Handle Objects
+		if (type === 'object') {
+			visited.add(obj);
+			const cleaned = {};
+			for (const [key, value] of Object.entries(obj)) {
+				const cleanedValue = this.stripDOMReferences(value, visited);
+				if (cleanedValue !== null) {
+					cleaned[key] = cleanedValue;
 				}
+			}
+			return cleaned;
+		}
 
-				this.notify('data-loaded', { count: items.length });
-				resolve(items);
+		return obj;
+	}
+
+	/**
+	 * Initialize database for a specific store
+	 */
+	async initStoreDB(name) {
+		const store = this.stores.get(name);
+		if (!store || store._initialized) return;
+
+		if (this.pendingInits.has(name)) {
+			return this.pendingInits.get(name);
+		}
+
+		const initPromise = this._performStoreInit(name);
+		this.pendingInits.set(name, initPromise);
+
+		try {
+			await initPromise;
+			store._initialized = true;
+		} finally {
+			this.pendingInits.delete(name);
+		}
+	}
+
+	async _performStoreInit(name) {
+		const store = this.stores.get(name);
+		const { dbName, version } = store.config;
+
+		try {
+			if (!this.databases.has(dbName)) {
+				const db = await this.openDatabase(dbName, version, (db) => {
+					this.setupStores(db, store.config);
+				});
+				this.databases.set(dbName, db);
+			}
+
+			store.db = this.databases.get(dbName);
+			this.loadStoreDataInBackground(name);
+			this.notify(name, 'db-init');
+
+		} catch (error) {
+			console.error(`Failed to initialize database for store "${name}":`, error);
+			throw error;
+		}
+	}
+
+	openDatabase(dbName, version, onUpgrade) {
+		return new Promise((resolve, reject) => {
+			const request = indexedDB.open(dbName, version);
+
+			request.onupgradeneeded = (e) => {
+				if (onUpgrade) {
+					onUpgrade(e.target.result, e.oldVersion, e.newVersion);
+				}
 			};
 
-			request.onerror = (e) => reject(e);
+			request.onsuccess = (e) => resolve(e.target.result);
+			request.onerror = (e) => reject(e.target.error);
+			request.onblocked = () => {
+				console.warn(`Database ${dbName} blocked. Close other tabs.`);
+			};
 		});
 	}
 
+	setupStores(db, config) {
+		// Main store
+		if (!db.objectStoreNames.contains(config.storeName)) {
+			const store = db.createObjectStore(config.storeName, {
+				keyPath: config.keyPath
+			});
 
+			config.indexes.forEach(index => {
+				store.createIndex(
+					index.name,
+					index.keyPath || index.name,
+					{ unique: index.unique || false }
+				);
+			});
+		}
 
-	/**
-	 * Load main data from IndexedDB
-	 */
-	async loadData() {
-		if (!this.db) return;
+		// Cache store
+		if (config.endpoint && !db.objectStoreNames.contains('cache')) {
+			const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
+			cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
+		}
 
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.getAll();
+		// HTTP headers store
+		if (config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
+			db.createObjectStore('headers', { keyPath: 'key' });
+		}
+	}
+
+	loadStoreDataInBackground(name) {
+		const store = this.stores.get(name);
+		if (!store?.db) return;
+
+		const tasks = [
+			this.loadStoreData(name),
+			this.loadStoreCache(name),
+			this.loadStoreHeaders(name)
+		];
+
+		Promise.all(tasks)
+			.then(() => {
+				this.notify(name, 'data-ready');
+
+				// Add to fetch queue instead of immediate fetch
+				if (store.config.endpoint && store.config.delayFetch) {
+					this.fetchQueue.push(name);
+
+					// Start processing queue if not already running
+					if (this.fetchQueue.length === 1) {
+						this.processFetchQueue();
+					}
+				} else if (store.config.endpoint && !store.config.delayFetch) {
+					// Immediate fetch
+					if ('requestIdleCallback' in window) {
+						requestIdleCallback(() => this.fetch(name), { timeout: 2000 });
+					} else {
+						setTimeout(() => this.fetch(name), 100);
+					}
+				}
+			})
+			.catch(error => {
+				console.error(`Background load error for store "${name}":`, error);
+			});
+	}
+
+	async processFetchQueue() {
+		if (this.fetchQueue.length === 0) return;
+
+		const name = this.fetchQueue.shift();
+		const store = this.stores.get(name);
+
+		if (!store) {
+			// Store was removed, continue with next
+			return this.processFetchQueue();
+		}
+
+		try {
+			await this.fetch(name);
+		} catch (error) {
+			console.error(`Queue fetch error for "${name}":`, error);
+		}
+
+		// Process next item with idle callback
+		if (this.fetchQueue.length > 0) {
+			if ('requestIdleCallback' in window) {
+				requestIdleCallback(() => this.processFetchQueue(), { timeout: 2000 });
+			} else {
+				setTimeout(() => this.processFetchQueue(), 50);
+			}
+		}
+	}
+
+	async loadStoreData(name) {
+		const store = this.stores.get(name);
+		if (!store?.db) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction([store.config.storeName], 'readonly');
+			const objectStore = tx.objectStore(store.config.storeName);
+			const request = objectStore.getAll();
 
 			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					// Strip DOM references if needed
-					const cleaned = this.config.stripDOMReferences
-						? this.stripDOMReferences(item)
-						: item;
+				const items = e.target.result || [];
+				items.forEach(item => {
+					const key = this.getItemKey(item, store.config.keyPath);
+					store.data.set(key, item);
+				});
+				this.notify(name, 'data-loaded', { count: items.length });
+				resolve(items);
+			};
 
-					const key = this.getItemKey(cleaned);
-					this.data.set(key, cleaned);
+			request.onerror = () => resolve([]);
+		});
+	}
+
+	async loadStoreCache(name) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains('cache')) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction(['cache'], 'readonly');
+			const objectStore = tx.objectStore('cache');
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				(e.target.result || []).forEach(item => {
+					if (this.isCacheValid(item, store.config.TTL)) {
+						store.cache.set(item.key, item);
+					}
 				});
 				resolve();
 			};
 
-			request.onerror = (e) => reject(e);
+			request.onerror = () => resolve();
 		});
 	}
 
-	/**
-	 * Strip DOM references from an object (recursive)
-	 */
-	stripDOMReferences(obj) {
-		if (!obj || typeof obj !== 'object') return obj;
+	async loadStoreHeaders(name) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains('headers')) return;
 
-		// Handle arrays
+		return new Promise((resolve) => {
+			const tx = store.db.transaction(['headers'], 'readonly');
+			const objectStore = tx.objectStore('headers');
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				(e.target.result || []).forEach(header => {
+					store.httpHeaders.set(header.key, header);
+				});
+				resolve();
+			};
+
+			request.onerror = () => resolve();
+		});
+	}
+
+	async ensureStoreInitialized(name) {
+		const store = this.stores.get(name);
+		if (!store) {
+			throw new Error(`Store "${name}" not registered`);
+		}
+
+		if (!store._initialized) {
+			await this.initStoreDB(name);
+		}
+	}
+
+	async fetch(name) {
+		await this.ensureStoreInitialized(name);
+
+		const store = this.stores.get(name);
+
+		if (store.isFetching) return;
+
+		// Check required filters
+		if (store.config.required) {
+			const required = Array.isArray(store.config.required)
+				? store.config.required
+				: [store.config.required];
+
+			const missing = required.some(key =>
+				!store.filters[key] || store.filters[key] === ''
+			);
+
+			if (missing) return;
+		}
+
+		store.isFetching = true;
+
+		try {
+			// Check cache
+			const cacheKey = this.generateCacheKey(store.filters);
+			const cached = store.cache.get(cacheKey);
+
+			if (cached && this.isCacheValid(cached, store.config.TTL)) {
+				this.notify(name, 'data-loaded', {
+					cached: true,
+					items: cached.items || []
+				});
+				return cached;
+			}
+
+			if (store.config.showLoading) {
+				this.setLoading(true);
+			}
+
+			const url = this.buildFetchUrl(name);
+			const headers = { ...store.config.headers };
+			const cachedHeaders = store.httpHeaders.get(cacheKey);
+
+			if (store.config.useHttpCaching && cachedHeaders) {
+				if (cachedHeaders.etag) {
+					headers['If-None-Match'] = cachedHeaders.etag;
+				}
+				if (cachedHeaders.lastModified) {
+					headers['If-Modified-Since'] = cachedHeaders.lastModified;
+				}
+			}
+
+			const controller = new AbortController();
+			store.currentRequest = controller;
+
+			const response = await fetch(url, {
+				method: 'GET',
+				headers,
+				signal: controller.signal
+			});
+
+			if (response.status === 304 && cached) {
+				this.notify(name, 'data-loaded', {
+					cached: true,
+					notModified: true,
+					items: cached.items || []
+				});
+				return cached;
+			}
+
+			if (!response.ok) {
+				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+			}
+
+			const data = await response.json();
+
+			if (store.config.useHttpCaching) {
+				this.storeResponseHeaders(name, cacheKey, response);
+			}
+
+			await this.processFetchedData(name, data, cacheKey);
+
+			this.notify(name, 'data-loaded', {
+				cached: false,
+				items: data.items || []
+			});
+
+			return data;
+
+		} catch (error) {
+			if (error.name !== 'AbortError') {
+				console.error(`Fetch error for store "${name}":`, error);
+				this.notify(name, 'fetch-error', { error });
+			}
+			throw error;
+
+		} finally {
+			store.isFetching = false;
+			store.currentRequest = null;
+
+			if (store.config.showLoading) {
+				this.setLoading(false);
+			}
+		}
+	}
+
+	buildFetchUrl(name) {
+		const store = this.stores.get(name);
+		const params = new URLSearchParams();
+
+		Object.entries(store.filters).forEach(([key, value]) => {
+			if (value !== null && value !== undefined && value !== '') {
+				if (typeof value === 'object') {
+					params.set(key, JSON.stringify(value));
+				} else {
+					params.set(key, value);
+				}
+			}
+		});
+
+		const baseUrl = store.config.apiBase + store.config.endpoint;
+		return params.toString() ? `${baseUrl}?${params}` : baseUrl;
+	}
+
+	async processFetchedData(name, data, cacheKey) {
+		const store = this.stores.get(name);
+		const items = data.items || [];
+
+		for (const item of items) {
+			await this.save(name, item);
+		}
+
+		const cacheEntry = {
+			key: cacheKey,
+			items: items.map(item => this.getItemKey(item, store.config.keyPath)),
+			timestamp: Date.now(),
+			endpoint: store.config.endpoint,
+			filters: { ...store.filters }
+		};
+
+		store.cache.set(cacheKey, cacheEntry);
+		await this.saveToCache(name, cacheKey, cacheEntry);
+
+		store.lastResponse = {
+			has_more: data.has_more || false,
+			total: data.total || items.length,
+			pages: data.pages || 1
+		};
+	}
+
+	/**
+	 * Save item to store
+	 * IMPORTANT: Item must be serializable (no DOM, FormData, Blobs)
+	 */
+	async save(name, item) {
+		const store = this.stores.get(name);
+
+		// Auto-normalize Sets/Maps
+		let processed = this.normalizeForStorage(item);
+		processed = this.stripDOMReferences(processed);
+
+		// Validate data is serializable
+		if (store.config.validateData) {
+			const validation = this.validateSerializable(processed);
+			if (!validation.valid) {
+				console.error(`Cannot save non-serializable data to store "${name}":`, validation.error);
+				throw new Error(`Non-serializable data: ${validation.error}`);
+			}
+		}
+
+		const key = this.getItemKey(processed, store.config.keyPath);
+
+		// Store in memory
+		store.data.set(key, item);
+
+		// Store in IndexedDB
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.put(processed);
+		}
+
+		this.notify(name, 'item-saved', { item, key });
+		return key;
+	}
+
+	/**
+	 * Validate that data is IndexedDB-serializable
+	 * Rejects: DOM elements, FormData, Blobs, Functions, etc.
+	 */
+	validateSerializable(obj, path = 'root') {
+		// Primitives are fine
+		if (obj === null || obj === undefined) {
+			return { valid: true };
+		}
+
+		const type = typeof obj;
+		if (type === 'string' || type === 'number' || type === 'boolean') {
+			return { valid: true };
+		}
+
+		// Functions cannot be serialized
+		if (type === 'function') {
+			return {
+				valid: false,
+				error: `Function at ${path}`
+			};
+		}
+
+		// Date is serializable
+		if (obj instanceof Date) {
+			return { valid: true };
+		}
+
+		// Reject DOM elements
+		if (obj instanceof HTMLElement ||
+			obj instanceof NodeList ||
+			obj instanceof HTMLCollection ||
+			(obj.nodeType !== undefined)) {
+			return {
+				valid: false,
+				error: `DOM element at ${path}`
+			};
+		}
+
+		// Reject FormData
+		if (obj instanceof FormData) {
+			return {
+				valid: false,
+				error: `FormData at ${path}. Convert to object first.`
+			};
+		}
+
+		// Reject Blobs/Files
+		if (obj instanceof Blob || obj instanceof File) {
+			return {
+				valid: false,
+				error: `Blob/File at ${path}. Handle file uploads separately.`
+			};
+		}
+
+		// Arrays
 		if (Array.isArray(obj)) {
-			return obj.map(item => this.stripDOMReferences(item));
+			for (let i = 0; i < obj.length; i++) {
+				const result = this.validateSerializable(obj[i], `${path}[${i}]`);
+				if (!result.valid) return result;
+			}
+			return { valid: true };
 		}
 
-		// Handle objects
-		const cleaned = {};
-		for (const [key, value] of Object.entries(obj)) {
-			// Skip DOM-related properties
-			if (this.isDOMReference(key, value)) {
-				continue;
+		// Plain objects
+		if (type === 'object') {
+			// Check for Sets/Maps (IndexedDB doesn't support them)
+			if (obj instanceof Set) {
+				return {
+					valid: false,
+					error: `Set at ${path}. Convert to Array first: Array.from(set)`
+				};
+			}
+			if (obj instanceof Map) {
+				return {
+					valid: false,
+					error: `Map at ${path}. Convert to Object first: Object.fromEntries(map)`
+				};
 			}
 
-			// Handle Set/Map collections
-			if (value instanceof Set) {
-				cleaned[key] = Array.from(value);
-			} else if (value instanceof Map) {
-				cleaned[key] = Object.fromEntries(value);
-			} else if (typeof value === 'object' && value !== null) {
-				cleaned[key] = this.stripDOMReferences(value);
-			} else {
-				cleaned[key] = value;
+			// Check all properties
+			for (const [key, value] of Object.entries(obj)) {
+				const result = this.validateSerializable(value, `${path}.${key}`);
+				if (!result.valid) return result;
+			}
+			return { valid: true };
+		}
+
+		return {
+			valid: false,
+			error: `Unknown type at ${path}: ${type}`
+		};
+	}
+
+	async delete(name, id) {
+		const store = this.stores.get(name);
+		store.data.delete(id);
+
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.delete(id);
+		}
+
+		this.notify(name, 'item-deleted', { id });
+	}
+
+	get(name, id) {
+		const store = this.stores.get(name);
+		return store.data.get(id);
+	}
+
+	getAll(name) {
+		const store = this.stores.get(name);
+		return Array.from(store.data.values());
+	}
+
+	getFiltered(name) {
+		const store = this.stores.get(name);
+		const cacheKey = this.generateCacheKey(store.filters);
+		const cacheEntry = store.cache.get(cacheKey);
+
+		if (cacheEntry && cacheEntry.items) {
+			return cacheEntry.items.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+		}
+
+		return this.getAll(name);
+	}
+
+	async clear(name) {
+		const store = this.stores.get(name);
+		store.data.clear();
+		store.cache.clear();
+
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.clear();
+		}
+
+		this.notify(name, 'data-cleared');
+	}
+
+	setFilter(name, key, value) {
+		const store = this.stores.get(name);
+		const oldValue = store.filters[key];
+
+		if (value === null || value === undefined || value === '') {
+			delete store.filters[key];
+		} else {
+			store.filters[key] = value;
+		}
+
+		this.notify(name, 'filters-changed', {
+			filters: store.filters,
+			changed: { key, oldValue, newValue: value }
+		});
+
+		if (store.config.endpoint) {
+			this.fetch(name);
+		}
+	}
+
+	async setFilters(name, filters) {
+		const store = this.stores.get(name);
+
+		const hasChanges = Object.keys(filters).some(
+			key => store.filters[key] !== filters[key]
+		);
+
+		if (!hasChanges) return;
+
+		store.filters = { ...store.filters, ...filters };
+
+		this.notify(name, 'filters-changed', {
+			filters: store.filters,
+			changed: filters
+		});
+
+		if (store.config.endpoint) {
+			await this.fetch(name);
+		}
+	}
+
+	removeFilter(name, key) {
+		const store = this.stores.get(name);
+		const oldValue = store.filters[key];
+
+		if (oldValue !== undefined) {
+			delete store.filters[key];
+
+			this.notify(name, 'filters-changed', {
+				filters: store.filters,
+				removed: { key, oldValue }
+			});
+
+			if (store.config.endpoint) {
+				this.fetch(name);
 			}
 		}
-
-		return cleaned;
 	}
 
-	/**
-	 * Check if a property is a DOM reference
-	 */
-	isDOMReference(key, value) {
-		// Check value types
-		if (value instanceof HTMLElement ||
-			value instanceof NodeList ||
-			value instanceof HTMLCollection ||
-			(value && value.nodeType !== undefined)) {
-			return true;
+	clearFilters(name) {
+		const store = this.stores.get(name);
+		const oldFilters = { ...store.filters };
+
+		store.filters = { ...store.config.filters };
+
+		this.notify(name, 'filters-cleared', {
+			oldFilters,
+			filters: store.filters
+		});
+
+		if (store.config.endpoint) {
+			this.fetch(name);
 		}
-
-		// Check key names - use exact match or word boundaries
-		const domKeys = ['element', 'el', 'dom', 'node', 'ui', 'container', 'wrapper'];
-		const lowerKey = key.toLowerCase();
-
-		// Only match if it's the exact key OR starts/ends with the pattern
-		if (domKeys.includes(lowerKey) ||
-			domKeys.some(k => lowerKey === k || lowerKey.startsWith(k + '_') || lowerKey.endsWith('_' + k))) {
-			return true;
-		}
-
-		return false;
 	}
 
-	/**
-	 * Get the key for an item based on configured keyPath
-	 */
-	getItemKey(item) {
-		if (typeof this.config.keyPath === 'function') {
-			return this.config.keyPath(item);
+	clearCache(name) {
+		const store = this.stores.get(name);
+		store.cache.clear();
+
+		if (store.db && store.db.objectStoreNames.contains('cache')) {
+			const tx = store.db.transaction(['cache'], 'readwrite');
+			const objectStore = tx.objectStore('cache');
+			objectStore.clear();
 		}
 
-		// Support nested keypaths like 'meta.id'
-		const keys = this.config.keyPath.split('.');
+		this.notify(name, 'cache-cleared');
+	}
+
+	clearHttpHeaders(name, cacheKey = null) {
+		const store = this.stores.get(name);
+
+		if (cacheKey) {
+			store.httpHeaders.delete(cacheKey);
+
+			if (store.db && store.db.objectStoreNames.contains('headers')) {
+				const tx = store.db.transaction(['headers'], 'readwrite');
+				const objectStore = tx.objectStore('headers');
+				objectStore.delete(cacheKey);
+			}
+		} else {
+			store.httpHeaders.clear();
+
+			if (store.db && store.db.objectStoreNames.contains('headers')) {
+				const tx = store.db.transaction(['headers'], 'readwrite');
+				const objectStore = tx.objectStore('headers');
+				objectStore.clear();
+			}
+		}
+	}
+
+	subscribe(name, callback) {
+		const subscribers = this.subscribers.get(name);
+		subscribers.add(callback);
+		return () => subscribers.delete(callback);
+	}
+
+	notify(name, event, data = {}) {
+		const subscribers = this.subscribers.get(name);
+		if (!subscribers) return;
+
+		subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error(`Subscriber error for store "${name}":`, error);
+			}
+		});
+	}
+
+	storeResponseHeaders(name, key, response) {
+		const store = this.stores.get(name);
+
+		const headers = {
+			key,
+			etag: response.headers.get('ETag'),
+			lastModified: response.headers.get('Last-Modified'),
+			timestamp: Date.now()
+		};
+
+		store.httpHeaders.set(key, headers);
+
+		if (store.db && store.db.objectStoreNames.contains('headers')) {
+			const tx = store.db.transaction(['headers'], 'readwrite');
+			const objectStore = tx.objectStore('headers');
+			objectStore.put(headers);
+		}
+	}
+
+	async saveToCache(name, key, data) {
+		const store = this.stores.get(name);
+		if (!store.db || !store.db.objectStoreNames.contains('cache')) return;
+
+		const tx = store.db.transaction(['cache'], 'readwrite');
+		const objectStore = tx.objectStore('cache');
+		await objectStore.put(data);
+	}
+
+	generateCacheKey(filters) {
+		const normalized = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+
+		return JSON.stringify(normalized);
+	}
+
+	isCacheValid(entry, ttl) {
+		if (!entry || !entry.timestamp) return false;
+		const age = Date.now() - entry.timestamp;
+		return age < ttl;
+	}
+
+	getItemKey(item, keyPath) {
+		if (typeof keyPath === 'function') {
+			return keyPath(item);
+		}
+
+		const keys = keyPath.split('.');
 		let value = item;
 
 		for (const key of keys) {
@@ -318,789 +990,31 @@
 		return value;
 	}
 
-	/**
-	 * Save a single item
-	 */
-	/**
-	 * Save a single item
-	 */
-	async save(item) {
-		const key = this.getItemKey(item);
-
-		// Keep ORIGINAL item in memory (with FormData intact)
-		this.data.set(key, item);  // ← Store original
-
-		// Create cleaned version ONLY for IndexedDB
-		let cleaned = { ...item };
-		if (cleaned.data instanceof FormData) {
-			cleaned.data = this.formDataToObject(cleaned.data);
-		}
-
-		if (this.config.stripDOMReferences) {
-			cleaned = this.stripDOMReferences(cleaned);
-		}
-
-		// Persist cleaned version to IndexedDB
-		await this.saveToDB(cleaned);
-
-		if(this.config.endpoint){
-			this.saveToServer(item);
-		}
-
-		this.notify('item-saved', { item: cleaned, key });
-
-		return cleaned;
-	}
-
-	/**
-	 * Convert FormData to plain object for storage
-	 */
-	formDataToObject(formData) {
-		const obj = {
-			_isFormData: true, // Flag to reconstruct later
-			entries: {}
-		};
-
-		for (const [key, value] of formData.entries()) {
-			// Skip File/Blob objects - they're stored separately
-			if (value instanceof File || value instanceof Blob) {
-				continue;
-			}
-
-			// Handle multiple values for same key
-			if (obj.entries[key]) {
-				if (!Array.isArray(obj.entries[key])) {
-					obj.entries[key] = [obj.entries[key]];
-				}
-				obj.entries[key].push(value);
-			} else {
-				obj.entries[key] = value;
-			}
-		}
-
-		return obj;
-	}
-
-	/**
-	 * Convert stored object back to FormData
-	 */
-	async objectToFormData(obj) {
-		if (!obj._isFormData) return obj;
-
-		const formData = new FormData();
-
-		for (const [key, value] of Object.entries(obj.entries)) {
-			if (Array.isArray(value)) {
-				value.forEach(v => formData.append(key, v));
-			} else {
-				formData.append(key, value);
-			}
-		}
-		// Restore files from external blob store (UploadManager)
-		if (this.config.getBlobs && obj.entries.upload_ids) {
-			const uploadIds = JSON.parse(obj.entries.upload_ids);
-			const blobs = await this.config.getBlobs(uploadIds);  // ← Await here
-
-			for (const blobData of blobs) {
-				if (blobData) {
-					const file = new File(
-						[blobData.data],
-						blobData.name,
-						{ type: blobData.type, lastModified: blobData.lastModified }
-					);
-					formData.append('files[]', file);
-				}
-			}
-		}
-
-		return formData;
-	}
-
-	/**
-	 * Save item to IndexedDB
-	 */
-	async saveToDB(item) {
-		if (!this.db) return;
-
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readwrite');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.put(item);
-
-			request.onsuccess = () => resolve();
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-	/**
-	 * Batch save multiple items
-	 */
-	async saveMany(items) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction([this.config.storeName], 'readwrite');
-		const store = tx.objectStore(this.config.storeName);
-
-		const promises = items.map(item => {
-			const cleaned = this.config.stripDOMReferences
-				? this.stripDOMReferences(item)
-				: item;
-
-			const key = this.getItemKey(cleaned);
-			this.data.set(key, cleaned);
-
-			return store.put(cleaned);
-		});
-
-		await Promise.all(promises);
-		this.notify('items-saved', { count: items.length });
-	}
-
-	/**
-	 * Get a single item
-	 */
-	get(key) {
-		return this.data.get(key);  // ← Returns original with FormData
-	}
-
-	/**
-	 * Get all items
-	 */
-	getAll() {
-		return Array.from(this.data.values());
-	}
-
-	/**
-	 * Delete an item
-	 */
-	async delete(key, storeName = null) {
-		this.data.delete(key);
-
-		if (!storeName) {
-			storeName = this.config.storeName;
-		}
-		if (this.db) {
-			const tx = this.db.transaction([storeName], 'readwrite');
-			const store = tx.objectStore(storeName);
-			await store.delete(key);
-		}
-
-		this.notify('item-deleted', { key });
-	}
-
-	async saveBlob(key, blob) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['blobs'], 'readwrite');
-		const store = tx.objectStore('blobs');
-		await store.put({
-			uploadId: key,  // Match keyPath
-			data: blob,
-			type: blob.type,
-			name: blob.name,
-			lastModified: blob.lastModified || Date.now()
-		});
-	}
-
-	async getBlob(key) {
-		if (!this.db) return null;
-
-		return new Promise(resolve => {
-			const tx = this.db.transaction(['blobs'], 'readonly');
-			const request = tx.objectStore('blobs').get(key);
-			request.onsuccess = () => resolve(request.result);
-			request.onerror = () => resolve(null);
-		});
-	}
-
-	/**
-	 * Clear all data
-	 */
-	async clear() {
-		this.data.clear();
-		this.cache.clear();
-		this.httpHeaders.clear();
-
-		if (this.domCache) {
-			this.domCache.clear();
-		}
-
-		if (this.db) {
-			const stores = [this.config.storeName];
-			if (this.config.endpoint) stores.push('cache');
-			if (this.config.useHttpCaching) stores.push('headers');
-
-			const tx = this.db.transaction(stores, 'readwrite');
-			stores.forEach(storeName => {
-				if (this.db.objectStoreNames.contains(storeName)) {
-					tx.objectStore(storeName).clear();
-				}
-			});
-		}
-
-		this.notify('data-cleared');
-	}
-
-	/**
-	 * Fetch data from server with HTTP caching
-	 */
-	async fetch(options = {}) {
-		if (!this.config.endpoint) {
-			throw new Error('No endpoint configured for fetch');
-		}
-		const {
-			filters = this.filters,
-			headers = {},
-		} = options;
-
-		if (this.config.required && this.filters[this.config.required] === ''){
-			console.log(this.config.storeName+ ': Not fetch as we don\'t have the required items');
-			return;
-		}
-
-		// PREVENT CONCURRENT FETCHES FOR SAME DATA
-		const cacheKey = this.generateCacheKey(filters);
-		console.log('CacheKey: ', cacheKey);
-
-		// If already fetching this exact query, return a promise that resolves when done
-		if (this.isFetching && this.currentCacheKey === cacheKey) {
-			return new Promise((resolve) => {
-				// Store multiple waiting promises if needed
-				if (!this.pendingFetches) {
-					this.pendingFetches = [];
-				}
-				this.pendingFetches.push(resolve);
-			});
-		}
-
-		this.isFetching = true;
-		this.currentCacheKey = cacheKey;
-		let fetchResult = null; // Capture result for pending fetches
-
-		if (this.config.showLoading) {
-			this.setLoading(true);
-		}
-
-		//Check Cached data
-		const cachedData = this.cache.get(cacheKey);
-		console.log('Cached Data: ', cachedData);
-		if (cachedData && this.isCacheValid(cachedData)) {
-			console.log('Returning cached data: ');
-			this.isFetching = false;
-			this.currentCacheKey = null;
-			if (this.config.showLoading) {
-				this.setLoading(false);
-			}
-			return cachedData.data;
-		}
-
-		// Build request headers with HTTP caching
-		const requestHeaders = {
-			...this.headers,
-			...headers
-		};
-
-		if (this.config.useHttpCaching) {
-			const httpCache = this.httpHeaders.get(cacheKey);
-			if (httpCache) {
-				if (httpCache.etag) {
-					requestHeaders['If-None-Match'] = httpCache.etag;
-				}
-				if (httpCache.lastModified) {
-					requestHeaders['If-Modified-Since'] = httpCache.lastModified;
-				}
-			}
-		}
-
-		// Build URL with filters
-		const cleanedFilters = this.cleanFilters(filters);
-		const params = new URLSearchParams(cleanedFilters);
-		const url = `${this.config.apiBase}${this.config.endpoint}${params.toString() ? '?' + params : ''}`;
-
-		try {
-			const response = await fetch(url, {
-				method: 'GET',
-				headers: requestHeaders
-			});
-
-			// Handle 304 Not Modified
-			if (response.status === 304 && cachedData) {
-				// Update timestamp but keep existing data
-				cachedData.timestamp = Date.now();
-				cachedData.fromCache = true;
-				cachedData.isError = false;
-				this.saveCache(cacheKey, cachedData);
-				console.log(this.config.storeName+' Data loaded from cache');
-				this.notify('data-loaded', cachedData);
-				fetchResult = cachedData.data;
-				return cachedData.data;
-			}
-
-			if (!response.ok) {
-				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
-			}
-
-			const data = await response.json();
-
-			// Store HTTP caching headers
-			if (this.config.useHttpCaching) {
-				this.storeResponseHeaders(cacheKey, response);
-			}
-
-			// Cache the response
-			const cacheEntry = {
-				key: cacheKey,
-				data: data,
-				timestamp: Date.now(),
-				endpoint: this.config.endpoint,
-				filters: filters
-			};
-			console.log(this.config.storeName + 'Fetched fresh from server');
-
-			this.cache.set(cacheKey, cacheEntry);
-			this.saveCache(cacheKey, cacheEntry);
-
-			let items = (Array.isArray(data)) ? data : data.items;
-			await this.saveMany(items);
-
-			this.notify('data-loaded', {
-				data: {
-					items: items,
-					...data
-				},
-				count: items.length,
-				filters: filters,
-				fromCache: false,
-				isError: false
-			});
-
-			fetchResult = data;
-			return data;
-
-		} catch (error) {
-			console.error('Fetch error:', error);
-
-			// Return cached data if available, even if expired
-			if (cachedData) {
-				console.warn('Using stale cache due to fetch error');
-				cachedData.isError = true;
-				this.notify('data-loaded', cachedData);
-				fetchResult = cachedData.data;
-				return cachedData.data;
-			}
-
-			throw error;
-		} finally {
-			if (this.config.showLoading) {
-				this.setLoading(false);
-			}
-
-			this.isFetching = false;
-			this.currentCacheKey = null;
-
-			// Resolve any pending fetches that were waiting
-			if (this.pendingFetches && this.pendingFetches.length > 0) {
-				this.pendingFetches.forEach(resolve => resolve(fetchResult));
-				this.pendingFetches = [];
-			}
-		}
-	}
-
-	/**
-	 * Fetch data from server with HTTP caching
-	 */
-	async saveToServer(item) {
-		if (!this.config.saveToServer || !jvbSettings.currentUser) {
-			return;
-		}
-		if (!this.config.endpoint && this.config.saveToServer) {
-			throw new Error('No endpoint configured for saving to server');
-		}
-
-		let requestBody;
-		let headers = this.config.headers;
-		headers['X-WP-Nonce'] = jvbSettings.nonce;
-		if (item instanceof FormData) {
-			item.append('user', jvbSettings.currentUser);
-			requestBody = item;
-
-			// console.log('Sending formData: ');
-			// for (const pair of requestBody.entries()) {
-			// 	console.log(pair[0], pair[1]);
-			// }
-		} else {
-			requestBody = JSON.stringify({
-				...item,
-				user: jvbSettings.currentUser
-			});
-			// console.log('Sending data: ', {
-			// 	...operation.data,
-			// 	id: operation.id,
-			// 	user: this.user
-			// });
-
-			headers['Content-Type'] = 'application/json';
-		}
-
-		const response = await fetch(
-			`${this.config.apiBase}${this.config.endpoint}`,
-			{
-				method: 'POST',
-				headers: headers,
-				body: requestBody
-			}
-		);
-
-		const result = await response.json();
-		this.notify(
-			'saved-to-server',
-			{
-				success: result.ok && result.success
-			}
-		);
-	}
-
-	cleanFilters(filters) {
-		const cleaned = {};
-		Object.entries(filters).forEach(([key, value]) => {
-			if (value !== null && value !== undefined && value !== '') {
-				// Handle special cases based on existing patterns
-				if (key === 'taxonomies' && typeof value === 'object') {
-					Object.entries(value).forEach(([taxName, terms]) => {
-						if (Array.isArray(terms) && terms.length > 0) {
-							cleaned[`tax_${taxName}`] = terms.join(',');
-						} else if (terms) {
-							cleaned[`tax_${taxName}`] = terms;
-						}
-					});
-				} else if (key === 'date' && typeof value === 'object') {
-					if (value.after) cleaned.after = value.after;
-					if (value.before) cleaned.before = value.before;
-				} else {
-					cleaned[key] = value;
-				}
-			}
-		});
-		return cleaned;
-	}
-
-	/**
-	 * Generate cache key from filters
-	 */
-	generateCacheKey(filters) {
-		if (this.config.cacheKeyStrategy === 'custom' && this.config.generateCacheKey) {
-			return this.config.generateCacheKey(filters);
-		}
-
-		// Default strategy: sort keys and create string
-		const sorted = Object.keys(filters)
-			.sort()
-			.reduce((acc, key) => {
-				acc[key] = filters[key];
-				return acc;
-			}, {});
-
-		return JSON.stringify(sorted);
-	}
-
-	setFilter(key, value) {
-		if (!this.filters) {
-			this.filters = {};
-		}
-		const oldValue = this.filters[key];
-		if (oldValue === value) {
-			return;
-		}else if (value === '' || value === null || value === undefined) {
-			delete this.filters[key];
-		} else {
-			this.filters[key] = value;
-		}
-
-		this.notify('filters-changed', {
-			filters: this.filters,
-			changed: { key, oldValue, newValue: value }
-		});
-
-		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint) {
-			window.debouncer.schedule(
-				this.config.endpoint,
-				this.fetch.bind(this),
-				100
-			);
-		}
-	}
-
-
-	/**
-	 * Remove a filter
-	 */
-	removeFilter(key) {
-		const oldValue = this.filters[key];
-
-		if (oldValue !== undefined) {
-			delete this.filters[key];
-			this.notify('filters-changed', {
-				filters: this.filters,
-				removed: { key, oldValue }
-			});
-
-			// Auto-fetch if endpoint is configured
-			if (this.config.endpoint) {
-				window.debouncer.schedule(
-					this.config.endpoint,
-					this.fetch.bind(this),
-					100
-				);
-			}
-		}
-	}
-
-	/**
-	 * Clear all filters
-	 */
-	clearFilters() {
-		const oldFilters = { ...this.filters };
-		//Restore baseline filters
-		this.filters = this.config.filters;
-
-		this.notify('filters-cleared', {
-			oldFilters,
-			filters: this.filters
-		});
-
-		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint) {
-			this.fetch();
-		}
-	}
-
-	/**
-	 * Set multiple filters at once
-	 */
-	async setFilters(filters) {
-		const hasChanges = Object.keys(filters).some(
-			key => this.filters[key] !== filters[key]
-		);
-
-		if (!hasChanges) {
-			return;
-		}
-
-		this.filters = { ...this.filters, ...filters };
-
-		this.notify('filters-changed', {
-			filters: this.filters,
-			changed: filters,
-		});
-
-		// Only fetch if endpoint configured
-		if (this.config.endpoint) {
-			window.debouncer.schedule(
-				this.config.endpoint,
-				this.fetch.bind(this),
-				100
-			);
-		}
-	}
-
-	/**
-	 * Check if cache entry is still valid
-	 */
-	isCacheValid(cacheEntry) {
-		if (!cacheEntry || !cacheEntry.timestamp) return false;
-
-		const age = Date.now() - cacheEntry.timestamp;
-		return age < this.config.TTL;
-	}
-
-	/**
-	 * Store HTTP response headers for caching
-	 */
-	storeResponseHeaders(key, response) {
-		const headers = {
-			key,
-			etag: response.headers.get('ETag'),
-			lastModified: response.headers.get('Last-Modified'),
-			timestamp: Date.now()
-		};
-
-		this.httpHeaders.set(key, headers);
-
-		if (this.db && this.db.objectStoreNames.contains('headers')) {
-			const tx = this.db.transaction(['headers'], 'readwrite');
-			const store = tx.objectStore('headers');
-			store.put(headers);
-		}
-	}
-
-	/**
-	 * Save cache entry to IndexedDB
-	 */
-	async saveCache(key, data) {
-		if (!this.db || !this.db.objectStoreNames.contains('cache')) return;
-
-		const tx = this.db.transaction(['cache'], 'readwrite');
-		const store = tx.objectStore('cache');
-		await store.put(data);
-	}
-
-	/**
-	 * Load cache from IndexedDB
-	 */
-	async loadCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['cache'], 'readonly');
-			const store = tx.objectStore('cache');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					if (this.isCacheValid(item)) {
-						this.cache.set(item.key, item);
-					}
-				});
-				resolve();
-			};
-		});
-	}
-
-	/**
-	 * Load HTTP headers from IndexedDB
-	 */
-	async loadHeaders() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['headers'], 'readonly');
-			const store = tx.objectStore('headers');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(header => {
-					this.httpHeaders.set(header.key, header);
-				});
-				resolve();
-			};
-		});
-	}
-
-
-	/**
-	 * Subscribe to store events
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	/**
-	 * Notify subscribers of events
-	 */
-	notify(event, data = {}) {
-		this.subscribers.forEach(callback => {
-			try {
-				callback(event, data);
-			} catch (error) {
-				console.error('Subscriber error:', error);
-			}
-		});
-	}
-
-	/**
-	 * Query items using an index
-	 */
-	async query(indexName, value) {
-		if (!this.db) return [];
-
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-
-			if (!store.indexNames.contains(indexName)) {
-				reject(new Error(`Index ${indexName} does not exist`));
-				return;
-			}
-
-			const index = store.index(indexName);
-			const request = value !== undefined
-				? index.getAll(value)
-				: index.getAll();
-
-			request.onsuccess = (e) => {
-				const results = e.target.result.map(item => {
-					return this.config.stripDOMReferences
-						? this.stripDOMReferences(item)
-						: item;
-				});
-				resolve(results);
-			};
-
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-	/**
-	 * Count items in store
-	 */
-	async count() {
-		if (!this.db) return this.data.size;
-
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.count();
-
-			request.onsuccess = (e) => resolve(e.target.result);
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-
 	setLoading(on) {
-		console.log('Setting Loading ' + (on) ? 'on' : 'off' + ' from '.this.config.storeName);
 		this.body.classList.toggle('loading', on);
 		if (on) {
-			this.loading.showModal();
+			this.loading?.showModal();
 		} else {
-			this.loading.close();
+			this.loading?.close();
 		}
-
 	}
 
-	/**
-	 * Cleanup and destroy
-	 */
 	destroy() {
-		if (this.currentRequest) {
-			this.currentRequest.abort();
-		}
+		this.stores.forEach(store => {
+			if (store.currentRequest) {
+				store.currentRequest.abort();
+			}
+		});
 
+		this.databases.forEach(db => db.close());
+		this.stores.clear();
 		this.subscribers.clear();
-		this.data.clear();
-		this.cache.clear();
-		this.httpHeaders.clear();
-
-		if (this.db) {
-			this.db.close();
-			this.db = null;
-		}
-	}
-
-	clearCache() {
-		this.cache.clear();
-
-		if (this.db) {
-			const tx = this.db.transaction(['cache'], 'readwrite');
-			const store = tx.objectStore('cache');
-			store.clear();
-		}
-
-		this.notify('cache-cleared');
+		this.databases.clear();
+		this.pendingInits.clear();
 	}
 }
 
-// Export for use
-window.jvbStore = DataStore;
+// Initialize singleton on DOMContentLoaded
+document.addEventListener('DOMContentLoaded', function() {
+	window.jvbStore = new DataStore();
+});
diff --git a/assets/js/concise/DataStoreOld.js b/assets/js/concise/DataStoreOld.js
index 3686ac2..dfb1f8b 100644
--- a/assets/js/concise/DataStoreOld.js
+++ b/assets/js/concise/DataStoreOld.js
@@ -1,578 +1,773 @@
 /**
- * Handles GET Requests, storing responses by a key of filters, set with setFilter method
- * Stores:
- * 		- Items: the individual item data, mapped by postID/termID
- * 		- Cache: the cacheKey generated by filters, and the results in the value
- * 		- httpHeaders: If Modified Since tracking
- * 		- domCache: rendered DOM elements, to reduce on-page rendering
+ * ExtendedDataStore - A flexible IndexedDB wrapper with HTTP caching
+ *
+ * Configuration-based approach for different storage needs:
+ * - Configurable endpoint, keyPath, and indexes
+ * - Built-in ETag and If-Modified-Since support
+ * - Automatic DOM reference stripping
+ * - TTL-based cache invalidation
+ *
+ * All notifications:
+ *
+  		this.store.subscribe((event, data) => {
+  			switch (event) {
+  				case 'data-loaded':
+  					break;
+  				case 'item-saved':
+  					break;
+  				case 'items-saved':
+  					break;
+  				case 'item-deleted':
+  					break;
+  				case 'data-cleared':
+  					break;
+  				case 'filters-changed':
+  					break;
+  				case 'filters-cleared':
+  					break;
+  				case 'cache-cleared':
+  					break;
+  			}
+  		});
  */
 class DataStore {
 	constructor(config = {}) {
+		// Core configuration with sensible defaults
 		this.config = {
+			// Storage configuration
 			name: 'default',
-			endpoint: false,
+			version: 1,
+			storeName: 'items',
+			keyPath: 'id',
+			indexes: [], // Array of {name, keyPath, unique}
+
+			// API configuration
+			endpoint: null,
+			saveToServer: false,
 			apiBase: jvbSettings.api,
-			TTL: 3600000, // 1 hour default
-			showLoading: true,
 			headers: {},
 			filters: {},
+			required:  null, //any required filters before fetching
+			icon: null,
+			getBlobs: null,
+
+			// Cache configuration
+			TTL: 3600000, // 1 hour default
+			useHttpCaching: true, // ETag and If-Modified-Since
+			cacheKeyStrategy: 'filters', // How to generate cache keys
+
+			// UI configuration
+			showLoading: true,
+
+			// Features
+			stripDOMReferences: true,
+			storeBlobs: false,
+			delayFetch: false,
+
 			...config
 		};
-		if (!this.config.endpoint) {
-			console.warn('No endpoint set. Only saving locally');
-		}
+
+		// Initialize base properties
+		this.db = null;
+		this.data = new Map();
+		this.cache = new Map();
+		this.isFetching = false;
+		this.pendingFetch = null;
+		this.httpHeaders = new Map();
+		this.subscribers = new Set();
+		this.currentRequest = null;
+		this.filters = this.config.filters??{};
+
+		// Set up headers
+		this.headers = {
+			'X-WP-Nonce': jvbSettings?.nonce,
+			...this.config.headers
+		};
 
 		this.body = document.body;
 		this.loading = document.querySelector('dialog.loading');
 
-		this.headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			...this.config.headers
-		};
-
-		// Data stores
-		this.items = new Map();
-		this.cache = new Map(); //TODO: call this resultsCache
-		this.httpHeaders = new Map();
-		this.domCache = new Map();
-		this.forms = new Map();
-
-		// State management
-		this.filters = config.filters ?? {};
-		this.subscribers = new Set();
-		this.db = null;
-		this.currentRequest = null;
-
-		// Server Timestamps - needed?
-		this.cachedContent = JSON.parse(cacheJVB.cache) || {};
-		this.lastTimestampUpdate = Date.now();
-
-		this.initDB();
-		document.addEventListener('beforeUnload', () =>this.destroy());
+		this._initialized = false;
+		// Cleanup on page unload
+		window.addEventListener('beforeunload', () => this.destroy());
 	}
 
-	async initDB() {
-		if (!('indexedDB' in window)) return;
+	async init() {
+		if (this._initialized) return;
+		await this.initDB();
+		this._initialized = true;
+	}
 
-		const request = indexedDB.open(`jvb_${this.config.name}_db`, 1);
+	/**
+	 * Initialize IndexedDB with configurable schema
+	 */
+	async initDB() {
+		if (!('indexedDB' in window)) {
+			console.warn('IndexedDB not supported');
+			return;
+		}
+
+		const dbName = `jvb_${this.config.name}_db`;
+		const request = indexedDB.open(dbName, this.config.version);
 
 		request.onupgradeneeded = (e) => {
 			const db = e.target.result;
-			// Items store
-			if (!db.objectStoreNames.contains('items')) {
-				db.createObjectStore('items', { keyPath: 'id' });
-			}
 
-			// DOM cache for rendered elements
-			if (!db.objectStoreNames.contains('dom')) {
-				db.createObjectStore('dom', { keyPath: 'id' });
-			}
+			// Create main store with configurable keyPath
 
-			if (!db.objectStoreNames.contains('forms')) {
-				let forms = db.createObjectStore('forms', {
-					keyPath: 'formId',
+			if (!db.objectStoreNames.contains(this.config.storeName)) {
+				const store = db.createObjectStore(this.config.storeName, {
+					keyPath: this.config.keyPath
 				});
-				forms.createIndex('status', 'status', {unique:false});
-				forms.createIndex('operationId', 'operationId', {unique:false});
-				forms.createIndex('timestamp', 'timestamp', {unique:false});
+
+				// Add configured indexes
+				this.config.indexes.forEach(index => {
+					store.createIndex(
+						index.name,
+						index.keyPath || index.name,
+						{ unique: index.unique || false }
+					);
+				});
 			}
 
-			// Cache store for GET requests with endpoint index
-			if (!db.objectStoreNames.contains('cache')) {
+			// Cache store for HTTP responses
+			if (this.config.endpoint && !db.objectStoreNames.contains('cache')) {
 				const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
 				cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
 				cacheStore.createIndex('endpoint', 'endpoint', { unique: false });
 				cacheStore.createIndex('filters', 'filters', { unique: false });
 			}
 
-			// HTTP headers store
-			if (!db.objectStoreNames.contains('headers')) {
+			// HTTP headers store for ETag/If-Modified-Since
+			if (this.config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
 				db.createObjectStore('headers', { keyPath: 'key' });
 			}
+
+			if (this.config.storeBlobs && !db.objectStoreNames.contains('blobs')) {
+				db.createObjectStore('blobs', { keyPath: 'uploadId' });
+			}
+
+			// Call optional schema extension
+			if (this.config.onUpgrade) {
+				this.config.onUpgrade(db, e.oldVersion, e.newVersion);
+			}
+
+		};
+
+		request.onsuccess = async (e) => {
+			this.db = e.target.result;
+
+			// Load in background without blocking
+			this.loadInBackground();
+
+			this.notify('db-init');
+
+			// Only fetch if explicitly needed
+			if (this.config.endpoint && !this.config.delayFetch) {
+				requestIdleCallback(() => this.fetch(), { timeout: 2000 });
+			}
 		};
 
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFromDB();
-		};
-
 		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
+			console.error(`IndexedDB error for ${dbName}:`, e);
+			if (this.config.onError) {
+				this.config.onError(e);
+			}
 		};
 	}
 
-	async loadFromDB() {
-		if (!this.db) return;
-
-		try {
-			await Promise.all([
-				this.loadItems(),
-				this.loadCache(),
-				this.loadHeaders(),
-				this.loadDOMCache(),
-				this.loadForms()
-			]);
-		} catch (error) {
-			console.error('Error loading from DB:', error);
-		}
-	}
-
-	async loadItems() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['items'], 'readonly');
-			const store = tx.objectStore('items');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					this.items.set(item.id, item);
-				});
-				this.notify('items-loaded', { items: Array.from(this.items.values()) });
-				resolve();
-			};
-		});
-	}
-
-	async loadCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['cache'], 'readonly');
-			const store = tx.objectStore('cache');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					if (this.isCacheValid(item)) {
-						this.cache.set(item.key, item);
-					}
-				});
-				resolve();
-			};
-		});
-	}
-
-	async loadHeaders() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['headers'], 'readonly');
-			const store = tx.objectStore('headers');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(header => {
-					this.httpHeaders.set(header.key, header);
-				});
-				resolve();
-			};
-		});
-	}
-
-	async loadDOMCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['dom'], 'readonly');
-			const store = tx.objectStore('dom');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(domEntry => {
-					// Convert stored HTML back to DOM elements
-					const reconstructed = {};
-					Object.entries(domEntry.views).forEach(([viewName, html]) => {
-						const temp = document.createElement('div');
-						temp.innerHTML = html;
-						reconstructed[viewName] = temp.firstElementChild;
-					});
-					this.domCache.set(domEntry.id, reconstructed);
-				});
-				resolve();
-			};
-		});
-	}
-
-	async loadForms() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['forms'], 'readonly');
-			const store = tx.objectStore('forms');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(form => {
-					this.forms.set(form.key, form);
-				});
-				resolve();
-			};
-		});
-	}
-
-	setLoading(on) {
-		this.body.classList.toggle('loading', on);
-		if (on) {
-			this.loading.showModal();
-		} else {
-			this.loading.close();
-		}
-
+	loadInBackground() {
+		// Non-blocking background load
+		Promise.all([
+			this.loadFromDB(),
+			this.loadCache(),
+			this.loadHeaders()
+		]).then(() => {
+			this.notify('data-ready');
+		}).catch(console.error);
 	}
 
 	/**
-	 * Main fetch method with caching and conditional requests
+	 * Load all data from IndexedDB
 	 */
-	async fetch(endpoint = null, options = {}) {
+	async loadFromDB() {
+		if (!this.db) return;
+
+		return new Promise(async (resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.getAll();
+
+			request.onsuccess = async (e) => {
+				const items = e.target.result;
+				console.log('fetched from cache');
+				for (const item of items) {
+					if (item.data?._isFormData && this.config.getBlobs) {
+						item.data = await this.objectToFormData(item.data);
+					}
+					const key = this.getItemKey(item);
+					this.data.set(key, item);
+				}
+
+				this.notify('data-loaded', { count: items.length });
+				resolve(items);
+			};
+
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+
+
+	/**
+	 * Load main data from IndexedDB
+	 */
+	async loadData() {
+		if (!this.db) return;
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					const key = this.getItemKey(item);
+					this.data.set(key, item);
+				});
+				resolve();
+			};
+
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+	/**
+	 * Strip DOM references from an object (recursive)
+	 */
+	stripDOMReferences(obj) {
+		if (!obj || typeof obj !== 'object') return obj;
+
+		// Handle arrays
+		if (Array.isArray(obj)) {
+			return obj.map(item => this.stripDOMReferences(item));
+		}
+
+		// Handle objects
+		const cleaned = {};
+		for (const [key, value] of Object.entries(obj)) {
+			// Skip DOM-related properties
+			if (this.isDOMReference(key, value)) {
+				continue;
+			}
+
+			// Handle Set/Map collections
+			if (value instanceof Set) {
+				cleaned[key] = Array.from(value);
+			} else if (value instanceof Map) {
+				cleaned[key] = Object.fromEntries(value);
+			} else if (typeof value === 'object' && value !== null) {
+				cleaned[key] = this.stripDOMReferences(value);
+			} else {
+				cleaned[key] = value;
+			}
+		}
+
+		return cleaned;
+	}
+
+	/**
+	 * Check if a property is a DOM reference
+	 */
+	isDOMReference(key, value) {
+		// Check value types
+		if (value instanceof HTMLElement ||
+			value instanceof NodeList ||
+			value instanceof HTMLCollection ||
+			(value && value.nodeType !== undefined)) {
+			return true;
+		}
+
+		// Check key names - use exact match or word boundaries
+		const domKeys = ['element', 'el', 'dom', 'node', 'ui', 'container', 'wrapper'];
+		const lowerKey = key.toLowerCase();
+
+		// Only match if it's the exact key OR starts/ends with the pattern
+		if (domKeys.includes(lowerKey) ||
+			domKeys.some(k => lowerKey === k || lowerKey.startsWith(k + '_') || lowerKey.endsWith('_' + k))) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Get the key for an item based on configured keyPath
+	 */
+	getItemKey(item) {
+		if (typeof this.config.keyPath === 'function') {
+			return this.config.keyPath(item);
+		}
+
+		// Support nested keypaths like 'meta.id'
+		const keys = this.config.keyPath.split('.');
+		let value = item;
+
+		for (const key of keys) {
+			value = value?.[key];
+		}
+
+		return value;
+	}
+
+	/**
+	 * Save a single item
+	 */
+	/**
+	 * Save a single item
+	 */
+	async save(item) {
+		const key = this.getItemKey(item);
+
+		// Keep ORIGINAL item in memory (with FormData intact)
+		this.data.set(key, item);  // ← Store original
+
+		// Create cleaned version ONLY for IndexedDB
+		let cleaned = { ...item };
+		if (cleaned.data instanceof FormData) {
+			cleaned.data = this.formDataToObject(cleaned.data);
+		}
+
+		if (this.config.stripDOMReferences) {
+			cleaned = this.stripDOMReferences(cleaned);
+		}
+
+		// Persist cleaned version to IndexedDB
+		await this.saveToDB(cleaned);
+
+		if(this.config.saveToServer && this.config.endpoint){
+			this.saveToServer(item);
+		}
+
+		this.notify('item-saved', { item: cleaned, key });
+
+		return cleaned;
+	}
+
+	/**
+	 * Convert FormData to plain object for storage
+	 */
+	formDataToObject(formData) {
+		const obj = {
+			_isFormData: true, // Flag to reconstruct later
+			entries: {}
+		};
+
+		for (const [key, value] of formData.entries()) {
+			// Skip File/Blob objects - they're stored separately
+			if (value instanceof File || value instanceof Blob) {
+				continue;
+			}
+
+			// Handle multiple values for same key
+			if (obj.entries[key]) {
+				if (!Array.isArray(obj.entries[key])) {
+					obj.entries[key] = [obj.entries[key]];
+				}
+				obj.entries[key].push(value);
+			} else {
+				obj.entries[key] = value;
+			}
+		}
+
+		return obj;
+	}
+
+	/**
+	 * Convert stored object back to FormData
+	 */
+	async objectToFormData(obj) {
+		if (!obj._isFormData) return obj;
+
+		const formData = new FormData();
+
+		for (const [key, value] of Object.entries(obj.entries)) {
+			if (Array.isArray(value)) {
+				value.forEach(v => formData.append(key, v));
+			} else {
+				formData.append(key, value);
+			}
+		}
+		// Restore files from external blob store (UploadManager)
+		if (this.config.getBlobs && obj.entries.upload_ids) {
+			const uploadIds = JSON.parse(obj.entries.upload_ids);
+			const blobs = await this.config.getBlobs(uploadIds);  // ← Await here
+
+			for (const blobData of blobs) {
+				if (blobData) {
+					const file = new File(
+						[blobData.data],
+						blobData.name,
+						{ type: blobData.type, lastModified: blobData.lastModified }
+					);
+					formData.append('files[]', file);
+				}
+			}
+		}
+
+		return formData;
+	}
+
+	/**
+	 * Save item to IndexedDB
+	 */
+	async saveToDB(item) {
+		if (!this.db) return;
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readwrite');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.put(item);
+
+			request.onsuccess = () => resolve();
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+	/**
+	 * Batch save multiple items
+	 */
+	async saveMany(items) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction([this.config.storeName], 'readwrite');
+		const store = tx.objectStore(this.config.storeName);
+
+		const promises = items.map(item => {
+			const cleaned = this.config.stripDOMReferences
+				? this.stripDOMReferences(item)
+				: item;
+
+			const key = this.getItemKey(cleaned);
+			this.data.set(key, cleaned);
+
+			return store.put(cleaned);
+		});
+
+		await Promise.all(promises);
+		this.notify('items-saved', { count: items.length });
+	}
+
+	/**
+	 * Get a single item
+	 */
+	get(key) {
+		return this.data.get(key);  // ← Returns original with FormData
+	}
+
+	/**
+	 * Get all items
+	 */
+	getAll() {
+		return Array.from(this.data.values());
+	}
+
+	/**
+	 * Delete an item
+	 */
+	async delete(key, storeName = null) {
+		this.data.delete(key);
+
+		if (!storeName) {
+			storeName = this.config.storeName;
+		}
+		if (this.db) {
+			const tx = this.db.transaction([storeName], 'readwrite');
+			const store = tx.objectStore(storeName);
+			await store.delete(key);
+		}
+
+		this.notify('item-deleted', { key });
+	}
+
+	async saveBlob(key, blob) {
+		if (!this.db) return;
+
+		const tx = this.db.transaction(['blobs'], 'readwrite');
+		const store = tx.objectStore('blobs');
+		await store.put({
+			uploadId: key,  // Match keyPath
+			data: blob,
+			type: blob.type,
+			name: blob.name,
+			lastModified: blob.lastModified || Date.now()
+		});
+	}
+
+	async getBlob(key) {
+		if (!this.db) return null;
+
+		return new Promise(resolve => {
+			const tx = this.db.transaction(['blobs'], 'readonly');
+			const request = tx.objectStore('blobs').get(key);
+			request.onsuccess = () => resolve(request.result);
+			request.onerror = () => resolve(null);
+		});
+	}
+
+	/**
+	 * Clear all data
+	 */
+	async clear() {
+		this.data.clear();
+		this.cache.clear();
+		this.httpHeaders.clear();
+
+		if (this.domCache) {
+			this.domCache.clear();
+		}
+
+		if (this.db) {
+			const stores = [this.config.storeName];
+			if (this.config.endpoint) stores.push('cache');
+			if (this.config.useHttpCaching) stores.push('headers');
+
+			const tx = this.db.transaction(stores, 'readwrite');
+			stores.forEach(storeName => {
+				if (this.db.objectStoreNames.contains(storeName)) {
+					tx.objectStore(storeName).clear();
+				}
+			});
+		}
+
+		this.notify('data-cleared');
+	}
+
+	/**
+	 * Fetch data from server with HTTP caching
+	 */
+	async fetch(options = {}) {
+		if (!this.config.endpoint) {
+			throw new Error('No endpoint configured for fetch');
+		}
+		await this.init(); // Lazy init
 		const {
 			filters = this.filters,
 			headers = {},
 		} = options;
 
+		if (this.config.required && this.filters[this.config.required] === ''){
+			console.log(this.config.storeName+ ': Not fetch as we don\'t have the required items');
+			return;
+		}
+
+		// PREVENT CONCURRENT FETCHES FOR SAME DATA
+		const cacheKey = this.generateCacheKey(filters);
+
+		// If already fetching this exact query, return a promise that resolves when done
+		if (this.isFetching && this.currentCacheKey === cacheKey) {
+			return new Promise((resolve) => {
+				// Store multiple waiting promises if needed
+				if (!this.pendingFetches) {
+					this.pendingFetches = [];
+				}
+				this.pendingFetches.push(resolve);
+			});
+		}
+
+
+		this.isFetching = true;
+		this.currentCacheKey = cacheKey;
+		let fetchResult = null; // Capture result for pending fetches
+
 		if (this.config.showLoading) {
 			this.setLoading(true);
 		}
 
+		//Check Cached data
+		const cachedData = this.cache.get(cacheKey);
+		if (cachedData && this.isCacheValid(cachedData)) {
+			this.isFetching = false;
 
-		// Use provided endpoint or config endpoint
-		const apiEndpoint = endpoint || this.config.endpoint;
-		if (!apiEndpoint) {
-			throw new Error('No endpoint specified');
+			if (this.config.showLoading) {
+				this.setLoading(false);
+			}
+			this.notify('data-loaded', cachedData.data);
+			return cachedData.data;
 		}
 
-		// Generate cache key from endpoint and filters
-		const cacheKey = this.generateCacheKey(apiEndpoint, filters);
-		const cleanedFilters = this.cleanFilters(filters);
-
-		// Build request URL
-		const params = new URLSearchParams(cleanedFilters);
-		const url = `${this.config.apiBase}${apiEndpoint}${params.toString() ? '?' + params : ''}`;
-
-		// Prepare headers with conditional requests
+		// Build request headers with HTTP caching
 		const requestHeaders = {
 			...this.headers,
 			...headers
 		};
 
-		// Add conditional headers from stored data
-		const headerKey = this.generateHeaderKey(url);
-		const storedHeaders = this.httpHeaders.get(headerKey);
-		const cachedData = this.cache.get(cacheKey);
-
-		if (storedHeaders && cachedData) {
-			if (storedHeaders.etag) {
-				requestHeaders['If-None-Match'] = storedHeaders.etag;
-			}
-			if (storedHeaders.lastModified) {
-				requestHeaders['If-Modified-Since'] = storedHeaders.lastModified;
+		if (this.config.useHttpCaching) {
+			const httpCache = this.httpHeaders.get(cacheKey);
+			if (httpCache) {
+				if (httpCache.etag) {
+					requestHeaders['If-None-Match'] = httpCache.etag;
+				}
+				if (httpCache.lastModified) {
+					requestHeaders['If-Modified-Since'] = httpCache.lastModified;
+				}
 			}
 		}
 
+		// Build URL with filters
+		const cleanedFilters = this.cleanFilters(filters);
+		const params = new URLSearchParams(cleanedFilters);
+		const url = `${this.config.apiBase}${this.config.endpoint}${params.toString() ? '?' + params : ''}`;
+
 		try {
 			const response = await fetch(url, {
 				method: 'GET',
 				headers: requestHeaders
 			});
 
-			// Handle 304 Not Modified - return cached data
-			if (response.status === 304) {
-				console.debug(`304 Not Modified for ${url}`);
-				if (cachedData) {
-					// Update timestamp but keep data
-					cachedData.timestamp = Date.now();
-					this.cache.set(cacheKey, cachedData);
-					await this.saveCacheToDB(cacheKey, cachedData);
+			// Handle 304 Not Modified
+			if (response.status === 304 && cachedData) {
+				console.log('304 response');
+				// Update timestamp but keep existing data
+				cachedData.timestamp = Date.now();
+				cachedData.fromCache = true;
+				cachedData.isError = false;
+				this.saveCache(cacheKey, cachedData);
 
-					// Store current request info
-					this.currentRequest = {
-						filters: cleanedFilters,
-						data: cachedData.data,
-						cached: true
-					};
-
-					//TODO: should this be items-loaded?
-					this.notify('data-cached', {
-						data: cachedData.data,
-						filters: cleanedFilters,
-						cached: true
-					});
-					return cachedData.data;
-				}
+				this.lastResponse = cachedData;
+				this.notify('data-loaded', cachedData);
+				fetchResult = cachedData.data;
+				return cachedData.data;
 			}
 
 			if (!response.ok) {
 				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 			}
 
-			// Store response headers for future conditional requests
-			this.storeResponseHeaders(headerKey, response);
-
 			const data = await response.json();
 
+			//Store full response for accesss to metadata, like stats
+			this.lastResponse = data;
+
+			// Store HTTP caching headers
+			if (this.config.useHttpCaching) {
+				this.storeResponseHeaders(cacheKey, response);
+			}
+
 			// Cache the response
 			const cacheEntry = {
 				key: cacheKey,
-				endpoint: apiEndpoint,
-				data: data,
+				items: data.items.map(item => item.id),
+				total: data.total,
+				maxPages: data['total_pages'],
 				timestamp: Date.now(),
-				filters: cleanedFilters
+				endpoint: this.config.endpoint,
+				filters: filters
 			};
 
 			this.cache.set(cacheKey, cacheEntry);
-			await this.saveCacheToDB(cacheKey, cacheEntry);
+			this.saveCache(cacheKey, cacheEntry);
 
-			// Update items if data contains them
-			if (data.items && this.config.endpoint === apiEndpoint) {
-				this.updateItems(data.items);
-			}
+			let items = (Array.isArray(data)) ? data : data.items;
+			await this.saveMany(items);
 
-			// Store current request info
-			this.currentRequest = {
-				filters: cleanedFilters,
-				data: data,
-				cached: false
-			};
-
-			this.notify('data-fetched', {
-				endpoint: apiEndpoint,
-				data: data,
-				filters: cleanedFilters
+			this.notify('data-loaded', {
+				data: {
+					items: items,
+					...data
+				},
+				count: items.length,
+				filters: filters,
+				fromCache: false,
+				isError: false
 			});
+
+			fetchResult = data;
 			return data;
 
 		} catch (error) {
 			console.error('Fetch error:', error);
 
-			// Try to return stale cache on error
+			// Return cached data if available, even if expired
 			if (cachedData) {
-				console.warn('Returning stale cache due to fetch error');
-				this.currentRequest = {
-					filters: cleanedFilters,
-					data: cachedData.data,
-					cached: true,
-					stale: true
-				};
-				this.notify('stale-cache-used', {
-					data: cachedData.data,
-					filters: cleanedFilters
-				});
+				console.warn('Using stale cache due to fetch error');
+				cachedData.isError = true;
+				this.notify('data-loaded', cachedData);
+				fetchResult = cachedData.data;
 				return cachedData.data;
 			}
 
-			this.notify('fetch-error', { error, filters: cleanedFilters });
 			throw error;
 		} finally {
 			if (this.config.showLoading) {
 				this.setLoading(false);
 			}
+
+			this.isFetching = false;
+			this.currentCacheKey = null;
+
+			// Resolve any pending fetches that were waiting
+			if (this.pendingFetches && this.pendingFetches.length > 0) {
+				this.pendingFetches.forEach(resolve => resolve(fetchResult));
+				this.pendingFetches = [];
+			}
 		}
 	}
 
 	/**
-	 * Update items in local store
+	 * Fetch data from server with HTTP caching
 	 */
-	updateItems(items) {
-		this.items.clear();
-		items.forEach(item => {
-			this.items.set(item.id, item);
-		});
-		this.saveItemsToDB();
-		this.notify('items-updated', { items });
-	}
-
-	/**
-	 * Get current request data and state
-	 */
-	getCurrentRequest() {
-		return this.currentRequest;
-	}
-
-	/**
-	 * Get a specific item by ID
-	 */
-	getItem(id) {
-		let check = parseInt(id);
-		id = isNaN(check) ? id : check;
-		const item = this.items.get(id);
-		return item ? this.unserializeData(item) : null;
-	}
-
-	setItem(id, data, mergeExisting = true) {
-		if (mergeExisting && this.items.has(id)) {
-			let existing = this.getItem(id); // Get unserialized version
-			data = window.deepMerge(existing, data);
+	async saveToServer(item) {
+		if (!this.config.saveToServer || !jvbSettings.currentUser) {
+			return;
+		}
+		if (!this.config.endpoint && this.config.saveToServer) {
+			throw new Error('No endpoint configured for saving to server');
 		}
 
-		const serialized = this.serializeData(data);
-		this.items.set(id, serialized); // Store serialized version
-		this.saveItemsToDB();
-		this.notify('item-stored', data); // Notify with original data
-		return data;
-	}
+		let requestBody;
+		let headers = this.config.headers;
+		headers['X-WP-Nonce'] = jvbSettings.nonce;
+		if (item instanceof FormData) {
+			item.append('user', jvbSettings.currentUser);
+			requestBody = item;
 
-	hasUnrecoverableFiles(data) {
-		if (!data || typeof data !== 'object') return false;
+			// console.log('Sending formData: ');
+			// for (const pair of requestBody.entries()) {
+			// 	console.log(pair[0], pair[1]);
+			// }
+		} else {
+			requestBody = JSON.stringify({
+				...item,
+				user: jvbSettings.currentUser
+			});
+			// console.log('Sending data: ', {
+			// 	...operation.data,
+			// 	id: operation.id,
+			// 	user: this.user
+			// });
 
-		if (data._wasFile || data._wasBlob) return true;
-
-		if (Array.isArray(data)) {
-			return data.some(item => this.hasUnrecoverableFiles(item));
+			headers['Content-Type'] = 'application/json';
 		}
 
-		if (data instanceof FormData) {
-			for (const [key, value] of data.entries()) {
-				if (value instanceof File || value instanceof Blob) return true;
+		const response = await fetch(
+			`${this.config.apiBase}${this.config.endpoint}`,
+			{
+				method: 'POST',
+				headers: headers,
+				body: requestBody
 			}
-			return false;
-		}
+		);
 
-		return Object.values(data).some(value => this.hasUnrecoverableFiles(value));
-	}
-
-	serializeFormData(formData) {
-		const obj = {};
-
-		for (const [key, value] of formData.entries()) {
-			// Handle file metadata (can't store actual file)
-			if (value instanceof File) {
-				continue;
+		const result = await response.json();
+		this.notify(
+			'saved-to-server',
+			{
+				success: result.ok && result.success
 			}
-			// Check if key already exists (for multiple values)
-			if (key in obj) {
-				// Convert to array if not already
-				if (!Array.isArray(obj[key])) {
-					obj[key] = [obj[key]];
-				}
-				obj[key].push(value);
-			} else {
-				obj[key] = value;
-			}
-		}
-		return obj;
+		);
 	}
 
-	serializeData(data) {
-		if (!data) return null;
-
-		if (data instanceof HTMLElement) {
-			return null;
-		}
-		if (typeof data !== 'object') return data;
-
-		if (data === null) return null;
-
-		if (data instanceof FormData) {
-			return {
-				_type: 'FormData',
-				... this.serializeFormData(data)
-			};
-		}
-
-		// Handle Arrays
-		if (Array.isArray(data)) {
-			return data.map(item => this.serializeData(item));
-		}
-
-		// Handle Date objects
-		if (data instanceof Date) {
-			return {
-				_type: 'Date',
-				value: data.toISOString()
-			};
-		}
-
-		// Handle plain objects
-		const output = {};
-		for (const [key, value] of Object.entries(data)) {
-			output[key] = this.serializeData(value);
-		}
-		return output;
-	}
-
-	unserializeData(data) {
-		if (!data || typeof data !== 'object') return data;
-		if (data === null) return null;
-
-		// Check for special types
-		if (data._type) {
-			switch (data._type) {
-				case 'FormData':
-					return this.unserializeFormData(data);
-				case 'File':
-					// Can't reconstruct File, return metadata with warning flag
-					return {
-						_wasFile: true,
-						_fileMetadata: data,
-						name: data.name,
-						type: data.type,
-						size: data.size
-					};
-				case 'Blob':
-					// Can't reconstruct Blob
-					return {
-						_wasBlob: true,
-						_blobMetadata: data,
-						type: data.type,
-						size: data.size
-					};
-				case 'Date':
-					return new Date(data.value);
-			}
-		}
-
-		// Handle Arrays
-		if (Array.isArray(data)) {
-			return data.map(item => this.unserializeData(item));
-		}
-
-		// Handle plain objects
-		const output = {};
-		for (const [key, value] of Object.entries(data)) { // Fixed: 'of' not 'in'
-			output[key] = this.unserializeData(value);
-		}
-		return output;
-	}
-	unserializeFormData(data) {
-		const formData = new FormData();
-
-		for (const [key, value] of Object.entries(data)) {
-			if (Array.isArray(value)) {
-				value.forEach(item => {
-					if (item?._isFile) {
-						console.warn(`Cannot restore file "${item.name}" from stored data`);
-						// Optionally append metadata as JSON string for reference
-						formData.append(key + '_was_file', JSON.stringify(item));
-					} else {
-						formData.append(key, item);
-					}
-				});
-			} else if (value?._isFile) {
-				console.warn(`Cannot restore file "${value.name}" from stored data`);
-				// Optionally append metadata as JSON string for reference
-				formData.append(key + '_was_file', JSON.stringify(value));
-			} else if (value !== null && value !== undefined) {
-				formData.append(key, value);
-			}
-		}
-
-		return formData;
-	}
-
-
-	clearItem(key) {
-		this.items.delete(key);
-		if (this.db) {
-			const tx = this.db.transaction(['items'], 'readwrite');
-			const store = tx.objectStore('items');
-			store.delete(key);
-		}
-	}
-
-	/**
-	 * Filter helpers
-	 */
 	cleanFilters(filters) {
 		const cleaned = {};
 		Object.entries(filters).forEach(([key, value]) => {
@@ -597,10 +792,33 @@
 		return cleaned;
 	}
 
-	setFilter(key, value) {
-		const oldValue = this.filters[key];
+	/**
+	 * Generate cache key from filters
+	 */
+	generateCacheKey(filters) {
+		if (this.config.cacheKeyStrategy === 'custom' && this.config.generateCacheKey) {
+			return this.config.generateCacheKey(filters);
+		}
 
-		if (value === '' || value === null || value === undefined) {
+		// Default strategy: sort keys and create string
+		const sorted = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+
+		return JSON.stringify(sorted);
+	}
+
+	setFilter(key, value) {
+		if (!this.filters) {
+			this.filters = {};
+		}
+		const oldValue = this.filters[key];
+		if (oldValue === value) {
+			return;
+		}else if (value === '' || value === null || value === undefined) {
 			delete this.filters[key];
 		} else {
 			this.filters[key] = value;
@@ -612,11 +830,12 @@
 		});
 
 		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint) {
+		if (this.config.endpoint !== null) {
 			this.fetch();
 		}
 	}
 
+
 	/**
 	 * Remove a filter
 	 */
@@ -657,25 +876,58 @@
 	}
 
 	/**
-	 * Cache management
+	 * Set multiple filters at once
 	 */
-	generateCacheKey(endpoint, filters) {
-		const sorted = Object.keys(filters).sort().reduce((obj, key) => {
-			obj[key] = filters[key];
-			return obj;
-		}, {});
-		return `${endpoint}_${JSON.stringify(sorted)}`;
+	async setFilters(filters) {
+		const hasChanges = Object.keys(filters).some(
+			key => this.filters[key] !== filters[key]
+		);
+
+		if (!hasChanges) {
+			return;
+		}
+
+		this.filters = { ...this.filters, ...filters };
+
+		this.notify('filters-changed', {
+			filters: this.filters,
+			changed: filters,
+		});
+
+		// Only fetch if endpoint configured
+		if (this.config.endpoint) {
+			this.fetch();
+		}
 	}
 
-	generateHeaderKey(url) {
-		return `headers_${url}`;
+	getFiltered() {
+		const cacheKey = this.generateCacheKey(this.filters);
+		const cacheEntry = this.cache.get(cacheKey);
+
+		if (cacheEntry && cacheEntry.items) {
+			return cacheEntry.items.reduce((acc, id) => {
+				const item = this.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+		}
+
+		return Array.from(this.data.values());
 	}
 
-	isCacheValid(cacheEntry, maxAge = this.config.TTL) {
+	/**
+	 * Check if cache entry is still valid
+	 */
+	isCacheValid(cacheEntry) {
 		if (!cacheEntry || !cacheEntry.timestamp) return false;
-		return (Date.now() - cacheEntry.timestamp) < maxAge;
+
+		const age = Date.now() - cacheEntry.timestamp;
+		return age < this.config.TTL;
 	}
 
+	/**
+	 * Store HTTP response headers for caching
+	 */
 	storeResponseHeaders(key, response) {
 		const headers = {
 			key,
@@ -685,10 +937,209 @@
 		};
 
 		this.httpHeaders.set(key, headers);
-		this.saveHeadersToDB(key, headers);
+
+		if (this.db && this.db.objectStoreNames.contains('headers')) {
+			const tx = this.db.transaction(['headers'], 'readwrite');
+			const store = tx.objectStore('headers');
+			store.put(headers);
+		}
+	}
+
+	/**
+	 * Clear HTTP cache headers for a specific cache key or all
+	 */
+	clearHttpHeaders(cacheKey = null) {
+		if (cacheKey) {
+			this.httpHeaders.delete(cacheKey);
+
+			if (this.db && this.db.objectStoreNames.contains('headers')) {
+				const tx = this.db.transaction(['headers'], 'readwrite');
+				const store = tx.objectStore('headers');
+				store.delete(cacheKey);
+			}
+		} else {
+			// Clear all
+			this.httpHeaders.clear();
+
+			if (this.db && this.db.objectStoreNames.contains('headers')) {
+				const tx = this.db.transaction(['headers'], 'readwrite');
+				const store = tx.objectStore('headers');
+				store.clear();
+			}
+		}
+	}
+
+	/**
+	 * Save cache entry to IndexedDB
+	 */
+	async saveCache(key, data) {
+		if (!this.db || !this.db.objectStoreNames.contains('cache')) return;
+
+		const tx = this.db.transaction(['cache'], 'readwrite');
+		const store = tx.objectStore('cache');
+		await store.put(data);
+	}
+
+	getCurrentRequest() {
+		return this.lastResponse;
+	}
+
+	/**
+	 * Load cache from IndexedDB
+	 */
+	async loadCache() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['cache'], 'readonly');
+			const store = tx.objectStore('cache');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(item => {
+					if (this.isCacheValid(item)) {
+						this.cache.set(item.key, item);
+					}
+				});
+				resolve();
+			};
+		});
+	}
+
+	/**
+	 * Load HTTP headers from IndexedDB
+	 */
+	async loadHeaders() {
+		if (!this.db) return;
+
+		return new Promise((resolve) => {
+			const tx = this.db.transaction(['headers'], 'readonly');
+			const store = tx.objectStore('headers');
+			const request = store.getAll();
+
+			request.onsuccess = (e) => {
+				e.target.result.forEach(header => {
+					this.httpHeaders.set(header.key, header);
+				});
+				resolve();
+			};
+		});
 	}
 
 
+	/**
+	 * Subscribe to store events
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	/**
+	 * Notify subscribers of events
+	 */
+	notify(event, data = {}) {
+		this.subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
+	/**
+	 * Check if store has items matching a specific filter
+	 * @param {string} filterName - The filter to check
+	 * @param {*} filterValue - The value to match
+	 * @returns {boolean}
+	 */
+	hasItemsForFilter(filterName, filterValue) {
+		if (!this.data || this.data.size === 0) return false;
+
+		return Array.from(this.data.values()).some(item => {
+			return item[filterName] === filterValue;
+		});
+	}
+
+	/**
+	 * Query items using an index
+	 */
+	async query(indexName, value) {
+		if (!this.db) return [];
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+
+			if (!store.indexNames.contains(indexName)) {
+				reject(new Error(`Index ${indexName} does not exist`));
+				return;
+			}
+
+			const index = store.index(indexName);
+			const request = value !== undefined
+				? index.getAll(value)
+				: index.getAll();
+
+			request.onsuccess = (e) => {
+				const results = e.target.result.map(item => {
+					return this.config.stripDOMReferences
+						? this.stripDOMReferences(item)
+						: item;
+				});
+				resolve(results);
+			};
+
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+	/**
+	 * Count items in store
+	 */
+	async count() {
+		if (!this.db) return this.data.size;
+
+		return new Promise((resolve, reject) => {
+			const tx = this.db.transaction([this.config.storeName], 'readonly');
+			const store = tx.objectStore(this.config.storeName);
+			const request = store.count();
+
+			request.onsuccess = (e) => resolve(e.target.result);
+			request.onerror = (e) => reject(e);
+		});
+	}
+
+
+	setLoading(on) {
+		this.body.classList.toggle('loading', on);
+		if (on) {
+			this.loading.showModal();
+		} else {
+			this.loading.close();
+		}
+
+	}
+
+	/**
+	 * Cleanup and destroy
+	 */
+	destroy() {
+		if (this.currentRequest) {
+			this.currentRequest.abort();
+		}
+
+		this.subscribers.clear();
+		this.data.clear();
+		this.cache.clear();
+		this.httpHeaders.clear();
+
+		if (this.db) {
+			this.db.close();
+			this.db = null;
+		}
+	}
 
 	clearCache() {
 		this.cache.clear();
@@ -701,231 +1152,7 @@
 
 		this.notify('cache-cleared');
 	}
-
-	invalidateCache(pattern) {
-		const keysToDelete = [];
-
-		this.cache.forEach((value, key) => {
-			if (typeof pattern === 'string' && key.includes(pattern)) {
-				keysToDelete.push(key);
-			} else if (pattern instanceof RegExp && pattern.test(key)) {
-				keysToDelete.push(key);
-			}
-		});
-
-		keysToDelete.forEach(key => {
-			this.cache.delete(key);
-			if (this.db) {
-				const tx = this.db.transaction(['cache'], 'readwrite');
-				const store = tx.objectStore('cache');
-				store.delete(key);
-			}
-		});
-
-		this.notify('cache-invalidated', { count: keysToDelete.length });
-	}
-
-	/**
-	 * DOM Cache Management
-	 */
-
-	/**
-	 * Store rendered DOM element for a specific item and view
-	 */
-	storeDOMElement(itemId, viewName, element) {
-		if (!this.domCache.has(itemId)) {
-			this.domCache.set(itemId, {});
-		}
-
-		const itemCache = this.domCache.get(itemId);
-		itemCache[viewName] = element.cloneNode(true);
-		this.domCache.set(itemId, itemCache);
-
-		// Save to IndexedDB
-		this.saveDOMCacheToDB(itemId, itemCache);
-	}
-
-
-	/**
-	 * Retrieve cached DOM element for a specific item and view
-	 */
-	getDOMElement(itemId, viewName) {
-		const itemCache = this.domCache.get(itemId);
-		if (itemCache && itemCache[viewName]) {
-			return itemCache[viewName].cloneNode(true);
-		}
-		return null;
-	}
-
-	/**
-	 * Check if DOM element exists in cache
-	 */
-	hasDOMElement(itemId, viewName) {
-		const itemCache = this.domCache.get(itemId);
-		return itemCache && itemCache[viewName];
-	}
-
-	/**
-	 * Clear DOM cache for a specific item
-	 */
-	clearDOMCache(itemId) {
-		this.domCache.delete(itemId);
-
-		if (this.db) {
-			const tx = this.db.transaction(['dom'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.delete(itemId);
-		}
-	}
-
-	/**
-	 * Clear all DOM cache
-	 */
-	clearAllDOMCache() {
-		this.domCache.clear();
-
-		if (this.db) {
-			const tx = this.db.transaction(['dom'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.clear();
-		}
-	}
-
-	/**
-	 * Helper method to render or retrieve cached DOM elements
-	 */
-	renderOrRetrieve(item, viewName, renderFunction) {
-		// Check cache first
-		const cached = this.getDOMElement(item.id, viewName);
-		if (cached) {
-			return cached;
-		}
-
-		// Render new element
-		const element = renderFunction(item);
-
-		// Cache the rendered element
-		this.storeDOMElement(item.id, viewName, element);
-
-		return element;
-	}
-	/**
-	 * Database operations
-	 */
-	async saveItemsToDB() {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['items'], 'readwrite');
-		const store = tx.objectStore('items');
-
-		store.clear();
-		this.items.forEach(item => {
-			if (!item._deleted) {
-				store.put(item);
-			}
-		});
-	}
-
-	async saveCacheToDB(key, data) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['cache'], 'readwrite');
-		const store = tx.objectStore('cache');
-		store.put(data);
-	}
-
-	async saveHeadersToDB(key, headers) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['headers'], 'readwrite');
-		const store = tx.objectStore('headers');
-		store.put(headers);
-	}
-
-	async saveDOMCacheToDB(itemId, domCache) {
-		if (!this.db) return;
-
-		// Convert DOM elements to HTML strings for storage
-		const serialized = {
-			id: itemId,
-			views: {}
-		};
-
-		Object.entries(domCache).forEach(([viewName, element]) => {
-			if (element && element.outerHTML) {
-				serialized.views[viewName] = element.outerHTML;
-			}
-		});
-
-		const tx = this.db.transaction(['dom'], 'readwrite');
-		const store = tx.objectStore('dom');
-		store.put(serialized);
-	}
-
-	async saveFormsToDB(key, form) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['forms'], 'readwrite');
-		const store = tx.objectStore('forms');
-		store.put(form);
-	}
-
-	storeForm(key, form) {
-		this.forms.set(key, form);
-		this.saveFormsToDB(key, form);
-	}
-
-	getForm(key) {
-		return this.forms.has(key) ? this.forms.get(key) : null;
-	}
-
-	getAllForms() {
-		return this.forms;
-	}
-
-	clearForm(key) {
-		this.forms.delete(key);
-		if (this.db) {
-			const tx = this.db.transaction(['forms'], 'readwrite');
-			const store = tx.objectStore('forms');
-			store.delete(key);
-		}
-	}
-
-	clearAllForms() {
-		this.forms.clear();
-		if (this.db) {
-			const tx = this.db.transaction(['forms'], 'readwrite');
-			const store = tx.objectStore('dom');
-			store.clear();
-		}
-	}
-
-	/**
-	 * Event system
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	/**
-	 * Cleanup
-	 */
-	destroy() {
-		if (this.db) {
-			this.db.close();
-		}
-		this.subscribers.clear();
-		this.items.clear();
-		this.cache.clear();
-		this.domCache.clear();
-		this.httpHeaders.clear();
-	}
 }
 
+// Export for use
 window.jvbStore = DataStore;
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 73a2756..80be7b3 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -3,19 +3,26 @@
  * Works with DataStore for CRUD operations and standalone for front-end forms
  */
 class FormController {
-	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: 604800000, //7 days
-		});
+	constructor(config = {}) {
+		this.config = {
+			collectFormData: false,
+			... config
+		}
+		this.store = window.jvbStore.register(
+			'forms',
+			{
+				storeName: 'forms',
+				keyPath: 'formId',
+				indexes: [
+					{ name: 'status', keyPath: 'status' },
+					{ name: 'operationId', keyPath: 'operationId' },
+					{ name: 'timestamp', keyPath: 'timestamp' },
+					{ name: 'formType', keyPath: 'type' }
+				],
+				TTL: 7 * 24 * 60 * 1000, //7 days
+				validateData: true,
+				delayFetch: true
+			});
 
 		this.debouncer = window.debouncer;
 
@@ -49,6 +56,10 @@
 			remove: 800,
 			reorder: 1000
 		};
+		this.isTimeline = false;
+		if (window.crudManager && window.crudManager.isTimeline) {
+			this.isTimeline = true;
+		}
 
 		// Bind handlers
 		this.clickHandler = this.handleClick.bind(this);
@@ -57,18 +68,49 @@
 		this.inputHandler = this.handleInput.bind(this);
 		this.focusHandler = this.handleFocus.bind(this);
 		this.blurHandler = this.handleBlur.bind(this);
+		//Processors
+		this.processRepeaterField = this.processRepeaterField.bind(this);
+		this.processGroupField = this.processGroupField.bind(this);
+		this.processLocationField = this.processLocationField.bind(this);
+		this.processRegularField = this.processRegularField.bind(this);
 
 		this.init();
 	}
 
 	async init() {
-		// Check for pending operations on page load
-		await this.checkPendingOperations();
-
 		this.store.subscribe(this.handleStoreEvent.bind(this));
 
 		// Set up global form handlers for standalone forms
 		this.initListeners();
+		if (window.jvbQueue) {
+			window.jvbQueue.subscribe((event, data) => {
+				if (event === 'operation-completed' && data.type === 'form') {
+					this.handleOperationComplete(data);
+				}
+			});
+		}
+	}
+
+	/**
+	 * Handle operation completion - clear related form cache
+	 */
+	async handleOperationComplete(operation) {
+		// Clear the form data from store
+		if (operation.formId) {
+			try {
+				await this.store.delete(operation.formId);
+			} catch (error) {
+				console.warn('Failed to clear form cache:', error);
+			}
+		}
+
+		// Clear any related form state
+		const form = this.forms.get(operation.formId);
+		if (form) {
+			form.isDirty = false;
+			form.lastSaved = Date.now();
+			form.data = {};
+		}
 	}
 
 	handleStoreEvent(event, data) {
@@ -84,16 +126,21 @@
 		}
 	}
 
-	async checkPendingForms() {
-		let items = await this.store.query('status', 'draft');
-		items.forEach(item => {
-			let form = this.forms.get(item.formId);
-			if (form && form.element) {
-				form.element.querySelector('.restore-form').hidden = false;
+	checkPendingForms() {
+		// No async needed - data is already loaded in memory
+		const allForms = this.store.getAll();
+		const pendingForms = allForms.filter(form => form.status === 'draft');
+
+		pendingForms.forEach(item => {
+			const form = this.forms.get(item.formId);
+			if (form?.element) {
+				const restoreBtn = form.element.querySelector('.restore-form');
+				if (restoreBtn) {
+					restoreBtn.hidden = false;
+				}
 				new this.populateForm(form.element, item.data);
 			}
 		});
-
 	}
 	/**
 	 * Check for pending operations from previous session
@@ -113,28 +160,31 @@
 	/**
 	 * Show notification for pending changes
 	 */
-	showPendingNotification(pendingData) {
-		const formElement = document.querySelector(`[data-form-id="${pendingData.formId}"]`);
+	/**
+	 * Show notification for pending changes
+	 */
+	showPendingNotification(formId, formData) {
+		const formElement = document.querySelector(`[data-form-id="${formId}"]`);
 		if (!formElement) return;
 
 		const notification = document.createElement('div');
 		notification.className = 'pending-changes-notification';
 		notification.innerHTML = `
-			<p>We noticed unsaved changes from last time. Would you like to restore them?</p>
-			<button class="restore-changes" data-form-id="${pendingData.formId}">Restore</button>
-			<button class="discard-changes" data-form-id="${pendingData.formId}">Discard</button>
-		`;
+        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>
+        <button class="restore-changes" data-form-id="${formId}">Restore</button>
+        <button class="discard-changes" data-form-id="${formId}">Discard</button>
+    `;
 
 		formElement.insertBefore(notification, formElement.firstChild);
 
 		// Add handlers
-		notification.querySelector('.restore-changes').addEventListener('click', () => {
-			this.restorePendingForm(pendingData);
+		notification.querySelector('.restore-changes').addEventListener('click', async () => {
+			await this.restorePendingForm(formId, formData);
 			notification.remove();
 		});
 
-		notification.querySelector('.discard-changes').addEventListener('click', () => {
-			this.discardPendingForm(pendingData.formId);
+		notification.querySelector('.discard-changes').addEventListener('click', async () => {
+			await this.discardPendingForm(formId);
 			notification.remove();
 		});
 	}
@@ -142,16 +192,20 @@
 	/**
 	 * Restore pending form data
 	 */
-	restorePendingForm(pendingData) {
-		const form = document.querySelector(`[data-form-id="${pendingData.formId}"]`);
+	async restorePendingForm(formId, formData) {
+		const form = document.querySelector(`[data-form-id="${formId}"]`);
 		if (!form) return;
 
 		// Populate form with cached data
-		new this.populateForm(form, pendingData.formData);
+		new this.populateForm(form, formData);
 
-		// Mark as restored
-		pendingData.status = 'restored';
-		this.pendingForms.set(pendingData.formId, pendingData);
+		// Update status in store (mark as restored, not draft)
+		await this.store.save({
+			formId: formId,
+			data: formData,
+			status: 'restored',
+			timestamp: Date.now()
+		});
 
 		if (window.jvbA11y) {
 			window.jvbA11y.announce('Previous changes restored');
@@ -162,10 +216,14 @@
 	 * Discard pending form data
 	 */
 	async discardPendingForm(formId) {
-		this.store.delete(formId);
+		try {
+			await this.store.delete(formId);
 
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Previous changes discarded');
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Previous changes discarded');
+			}
+		} catch (error) {
+			console.error('Failed to discard pending form:', error);
 		}
 	}
 
@@ -207,7 +265,7 @@
 				...options
 			},
 			dependencies: new Map(),
-			data: this.collectFormData(formElement),
+			data: this.collectFormData(formElement, true),
 		};
 
 		// Initialize special fields
@@ -661,12 +719,21 @@
 		if (this.subscribers.size > 0) {
 			event.preventDefault();
 			const formData = this.collectFormData(form);
+
+			// Notify subscribers (they'll handle actual submission)
 			this.notify('form-submit', {
-				formId: formConfig.id,
-				data: formData,
+				formId: form.dataset.formId,
+				fullData: formData,
 				config: formConfig
 			});
+
+			// Don't delete yet - wait for success/error from subscriber
+			return;
 		}
+
+		// For forms that submit normally (not prevented)
+		// We can clean up the cache on successful submission
+		// This would typically be called from handleFormSuccess
 	}
 
 	handleFormSuccess(form, data) {
@@ -686,7 +753,6 @@
 			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');
@@ -706,7 +772,6 @@
 			}
 
 			if (data.description) {
-				// Handle both string and array descriptions
 				const descriptions = Array.isArray(data.description)
 					? data.description
 					: [data.description];
@@ -721,6 +786,21 @@
 			form.insertBefore(successBox, form.firstChild);
 		}
 
+		// ✅ DELETE CACHED FORM DATA ON SUCCESS
+		if (form.dataset.formId) {
+			this.store.delete(form.dataset.formId).catch(err => {
+				console.warn('Failed to clear form cache:', err);
+			});
+
+			// Clear form config dirty state
+			const formConfig = this.forms.get(form.dataset.formId);
+			if (formConfig) {
+				formConfig.isDirty = false;
+				formConfig.lastSaved = Date.now();
+				formConfig.data = {}; // Clear cached data
+			}
+		}
+
 		// Announce success for accessibility
 		if (window.jvbA11y) {
 			window.jvbA11y.announce(data.message || 'Form submitted successfully');
@@ -881,7 +961,7 @@
 
 		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);
@@ -1409,33 +1489,40 @@
 		const formData = this.collectFormData(formConfig.element);
 
 		this.showFormStatus(formConfig.id, 'saving');
+
+		// DataStore will now automatically:
+		// - Convert Sets/Maps to Arrays/Objects
+		// - Strip DOM references
+		// - Validate serializability
 		await this.store.save({
 			formId: formConfig.id,
 			data: formData,
 			status: 'draft',
 			timestamp: Date.now()
-		}).then(()=> {
+		}).then(() => {
 			this.showFormStatus(formConfig.id, 'autosaved');
+		}).catch(error => {
+			console.error('Autosave failed:', error);
+			this.showFormStatus(formConfig.id, 'error', 'Failed to save changes');
 		});
 
 		// 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;
 		this.forms.set(formConfig.id, formConfig);
 		document.removeEventListener('input', this.handleInput);
 
-		for (let [key,  value] of Object.entries(formData)) {
-			//We want all data for complex fields, like group, repeater, or location
+		for (let [key, value] of Object.entries(formData)) {
+			// Complex fields need full data
 			if (typeof value === 'object') {
 				changes[key] = value;
 			}
 		}
-		// Notify instead of callback
+
+		// Notify
 		this.notify('form-autosave', {
 			formId: formConfig.id,
 			changes: changes,
@@ -1474,8 +1561,6 @@
 
 		form.status = status;
 
-		console.log('Setting status: ', status);
-
 		// Add new status
 		const statusWrap = form.element.querySelector('.fstatus');
 		statusWrap.hidden = false;
@@ -1534,10 +1619,15 @@
 
 	/* ========== Form Data Methods ========== */
 
-	collectFormData(form) {
+	collectFormData(form, isInit = false) {
 		if (Object.hasOwn(form.dataset, 'timeline')) {
 			return this.collectTimeline(form);
 		}
+		//Table forms are handled separately
+		if (form.classList.contains('table') && form.tagName === 'FORM') {
+			return {};
+		}
+
 		const formData = new FormData(form);
 		let data = {};
 		const repeaterData = {};
@@ -1557,7 +1647,6 @@
 	}
 
 	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)
@@ -1572,11 +1661,18 @@
 				// Timeline-specific field: [postId]fieldName
 				const [, postId, fieldName] = match;
 				if (!posts[postId]) {
-					posts[postId] = { id: parseInt(postId) };
+					posts[postId] = {
+						id: parseInt(postId),
+					};
 					postOrder.push(postId); // Track first occurrence
 				}
-				const processor = this.getFieldProcessor(fieldName);
-				processor(fieldName, value, posts[postId], {}, {}, form);
+				if (fieldName === 'post_thumbnail') {
+					posts[postId]['post_thumbnail'] = parseInt(form.querySelector(`[name="${key}"]`).closest('.item')?.dataset.id);
+				} else {
+					const processor = this.getFieldProcessor(fieldName);
+					processor(fieldName, value, posts[postId], {}, {}, form);
+				}
+
 			} else {
 				// Shared field (post_title, taxonomies, etc.)
 				const processor = this.getFieldProcessor(key);
@@ -1592,12 +1688,10 @@
 		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;
 		if (key.includes(':')) return this.processRepeaterField;
 		if (/\[[^\]]+\]/.test(key)) return this.processLocationField;
@@ -1622,42 +1716,12 @@
 	}
 
 	mergePostData(data, postData) {
-		for (let [postId, postData] in Object.entries(postData)) {
-			data[postId] = postData;
+		for (let [postId, fields] of Object.entries(postData)) {
+			data[postId] = fields;
 		}
 		return data;
 	}
 
-	processTableField(key, value, data, repeaterData, postData, form) {
-		/***
-		 * Table forms are a huge form containing multiple posts and their data
-		 * Field names are prepended with  `${postID}|`
-		 * Goal:
-		 * 1) Separate out the post id from the field name
-		 * 2) store the original data in a temporary 'original' variable
-		 * 3) Process the field as normal
-		 * 4) return the original data, as PostID: {$field data}
-		 * Final format:
-		 * {
-		 *     id1: {
-		 *         field1: "A title",
-		 *         field3: 32
-		 *     },
-		 *     id2: {
-		 *         field1: "Another title",
-		 *         field2: "122,21,32"
-		 *     }
-		 * }
-		 **/
-		let [post, fieldKey] = key.split('|');
-		if (!post in postData) {
-			postData[post] = {};
-		}
-
-		const processor = this.getFieldProcessor(fieldKey);
-		processor(fieldKey, value, postData, repeaterData, postData, form);
-
-	}
 	processRepeaterField(key, value, data, repeaterData, postData, form) {
 		let [fieldName, index, subField] = key.split(':');
 
diff --git a/assets/js/concise/FrontendFavourites.js b/assets/js/concise/FrontendFavourites.js
index 769b4aa..b558f5f 100644
--- a/assets/js/concise/FrontendFavourites.js
+++ b/assets/js/concise/FrontendFavourites.js
@@ -1,33 +1,35 @@
 class FrontendFavourites {
 	constructor() {
 		// Initialize DataStore for queue persistence
-		this.store = new window.jvbStore({
-			name: 'favourites',
-			storeName: 'favourites',
-			endpoint: 'favourites',
-			indexes: [
-				{name: 'content', keyPath: 'content'},
-				{name: 'listId', keyPath: 'listId'},
-			],
-			TTL: 86400000,
-			showLoading: false,
-			filters: {
-				user: jvbSettings.currentUser,
-				content: 'all',
-				order: 'desc',
-				orderby: 'date',
-				page: 1,
-				all: true,
-			}
-		});
+		this.store = window.jvbStore.register(
+			'favourites',
+			{
+				storeName: 'favourites',
+				endpoint: 'favourites',
+				indexes: [
+					{name: 'content', keyPath: 'content'},
+					{name: 'listId', keyPath: 'listId'},
+				],
+				TTL: 6 * 60 * 1000,
+				showLoading: false,
+				filters: {
+					user: jvbSettings.currentUser,
+					content: 'all',
+					order: 'desc',
+					orderby: 'date',
+					page: 1,
+					all: true,
+				}
+			});
 
-		this.listStore = new window.jvbStore({
-			name: 'favourites_lists',
-			storeName: 'lists',
-			keyPath: 'listId',
-			endpoint: 'favourites/lists',
-			TTL: 86400000,
-		})
+		this.listStore = window.jvbStore.register(
+			'favourites_lists',
+			{
+				storeName: 'lists',
+				keyPath: 'listId',
+				endpoint: 'favourites/lists',
+				TTL: 6 * 60 * 1000,
+			});
 
 		this.store.subscribe((event, data) => {
 			switch (event) {
diff --git a/assets/js/concise/FrontendVotes.js b/assets/js/concise/FrontendVotes.js
index 93b03ff..fefc95a 100644
--- a/assets/js/concise/FrontendVotes.js
+++ b/assets/js/concise/FrontendVotes.js
@@ -1,13 +1,13 @@
 class FrontendVotes {
 	constructor() {
 		// Initialize DataStore for queue persistence
-		this.store = new window.jvbStore({
-			name: 'favourites',
-			endpoint: 'favourites',
-			useIndexedDB: true,
-			TTL: Infinity,
-			showLoading: false
-		});
+		this.store = window.jvbStore.register('votes',
+			{
+				endpoint: 'votes',
+				useIndexedDB: true,
+				TTL: 6 * 60 * 1000,
+				showLoading: false
+			});
 	}
 
 	handleVote(button) {
diff --git a/assets/js/concise/Gallery.js b/assets/js/concise/Gallery.js
new file mode 100644
index 0000000..07eaef1
--- /dev/null
+++ b/assets/js/concise/Gallery.js
@@ -0,0 +1,463 @@
+class Gallery {
+	constructor () {
+		this.a11y = window.jvbA11y;
+		this.index = 0;
+		this.images = [];
+
+		this.zoom = {
+			scale: 1,
+			min: 1,
+			max: 4,
+			threshold: 50,
+			x: 0,
+			y: 0,
+			startX: 0,
+			startY: 0,
+			ease: .2,
+			panning: false
+		};
+
+		this.swipe = this.resetSwipe();
+
+		this.activePointers = new Map();
+		this.lastTap = 0;
+
+		this.initElements();
+		this.initModal();
+		this.initListeners();
+		this.initSubscribers();
+
+	}
+	/*********************************************************************
+	 ELEMENTS
+	 *********************************************************************/
+	initElements() {
+		this.elements = {
+			imageSelector: 'a.open-gallery',
+			gallery: {
+				modal: 'dialog.gallery',
+				wrap: '.wrap',
+				nextButton: '.next',
+				prevButton: '.prev',
+				image: '.image',
+				leftImage: '.image-left',
+				rightImage: '.image-right',
+				counter: '.counter'
+			},
+		}
+		this.ui = window.uiFromSelectors(this.elements);
+	}
+
+	initModal() {
+		this.modal = new window.jvbModal(
+			this.ui.gallery.modal,
+			{
+				openMessage: 'Opened Gallery',
+				closeMessage: 'Closed Gallery',
+			}
+		);
+		this.modal.subscribe((event, data) => {
+			if (event === 'modal-close') {
+				this.toggleGallery(false);
+			}
+		});
+	}
+	buildGalleryItems(filtered = null) {
+		let selector = filtered ? `[data-opens="${filtered}"]` : this.elements.imageSelector;
+		this.items = Array.from(document.querySelectorAll(selector))
+			.map((img, index) => {
+				let image = img.querySelector('img');
+
+				return {
+					id: img.dataset.id||index,
+					small: image.dataset.small || img.src,
+					medium: image.dataset.medium || img.src,
+					full: image.dataset.full || img.src,
+					alt: image.alt || '',
+					element: image
+				};
+			});
+	}
+	/*********************************************************************
+	 LISTENERS
+	 *********************************************************************/
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.pointerDownHandler = this.onPointerDown.bind(this);
+		this.pointerMoveHandler = this.onPointerMove.bind(this);
+		this.pointerUpHandler   = this.onPointerUp.bind(this);
+		this.wheelHandler       = this.onWheel.bind(this);
+		this.keyHandler         = this.handleKeys.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+	}
+	handleClick(e) {
+		let target = window.targetCheck(e, this.elements.imageSelector);
+		if (target && !this.modal.isOpen) {
+			e.preventDefault();
+			this.buildGalleryItems((Object.hasOwn(target.dataset, 'opens')) ? target.dataset.opens : null);
+
+			this.index = this.items.findIndex(item => item.element === target.querySelector('img'));
+			this.toggleGallery(true);
+		} else if (this.modal.isOpen) {
+			if (window.targetCheck(e, this.elements.gallery.nextButton)) {
+				console.log('Next');
+				this.nextElement();
+			} else if (window.targetCheck(e, this.elements.gallery.prevButton)) {
+				console.log('Previous');
+				this.prevElement();
+			}
+		}
+	}
+
+	handleKeys(e) {
+		if (!this.modal.isOpen) return;
+		switch (e.key) {
+			case 'ArrowLeft':
+				e.preventDefault();
+				this.prevElement();
+				break;
+			case 'ArrowRight':
+				e.preventDefault();
+				this.nextElement();
+				break;
+		}
+		if (!e.ctrlKey) return;
+		if (e.key === '+' || e.key === '=') {
+			e.preventDefault();
+			this.handleZoom(.2); // centered
+		}
+
+		if (e.key === '-') {
+			e.preventDefault();
+			this.handleZoom(-.2); // centered
+		}
+
+		if (e.key === '0') {
+			e.preventDefault();
+			this.resetZoom();
+		}
+	}
+
+	onPointerDown(e) {
+		this.swipe.startX = e.clientX;
+		this.swipe.startY = e.clientY;
+		this.ui.gallery.image.setPointerCapture(e.pointerId);
+		this.activePointers.set(e.pointerId, {
+			x: e.clientX,
+			y: e.clientY
+		});
+
+		const now = performance.now();
+
+		// DOUBLE-TAP / DOUBLE-CLICK
+		if (now - this.lastTap < 300 && this.activePointers.size === 1) {
+			if (this.zoom.scale > 1) this.resetZoom();
+			else this.handleZoom(+1, e.clientX, e.clientY);
+
+			this.lastTap = 0;
+			return;
+		}
+
+		this.lastTap = now;
+
+		// PINCH START
+		if (this.activePointers.size === 2) {
+			const pts = [...this.activePointers.values()];
+			this.pinchStartDist = Math.hypot(
+				pts[0].x - pts[1].x,
+				pts[0].y - pts[1].y
+			);
+			this.pinchStartScale = this.zoom.scale;
+			return;
+		}
+
+		// PAN START
+		if (this.zoom.scale > 1) {
+			this.zoom.panning = true;
+			this.zoom.startX = e.clientX - this.zoom.x;
+			this.zoom.startY = e.clientY - this.zoom.y;
+		}
+	}
+
+	onPointerMove(e) {
+		if (!this.activePointers.has(e.pointerId)) return;
+
+		this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
+
+		// PINCH (two pointers)
+		if (this.activePointers.size === 2) {
+			const pts = [...this.activePointers.values()];
+			const dist = Math.hypot(
+				pts[0].x - pts[1].x,
+				pts[0].y - pts[1].y
+			);
+
+			const newScale = this.pinchStartScale * (dist / this.pinchStartDist);
+			const increment = newScale - this.zoom.scale;
+
+			// Zoom centered (NO midpoint)
+			this.handleZoom(increment);
+
+			return;
+		}
+
+		// PAN (one pointer)
+		if (this.zoom.panning) {
+			this.zoom.x = e.clientX - this.zoom.startX;
+			this.zoom.y = e.clientY - this.zoom.startY;
+			this.applyTransform();
+		}
+	}
+
+	onPointerUp(e) {
+		this.activePointers.delete(e.pointerId);
+
+		if (this.activePointers.size < 2) {
+			this.pinchStartDist = 0;
+		}
+
+		if (!this.zoom.panning && this.activePointers.size === 0) {
+			// End of tap or swipe - detect swipe
+			this.swipe.endX = e.clientX;
+			this.swipe.endY = e.clientY;
+			const dx = this.swipe.endX - this.swipe.startX;
+			const dy = this.swipe.endY - this.swipe.startY;
+
+			if (Math.abs(dx) > this.zoom.threshold) {
+				if (dx > 0) {
+					console.log('Swipe right');
+					this.prevElement();
+				} else {
+					console.log('Swipe left');
+					this.nextElement();
+				}
+			}
+
+			this.zoom.panning = false;
+		}
+	}
+
+	/* --------------------------------------------------
+		WHEEL ZOOM (pointer-centered)
+	-------------------------------------------------- */
+	onWheel(e) {
+		if (!e.ctrlKey) return;
+		e.preventDefault();
+
+		const increment = (e.deltaY < 0) ? +0.2 : -0.2;
+		this.handleZoom(increment, e.clientX, e.clientY);
+	}
+
+	clampPan() {
+		const BORDER = 32; // 2rem
+		const wrap = this.ui.gallery.wrap;
+		if (!wrap) return;
+
+		const wrapRect = wrap.getBoundingClientRect();
+
+		// MUST use natural dimensions
+		const naturalW = 1920;
+		const naturalH = 1920;
+
+		// But they must be constrained by how the CSS fits them (max-width: 90vw, max-height: 85vh)
+		// So compute the base display size:
+		const displayRatio = Math.min(
+			wrapRect.width / naturalW,
+			wrapRect.height / naturalH
+		);
+
+		const baseWidth  = naturalW * displayRatio;
+		const baseHeight = naturalH * displayRatio;
+
+		const scaledWidth  = baseWidth  * this.zoom.scale;
+		const scaledHeight = baseHeight * this.zoom.scale;
+
+		// Allowed pan range
+		const minX = wrapRect.width - scaledWidth - BORDER;
+		const maxX = BORDER;
+
+		const minY = wrapRect.height - scaledHeight - BORDER;
+		const maxY = BORDER;
+
+		// clamp
+		this.zoom.x = Math.min(maxX, Math.max(minX, this.zoom.x));
+		this.zoom.y = Math.min(maxY, Math.max(minY, this.zoom.y));
+	}
+
+
+
+
+	handleZoom(increment, clientX=null, clientY=null) {
+		const oldScale = this.zoom.scale;
+		let newScale = oldScale + increment;
+
+		newScale = Math.min(this.zoom.max, Math.max(this.zoom.min, newScale));
+
+		if (newScale === oldScale) return;
+
+		const ratio = newScale / oldScale;
+
+		// default: center of image
+		let rect = this.ui.gallery.image.getBoundingClientRect();
+		if (clientX === null || clientY === null) {
+			clientX = rect.left + rect.width / 2;
+			clientY = rect.top + rect.height / 2;
+		}
+
+		const localX = clientX - rect.left;
+		const localY = clientY - rect.top;
+
+		this.zoom.x = (this.zoom.x - localX) * ratio + localX;
+		this.zoom.y = (this.zoom.y - localY) * ratio + localY;
+
+		this.zoom.scale = newScale;
+
+		this.applyTransform();
+		this.notify("zoom", { scale: this.zoom.scale });
+	}
+
+	applyTransform() {
+		// this.clampPan();
+		const img = this.ui.gallery.image;
+		img.style.transform = `translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`;
+	}
+	resetZoom() {
+		this.zoom.scale = 1;
+		this.zoom.x = 0;
+		this.zoom.y = 0;
+		this.zoom.startX = 0;
+		this.zoom.startY = 0;
+		this.zoom.panning = false;
+		this.applyTransform();
+	}
+
+	resetSwipe() {
+		return {
+			startX: null,
+			startY: null,
+			endX: null,
+			endY: null
+		};
+	}
+
+	/**
+	 *
+	 * @param {boolean} open
+	 * @param {null|number} index
+	 */
+	toggleGallery(open, index= null) {
+		if (open) {
+			this.ui.gallery.image.addEventListener("pointerdown", this.pointerDownHandler);
+			this.ui.gallery.image.addEventListener("pointermove", this.pointerMoveHandler);
+			this.ui.gallery.image.addEventListener("pointerup", this.pointerUpHandler);
+			this.ui.gallery.image.addEventListener("pointercancel", this.pointerUpHandler);
+
+			window.addEventListener("wheel", this.wheelHandler, {passive: false});
+			window.addEventListener("keydown", this.keyHandler);
+			this.moveIntoView();
+		} else {
+			this.ui.gallery.image.removeEventListener("pointerdown", this.pointerDownHandler);
+			this.ui.gallery.image.removeEventListener("pointermove", this.pointerMoveHandler);
+			this.ui.gallery.image.removeEventListener("pointerup", this.pointerUpHandler);
+			this.ui.gallery.image.removeEventListener("pointercancel", this.pointerUpHandler);
+
+			window.removeEventListener("wheel", this.wheelHandler);
+			window.removeEventListener("keydown", this.keyHandler);
+
+			this.resetZoom();
+			this.resetSwipe();
+			this.activePointers.clear();
+			this.lastTap = 0;
+		}
+		if (open && !this.modal.isOpen) {
+			this.modal.handleOpen();
+		}
+	}
+	/*********************************************************************
+	 GALLERY FUNCTIONALITY
+	 *********************************************************************/
+	moveIntoView(index = 0) {
+		let newIndex = this.index + index;
+		//Wrap around
+		if (newIndex < 0) {
+			newIndex = this.items.length - 1;
+		} else if (newIndex >= this.items.length) {
+			newIndex = 0;
+		} else if (newIndex === this.items.length - 3) {
+			this.notify('load-more');
+		}
+
+		this.index = newIndex;
+		this.updateDisplay();
+		this.preloadAdjacent();
+
+		this.a11y.announce(`Image ${this.index + 1} of ${this.items.length}`);
+	}
+	nextElement() {
+		this.resetZoom();
+		this.moveIntoView(1);
+	}
+	prevElement() {
+		this.resetZoom();
+		this.moveIntoView(-1);
+	}
+
+	updateDisplay() {
+		const item = this.items[this.index];
+		if (!item) return;
+		this.ui.gallery.image.src = item.full;
+		this.ui.gallery.image.alt = item.alt;
+		this.ui.gallery.counter.textContent = `${this.index + 1} / ${this.items.length}`;
+
+		this.ui.gallery.prevButton.disabled = this.items.length <= 1;
+		this.ui.gallery.nextButton.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 = offset < 0 ? this.ui.gallery.leftImage : this.ui.gallery.rightImage;
+				img.src = item.full;
+			}
+		});
+	}
+	/*********************************************************************
+	 SUBSCRIBERS
+	 *********************************************************************/
+	initSubscribers() {
+		this.subscribers = new Set();
+	}
+
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data = {}) {
+		this.subscribers.forEach( callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
+	/******************************************************************
+	 CLEANUP
+	 ******************************************************************/
+
+	destroy() {
+		this.subscribers.clear();
+		this.toggleGallery(false);
+		document.removeEventListener('click', this.clickHandler);
+	}
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+	if (document.querySelector('dialog.gallery')) {
+		window.jvbGallery = new Gallery();
+	}
+});
diff --git a/assets/js/concise/Media.js b/assets/js/concise/Media.js
index ab20c7f..a9e1340 100644
--- a/assets/js/concise/Media.js
+++ b/assets/js/concise/Media.js
@@ -78,8 +78,8 @@
 	}
 
 	getTargetSize() {
-		if (this.currentWidth < 768) return 'small';
-		if (this.currentWidth < 1200) return 'medium';
+		if (this.currentWidth < 768) return 'medium';
+		if (this.currentWidth < 1200) return 'large';
 		return 'full';
 	}
 
diff --git a/assets/js/concise/PopulateForm.js b/assets/js/concise/PopulateForm.js
index a15d1fa..1f4273d 100644
--- a/assets/js/concise/PopulateForm.js
+++ b/assets/js/concise/PopulateForm.js
@@ -28,12 +28,10 @@
 		const fieldType = this.getFieldType(fieldWrapper);
 
 		switch (fieldType) {
-			case 'image':
-				this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
-				break;
-
+			case 'upload':
 			case 'gallery':
-				this.populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData);
+			case 'image':
+				this.populateUploadField(fieldWrapper, fieldName, fieldValue, imagesData);
 				break;
 
 			case 'repeater':
@@ -100,9 +98,18 @@
 	 * @returns {string} Field type
 	 */
 	getFieldType(fieldWrapper) {
+		// Check dataset first (most reliable for our use case)
+		if (fieldWrapper.dataset.fieldType) {
+			return fieldWrapper.dataset.fieldType;
+		}
+
+		if (fieldWrapper.dataset.type) {
+			return fieldWrapper.dataset.type;
+		}
+
 		// Check for specific field classes
 		const typeClasses = [
-			'image', 'gallery', 'repeater', 'taxonomy', 'user', 'location',
+			'upload', 'repeater', 'taxonomy', 'user', 'location',
 			'set', 'checkbox', 'select', 'radio', 'true_false', 'date',
 			'time', 'datetime', 'editor', 'number', 'text', 'textarea',
 			'email', 'url', 'tel', 'phone'
@@ -114,11 +121,6 @@
 			}
 		}
 
-		// Check for data attribute
-		if (fieldWrapper.dataset.type) {
-			return fieldWrapper.dataset.type;
-		}
-
 		// Check input type
 		const input = fieldWrapper.querySelector('input, select, textarea');
 		if (input) {
@@ -138,7 +140,12 @@
 	 */
 	populateTextField(fieldWrapper, fieldName, fieldValue) {
 		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input, textarea`);
+		// Skip file inputs - browsers don't allow setting their values programmatically
+		if (input && input.type === 'file') {
+			return;
+		}
 		if (input) {
+
 			input.value = String(fieldValue || '');
 
 			// Update character counter if present
@@ -413,134 +420,176 @@
 	}
 
 	/**
-	 * Populate image fields
+	 * Populate upload fields (images, videos, files)
 	 */
-	populateImageField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
+	populateUploadField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
+		// Check if this is a timeline gallery
+		const isTimeline = fieldWrapper.dataset.subtype === 'timeline' || fieldName === 'timeline';
+
+		if (isTimeline) {
+			this.populateTimelineGallery(fieldWrapper, fieldName, fieldValue, imagesData);
+			return;
+		}
+
 		if (!fieldValue) {
 			return;
 		}
 
 		// Handle comma-separated IDs or single ID
-		const imageIds = String(fieldValue).split(',').filter(id => parseInt(id.trim()));
-		if (imageIds.length === 0) {
+		const itemIds = String(fieldValue).split(',').filter(id => parseInt(id.trim()));
+		if (itemIds.length === 0) {
 			return;
 		}
 
 		// Update hidden input
 		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
 		if (hiddenInput) {
-			hiddenInput.value = imageIds.join(',');
+			hiddenInput.value = itemIds.join(',');
 		}
 
-		// Update image display
+		// Update display grid
 		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('uploadItem');
-				let img = image.querySelector('img');
+			itemIds.forEach(itemId => {
+				const template = window.getTemplate('uploadItem');
 
-				let details = image.querySelector('details');
-				let meta = window.getTemplate('uploadMeta')
-				details.append(meta);
-				[
-					img.src,
-					img.alt,
-					image.querySelector('[name="image-title"]').value,
-					image.querySelector('[name="image-alt-text"]').value,
-					image.querySelector('[name="image-caption"]').value,
-				] = [
-					imagesData[imageId].medium,
-					imagesData[imageId].alt,
-					imagesData[imageId].title,
-					imagesData[imageId].alt,
-					imagesData[imageId].caption,
-				];
+				if (!template) {
+					console.warn('uploadItem template not found');
+					return;
+				}
 
-				details.querySelector('.upload-meta > .hint')?.remove();
+				let input = template.querySelector('input[name="select-item"]');
+				let label = template.querySelector('label[for="select-item"]');
+				template.dataset.id = itemId;
+				input.name = input.name + `-${itemId}`;
+				input.id = input.name;
+				label.htmlFor = input.name;
 
-				grid.append(image);
+
+				const img = template.querySelector('img');
+
+				template.querySelector('video').remove();
+				const details = template.querySelector('details');
+
+				// Populate with data
+				if (imagesData[itemId]) {
+					const data = imagesData[itemId];
+					if (img) {
+						img.src = data.medium || data.small || data.large || '';
+						img.alt = data['image-alt-text'] || data.alt || '';
+					}
+
+					// Populate metadata fields
+					const titleInput = template.querySelector('[name="image-title"]');
+					const altInput = template.querySelector('[name="image-alt-text"]');
+					const captionInput = template.querySelector('[name="image-caption"]');
+
+					if (titleInput) titleInput.value = data['image-title'] || data.title || '';
+					if (altInput) altInput.value = data['image-alt-text'] || data.alt || '';
+					if (captionInput) captionInput.value = data['image-caption'] || data.caption || '';
+				} else {
+					console.warn('No image data found for ID:', itemId);
+				}
+
+				// Remove hint if present
+				details?.querySelector('.upload-meta > .hint')?.remove();
+
+				grid.append(template);
 			});
 
-			if (imageIds.length > 0) {
-				if (uploadContainer) {
-					uploadContainer.hidden = true;
-				}
+			// Hide upload container if items exist
+			if (itemIds.length > 0 && uploadContainer) {
+				uploadContainer.hidden = true;
 			}
 		}
 	}
 
-	/**
-	 * Populate gallery fields
-	 */
-	populateGalleryField(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) {
+	populateTimelineGallery(fieldWrapper, fieldName, fieldValue, imagesData) {
+		if (!fieldValue || typeof fieldValue !== 'object') {
 			return;
 		}
 
-		const imageIds = Object.keys(fieldValue);
+		const imageIds = Object.values(fieldValue);
 		if (imageIds.length === 0) {
 			return;
 		}
 
-		// Update image display
+		// Update 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');
+			console.log(imageIds);
+			for (let [index, data] of Object.entries(fieldValue)) {
+				let imageId = data['post_thumbnail'];
+				const template = window.getTemplate('timelineItem');
+				if (!template) return;
 
-				image.querySelector('video')?.remove();
-				image.querySelector('.select-item span')?.remove();
-				let data = fieldValue[imageId];
+				const img = template.querySelector('img');
 
-				let imgData = imagesData[imageId];
+				// Remove unnecessary elements
+				template.querySelector('video')?.remove();
+				template.querySelector('.select-item span')?.remove();
 
-				img.src = imgData.medium;
+				let input = template.querySelector('input[name="select-item"]');
+				let label = template.querySelector('label[for="select-item"]');
+				template.dataset.id = imageId;
+				template.dataset.postId = data.id;
+				input.name = input.name + `-${imageId}`;
+				input.id = input.name;
+				label.htmlFor = input.name;
 
-				data = { ... data, ... imgData};
+				const imgData = imagesData[imageId];
 
-				let fields = image.querySelectorAll('.field');
+				// Set image source
+				if (img && imgData) {
+					img.src = imgData.medium || imgData.small || imgData.large || '';
+					img.title = imgData['image-title'];
+				}
+
+				// Merge data
+				const mergedData = { ...data, ...imgData };
+
+				// Populate fields
+				const fields = template.querySelectorAll('.field');
 				fields.forEach(field => {
-					if (field.classList.contains('group')){
+					if (field.classList.contains('group')) {
 						return;
 					}
-					let input = field.querySelector('input, textarea');
 
-					let label = field.querySelector('label');
-					let name = input.name.replace('upload_data::','');
+					const input = field.querySelector('input:not([type="file"]), textarea');
+					if (!input) return;
 
-					let value = data[name];
+					const label = field.querySelector('label');
+					const name = input.name.replace('upload_data::', '');
+					const value = mergedData[name];
+
+					// Populate the field
 					this.populateField(field, name, value, imagesData);
 
-					let id = data.id;
-					let newName = `[${id}]${name}`;
-					[
-						input.name,
-						input.id,
-						label.htmlFor
-					] = [
-						newName,
-						newName,
-						newName,
-					]
+					// Update field identifiers
+					const id = data.id;
+					const newName = `[${id}]${name}`;
+					input.name = newName;
+					input.id = newName;
+					if (label) label.htmlFor = newName;
 				});
-				grid.append(image);
-			});
+
+				grid.append(template);
+			}
+
+			// Hide upload container if items exist
+			if (imageIds.length > 0 && uploadContainer) {
+				uploadContainer.hidden = true;
+			}
 		}
 	}
 
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 38d9105..56dcb97 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -5,7 +5,6 @@
 class QueueManager {
 	constructor(config = {}) {
 		this.canUpdateUI = true;
-		console.log('jvbSettings', jvbSettings);
 		this.config = {
 			apiBase: jvbSettings.api,
 			maxRetries: 3,
@@ -16,7 +15,6 @@
 			...config
 		};
 		this.user = jvbSettings.currentUser;
-		console.log(this.user);
 
 
 		this.headers = {
@@ -28,9 +26,7 @@
 		this.errors = window.jvbError;
 
 		// Initialize DataStore for queue persistence
-		this.store = new window.jvbStore({
-			name: 'queue',
-			storeName: 'operations',
+		this.store = window.jvbStore.register('queue', {
 			keyPath: 'id',
 			endpoint: this.config.endpoint,
 			TTL: Infinity,
@@ -39,19 +35,7 @@
 				{name: 'type', keyPath: 'type'},
 			],
 			showLoading: false,
-			getBlobs: async (ids) => {
-				if (window.jvbUploadBlobs) {
-					if (!Array.isArray(ids) && typeof ids === 'string') {
-						ids = [ids];
-					}
-					// Get individual blobs (not all items)
-					const blobs = await Promise.all(
-						ids.map(id => window.jvbUploadBlobs.getBlob(id))
-					);
-					return blobs.filter(Boolean); // Remove nulls
-				}
-				return null;
-			}
+			delayFetch: false, // Queue should fetch immediately
 		});
 
 		this.classes = [
@@ -80,7 +64,6 @@
 		// Initialize
 		this.initUI();
 		this.initListeners();
-		console.log(this.ui);
 		if (this.ui.panel) {
 			this.popup = new window.jvbPopup({
 				popup: this.ui.panel,
@@ -109,6 +92,7 @@
 		this.store.subscribe((event, data) => {
 			switch (event) {
 				case 'data-loaded':
+				case 'items-saved':
 					// Initial load from IndexedDB
 					const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
 					if (incomplete.length > 0) {
@@ -117,9 +101,17 @@
 					this.updateUI();
 					break;
 				case 'item-saved':
+					// Check for status changes
+					if (data.item) {
+						const oldItem = this.store.data.get(data.item.id);
+						if (oldItem && oldItem.status !== data.item.status) {
+							this.handleOperationStatusChange(data.item, oldItem.status);
+						}
+					}
 					if (this.hasQueuedOperations()) {
 						this.startPolling();
 					}
+					break;
 				default:
 					this.updateUI();
 					break;
@@ -128,10 +120,30 @@
 		});
 		this.notify('queue-initialized', {operations: incomplete});
 	}
+
+	/**
+	 * Handle operation status changes and notify subscribers
+	 */
+	handleOperationStatusChange(operation, oldStatus) {
+		if (!operation || oldStatus === operation.status) return;
+
+		// Notify based on new status
+		switch(operation.status) {
+			case 'completed':
+				this.notify('operation-completed', operation);
+				break;
+			case 'failed':
+				this.notify('operation-failed', operation);
+				break;
+			case 'failed_permanent':
+				this.notify('operation-failed-permanent', operation);
+				break;
+		}
+	}
 	/**
 	 *
 	 * @param {object} operation
-	 * @param {string} operatio	n.endpoint The endpoint, excluding the apiBase
+	 * @param {string} operation.endpoint The endpoint, excluding the apiBase
 	 * @param {object} operation.data The data to save
 	 * @param {boolean} operation.canMerge Whether data can merge
 	 * @param {string} operation.title The title of the operation for the Queue Panel
@@ -186,6 +198,7 @@
 		}
 
 		console.log('Added to Queue: ', item);
+		this.store.clearCache();
 
 		//Add new operation to DataStore
 		this.setQueue(item);
@@ -312,10 +325,6 @@
 				// 	console.log(pair[0], pair[1]);
 				// }
 
-				console.log('Sending to server:');
-				for (var [key, value] of requestBody.entries()) {
-					console.log(key, value);
-				}
 			} else {
 				requestBody = JSON.stringify({
 					...operation.data,
@@ -409,6 +418,7 @@
 
 		this.pollTimer = setInterval(async () => {
 			try {
+				this.store.clearCache();
 				await this.store.fetch(); // Fetches from server, updates store.data
 
 				const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
@@ -474,7 +484,7 @@
 						'Content-Type': 'application/json',
 						...this.headers
 					},
-					body: JSON.stringify({ids,action})
+					body: JSON.stringify({ids,action, user: jvbSettings.currentUser})
 				}
 			);
 
@@ -574,6 +584,8 @@
 			return;
 		}
 		if (e.target.closest(this.selectors.refreshButton)) {
+			this.store.clearCache();
+			this.store.clearHttpHeaders(); // Clear cached headers first
 			this.store.fetch();
 		} else if (e.target.closest(this.selectors.clearButton)) {
 			const completedOps = this.getOperationsByStatus('completed');
@@ -666,13 +678,27 @@
 		if (!this.canUpdateUI) {
 			return;
 		}
-		const stats = this.getQueueStats();
+
+		// Get current operations from store
+		const operations = Array.from(this.store.data.values());
+
+		// Get stats from last fetch response (server-provided)
+		const stats = this.store.lastResponse?.queue_stats || {
+			queued: 0,
+			localProcessing: 0,
+			uploading: 0,
+			pending: 0,
+			processing: 0,
+			completed: 0,
+			failed: 0,
+			failed_permanent: 0
+		};
 
 		// Update count badge
 		if (this.ui.count) {
-			const total = stats.total - stats.completed;
-			this.ui.count.textContent = total > 0 ? total : '';
-			this.ui.count.style.display = total > 0 ? '' : 'none';
+			const activeCount = operations.length - stats.completed;
+			this.ui.count.textContent = activeCount > 0 ? activeCount : '';
+			this.ui.count.style.display = activeCount > 0 ? '' : 'none';
 		}
 
 		// Update indicator
@@ -681,14 +707,16 @@
 				stats.pending > 0 || stats.processing > 0;
 			this.ui.indicator.classList.toggle('active', hasActive);
 		}
-		let failed = this.getOperationsByStatus('failed');
-		let completed = this.getOperationsByStatus('completed');
-		this.ui.clearButton.disabled = completed.length === 0;
-		this.ui.retryButton.disabled = failed.length === 0;
 
-		// Update filter counts
+		// Update button states
+		this.ui.clearButton.disabled = this.getOperationsByStatus('completed').length === 0;
+		this.ui.retryButton.disabled = this.getOperationsByStatus('failed').length === 0 && this.getOperationsByStatus('failed_permanent').length === 0;
+
+		// Update filter counts (from server stats)
 		Object.entries(this.ui.filters).forEach(([status, button]) => {
-			const count = status === 'all' ? stats.total : stats[status] || 0;
+			const count = status === 'all'
+				? operations.length
+				: stats[status] || 0;
 			const countEl = button.querySelector('.count');
 			if (countEl) {
 				countEl.textContent = count > 0 ? count : '';
@@ -696,7 +724,7 @@
 			button.setAttribute('data-count', count);
 		});
 
-		// Update operation list
+		// Render current operations
 		this.renderOperations();
 	}
 
@@ -755,46 +783,24 @@
 		return statusProgress[item.status] || 0;
 	}
 
-	getQueueStats() {
-		const stats = {};
-		this.statuses.forEach(status => {
-			stats[status] = 0;
-		});
-
-		Array.from(this.store.data.values())  // Change items to data
-			.forEach(op => {
-				if (stats.hasOwnProperty(op.status)) {
-					stats[op.status]++;
-				}
-			});
-
-		stats.total = Array.from(this.store.data.values()).length;  // Change items to data
-
-		return stats;
-	}
 
 	renderOperations() {
 		if (!this.ui.itemsContainer) return;
 
-		const activeFilter = this.getActiveFilter();
-		const operations = this.getFilteredOperations(activeFilter);
+		const operations = this.store.getFiltered();
 
 		// Clear container
 		window.removeChildren(this.ui.itemsContainer);
 
-		// Render each operation
+		// Render operations or empty state
 		if (operations.length === 0) {
 			let empty = window.getTemplate('emptyQueue');
 			this.ui.itemsContainer.append(empty);
 			this.a11y.announce('Nothing queued.');
 		} else {
-			let empty = this.ui.itemsContainer.querySelector('.emptyQueue');
-			if (empty) {
-				empty.remove();
-			}
 			operations.forEach(op => {
 				const element = this.createOperationUI(op);
-				this.ui.itemsContainer.appendChild(element);
+				this.ui.itemsContainer.append(element);
 			});
 		}
 	}
@@ -941,29 +947,18 @@
 	 FILTERS
 	 **************************************************/
 	setFilter(filter) {
+		// Update active button
 		Object.values(this.ui.filters).forEach(button => {
 			if (button) {
 				button.classList.toggle('active', button.dataset.filter === filter);
 			}
 		});
 
-		this.activeFilter = filter;
-		this.renderOperations();
-	}
-
-	getActiveFilter() {
-		const activeButton = this.ui.panel?.querySelector('.filter.active');
-		return activeButton?.dataset.filter || 'all';
-	}
-
-	getFilteredOperations(filter) {
-		const operations = Array.from(this.store.data.values());  // Change items to data
-
 		if (filter === 'all') {
-			return operations;
+			this.store.clearFilters();
+		} else {
+			this.store.setFilter('status', filter);
 		}
-
-		return operations.filter(op => op.status === filter);
 	}
 
 	/**************************************************************************
@@ -995,9 +990,8 @@
 			? Array.from(this.store.data.values()).filter((item) => status.includes(item.status))
 			: Array.from(this.store.data.values()).filter((item) => !status.includes(item.status));
 	}
-	async hasQueuedOperations() {
-		const queued = await this.store.query('status', 'queued');
-		return queued.length > 0;
+	hasQueuedOperations() {
+		return this.getOperationsByStatus('queued').length > 0;
 	}
 	subscribe(callback) {
 		this.subscribers.add(callback);
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index 61adcc2..9fe722d 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -29,7 +29,6 @@
 		};
 
 		this.forms = this.container.querySelectorAll('form');
-		console.log(this.forms);
 		this.popup = new window.jvbPopup({
 			toggle: this.toggle,
 			popup: this.container,
@@ -281,10 +280,8 @@
 	 * Handle form submission
 	 */
 	async handleFormSubmit(event) {
-		console.log('Form Submission!');
 		window.debouncer.cancel('check-referral');
 		event.preventDefault();
-		console.log('Still working?');
 
 		const form = event.target;
 
@@ -299,8 +296,6 @@
 		try {
 
 			let result = {success: false, message: ''};
-			console.log(form);
-			console.log(form.id);
 			if (form.id === 'referral-code-form') {
 				if (!formData.get('name')) {
 					result.message += 'We need your name to know who you are.';
diff --git a/assets/js/concise/Swiper.js b/assets/js/concise/Swiper.js
new file mode 100644
index 0000000..5b2b3de
--- /dev/null
+++ b/assets/js/concise/Swiper.js
@@ -0,0 +1,154 @@
+class Swiper {
+	constructor() {
+		this.isInitialized = false;
+		this.initSubscribers();
+		this.initHandlers();
+		this.swipe = {
+			startX: null,
+			endX: null,
+			startY: null,
+			endY: null,
+			minSwipe: 50
+		};
+
+		this.pinch = {
+			active: false,
+			startDistance: 0,
+			lastDistance: 0,
+			scale: 1
+		};
+	}
+
+	/*********************************************************************
+	 LISTENERS
+	 *********************************************************************/
+	initHandlers() {
+		this.touchStartHandler 	= this.handleTouchStart.bind(this);
+		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
+		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
+	}
+	initListeners() {
+		if (this.isInitialized) {
+			return;
+		}
+		this.isInitialized = true;
+		document.addEventListener('touchstart', this.touchStartHandler);
+		document.addEventListener('touchmove', this.touchMoveHandler);
+		document.addEventListener('touchend', this.touchEndHandler);
+	}
+	cleanupListeners() {
+		if (this.subscribers.size > 0) return;
+		this.isInitialized = false;
+		document.removeEventListener('touchstart', this.touchStartHandler);
+		document.removeEventListener('touchmove', this.touchMoveHandler);
+		document.removeEventListener('touchend', this.touchEndHandler);
+	}
+	handleTouchStart(e) {
+		if (e.touches.length === 2) {
+			// Two-finger pinch start
+			const dx = e.touches[0].clientX - e.touches[1].clientX;
+			const dy = e.touches[0].clientY - e.touches[1].clientY;
+			const distance = Math.sqrt(dx*dx + dy*dy);
+
+			this.pinch.active = true;
+			this.pinch.startDistance = this.pinch.lastDistance = distance;
+			this.notify('pinch-start', { distance });
+
+			return; // Don't treat this as a swipe
+		}
+
+		this.swipe.startX = e.touches[0].clientX;
+		this.swipe.startY = e.touches[0].clientY;
+	}
+	handleTouchMove(e) {
+		if (this.pinch.active && e.touches.length === 2) {
+			const dx = e.touches[0].clientX - e.touches[1].clientX;
+			const dy = e.touches[0].clientY - e.touches[1].clientY;
+			const distance = Math.sqrt(dx*dx + dy*dy);
+
+			const scale = distance / this.pinch.startDistance;
+
+			this.pinch.lastDistance = distance;
+			this.pinch.scale = scale;
+
+			this.notify('pinch-move', { e, distance, scale });
+
+			// Direction
+			if (distance > this.pinch.startDistance) {
+				this.notify('pinch-out', { scale });
+			} else {
+				this.notify('pinch-in', { scale });
+			}
+
+			return; // Do not fire swipe logic
+		}
+		this.swipe.endX = e.touches[0].clientX;
+		this.swipe.endY = e.touches[0].clientY;
+	}
+	handleTouchEnd(e) {
+		// Finish pinch
+		if (this.pinch.active) {
+			this.notify('pinch-end', {
+				finalScale: this.pinch.scale
+			});
+			this.pinch.active = false;
+			return;
+		}
+
+		if ((!this.swipe.startX || !this.swipe.endX) ||(!this.swipe.startY || !this.swipe.endY)) return;
+
+		const distanceX = this.swipe.startX - this.swipe.endX;
+		const distanceY = this.swipe.startY - this.swipe.endY;
+
+		if (Math.abs(distanceX) > this.swipe.minSwipe) {
+			let direction = distanceX > 0 ? 'swipe-right' : 'swipe-left';
+			this.notify(direction);
+		}
+		if (Math.abs(distanceY) > this.swipe.minSwipe) {
+			let direction = distanceY > 0 ? 'swipe-up' : 'swipe-down';
+			this.notify(direction);
+		}
+		this.swipe.startX = this.swipe.startY = this.swipe.endX = this.swipe.endY = null;
+	}
+	/*********************************************************************
+	 SUBSCRIBERS
+	 *********************************************************************/
+	initSubscribers() {
+		this.subscribers = new Set();
+	}
+
+	subscribe(callback) {
+		if (!this.isInitialized) {
+			this.initListeners();
+		}
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+	unsubscribe(callback) {
+		this.subscribers.delete(callback);
+		if (this.subscribers.size === 0) {
+			this.cleanupListeners();
+		}
+	}
+
+	notify(event, data = {}) {
+		this.subscribers.forEach( callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
+	/******************************************************************
+	 CLEANUP
+	 ******************************************************************/
+
+	destroy() {
+		this.subscribers.clear();
+		this.cleanupListeners();
+	}
+}
+
+window.jvbSwiper = new Swiper();
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 1dcba38..7f27f13 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -12,27 +12,33 @@
 		this.isInitializing = true;
 		this.taxonomiesToFetch = new Set();
 
-		this.store = new window.jvbStore({
-			name: `taxonomies`,
-			storeName: `terms`,
-			keyPath: 'id',
-			showLoading: false,
-			indexes: [
-				{name: 'taxonomy', keyPath: 'taxonomy'},
-				{name: 'parent', keyPath: 'parent'},
-				{name: 'slug', keyPath: 'slug', unique: true},
-				{name: 'count', keyPath: 'count'},
-			],
-			endpoint: 'terms',
-			TTL: 7200000, //2 hours
-			filters: {
-				taxonomy: '',
-				page: 1,
-				search: '',
-				parent: 0
-			},
-			required: 'taxonomy'
-		});
+		this.triggers = new Set(['.taxonomy-toggle']);
+
+		this.subscribers = new Set();
+
+		this.store = window.jvbStore.register(
+			'taxonomies',
+			{
+				storeName: `terms`,
+				keyPath: 'id',
+				showLoading: false,
+				indexes: [
+					{name: 'taxonomy', keyPath: 'taxonomy'},
+					{name: 'parent', keyPath: 'parent'},
+					{name: 'slug', keyPath: 'slug', unique: true},
+					{name: 'count', keyPath: 'count'},
+				],
+				endpoint: 'terms',
+				TTL: 2 * 60 * 1000, //2 hours
+				filters: {
+					taxonomy: '',
+					page: 1,
+					search: '',
+					parent: 0
+				},
+				required: 'taxonomy',
+				delayFetch: true,
+			});
 
 		// Central field management
 		this.fields = new Map();
@@ -43,7 +49,6 @@
 		this.currentConfig = null;
 		this.currentSingular = null;
 		this.currentPlural = null;
-		this.activeStore = null;
 
 		// Modal state
 		this.disabled = false;
@@ -64,6 +69,9 @@
 		this.scanExistingFields();
 		this.initGlobalListeners();
 
+		if (this.hasAutocomplete && window.jvbTaxCreator) {
+			this.creator = new window.jvbTaxCreator(this);
+		}
 		this.store.subscribe(this.handleStoreEvent.bind(this));
 		// Complete initialization
 		this.isInitializing = false;
@@ -76,11 +84,28 @@
 	handleStoreEvent(event, data) {
 		switch (event) {
 			case 'data-loaded':
-				// Only render if modal is open OR if it's an autocomplete request
+				const taxonomy = this.store.filters.taxonomy;
+				// Handle batch taxonomy loading (comma-separated)
+				if (taxonomy?.includes(',')) {
+					this.handleBatchDataLoaded(taxonomy, data);
+				}
+				// Update button states for this taxonomy (or taxonomies)
+				if (taxonomy) {
+					// Handle comma-separated taxonomies from batch fetch
+					const taxonomies = taxonomy.includes(',')
+						? taxonomy.split(',').map(t => t.trim())
+						: [taxonomy];
+
+					taxonomies.forEach(tax => {
+						this.updateFieldsForTaxonomy(tax);
+					});
+				}
+
+				// Only render if modal is open OR autocomplete active
 				if (this.modal?.open) {
 					this.handleTermsLoaded(data);
 				}
-				// Handle autocomplete results
+
 				if (this.isAutocompleteActive && this.activeField) {
 					const field = this.fields.get(this.activeField);
 					const terms = data.data?.items || [];
@@ -91,7 +116,6 @@
 				break;
 
 			case 'filters-changed':
-				// Modal UI updates happen here if needed
 				if (this.modal?.open) {
 					this.showLoading();
 				}
@@ -112,10 +136,12 @@
 	 */
 	handleTermsLoaded(data) {
 		this.hideLoading();
-		const terms = data.data?.items || [];
-		const pagination = data.data?.pagination || {};
+		const terms = this.store.getFiltered();  // Use getFiltered() instead of getFilteredItems()
+		const response = this.store.lastResponse?.page || {};
 		const isSearch = data.filters?.search && data.filters.search.length > 0;
-		const append = data.filters?.page > 1;
+		const append = response.page > 1;
+
+		this.notify('terms-loaded', { terms, filters: data.filters });
 
 		if (terms.length === 0) {
 			if (!append) {
@@ -124,9 +150,9 @@
 			this.observer.unobserve(this.ui.sentinel);
 		} else {
 			this.renderTerms(terms, append, isSearch);
-			this.currentTerms = terms;
+
 			// Handle pagination
-			if (pagination.has_more) {
+			if (response.has_more) {
 				this.observer.observe(this.ui.sentinel);
 			} else {
 				this.observer.unobserve(this.ui.sentinel);
@@ -154,28 +180,45 @@
 		}
 	}
 
+
+	/**
+	 * Check if taxonomy has terms and update button states
+	 */
+	updateFieldButtonState(fieldId) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		// Check store for items of this specific taxonomy
+		const hasTerms = Array.from(this.store.data.values())
+			.some(term => term.taxonomy === field.taxonomy);
+
+		if (field.toggle) {
+			field.toggle.disabled = !hasTerms && !field.canCreate;
+			field.toggle.title = !hasTerms
+				? `No ${this.getSingular(field.taxonomy)} available`
+				: `Select ${this.getPlural(field.taxonomy)}`;
+		}
+	}
 	/**
 	 * Update fields when taxonomy items are updated
 	 */
-	updateFieldsForTaxonomy(taxonomy, items) {
-		this.fields.forEach(field => {
-			if (field.taxonomy === taxonomy && field.selectedTerms.size > 0) {
-				// Update display with fresh term data
-				field.selectedTerms.forEach(termId => {
-					const term = items.find(item => item.id === termId);
-					if (term) {
-						const selectedItem = field.selectedContainer.querySelector(`[data-id="${termId}"]`);
-						if (selectedItem) {
-							selectedItem.dataset.path = term.path;
-							selectedItem.querySelector('span').textContent = term.path;
-						}
-					}
-				});
-			}
+	updateFieldsForTaxonomy(taxonomy) {
+		this.getFieldsForTaxonomy(taxonomy).forEach(field => {
+			this.updateFieldButtonState(field.id);
 		});
 	}
 
 	/**
+	 * Get fields for a specific taxonomy
+	 */
+	getFieldsForTaxonomy(taxonomy) {
+		return Array.from(this.fields.values())
+			.filter(field => field.taxonomy === taxonomy);
+	}
+
+
+
+	/**
 	 * Scan page for existing taxonomy fields and register them
 	 */
 	scanExistingFields(container = null) {
@@ -203,14 +246,18 @@
 	registerField(field, options = {}) {
 		let input = field.querySelector('input[type=hidden]');
 		if (!input) {
-			return;
+			return false;
 		}
-
 		if (!('fieldId' in field.dataset)) {
 			field.dataset.fieldId = this.createFieldId(field);
 		}
 		let fieldId = field.dataset.fieldId;
-		let button = field.querySelector('button.taxonomy-toggle');
+
+		let button = (Object.hasOwn(options, 'button')) ? options.button : field.querySelector('button.taxonomy-toggle');
+
+		if (Object.hasOwn(options, 'buttonSelector')) {
+			this.triggers.add(options.buttonSelector);
+		}
 
 		let config = {
 			id: fieldId,
@@ -226,7 +273,7 @@
 			isRequired: 'required' in button.dataset,
 			selectedTerms: new Set(),
 			toggle: button,
-			selectedContainer: field.querySelector('.selected-items'),
+			selectedContainer: (Object.hasOwn(options, 'selected')) ? options.selected : field.querySelector('.selected-items'),
 			...options
 		};
 
@@ -244,13 +291,19 @@
 			selectedIds.forEach(id => config.selectedTerms.add(id));
 		}
 
+		if (Object.hasOwn(options, 'selectedItems')) {
+			options.selectedItems.forEach(id => {
+				config.selectedTerms.add(id);
+			});
+		}
+
 		this.fields.set(fieldId, config);
 
 		// Ensure store exists for this taxonomy
 		if (this.isInitializing) {
 			this.taxonomiesToFetch.add(config.taxonomy);
 		} else {
-			this.store.setFilter('taxonomy', config.taxonomy);
+			// this.store.setFilter('taxonomy', config.taxonomy);
 		}
 
 		// Initialize display for any pre-selected values
@@ -262,32 +315,43 @@
 	}
 
 	/**
-	 * Batch fetch all unique taxonomies collected during init
+	 * Register a filter button (simplified registration for feed blocks)
 	 */
-	async batchFetchTaxonomies() {
-		if (this.taxonomiesToFetch.size === 0) return;
+	registerFilterButton(button, options = {}) {
+		const fieldId = this.createFieldId(button);
+		button.dataset.fieldId = fieldId;
 
-		const taxonomies = Array.from(this.taxonomiesToFetch);
-		this.taxonomiesToFetch.clear();
-
-		console.log(`Batch fetching ${taxonomies.length} unique taxonomies:`, taxonomies);
-
-		// Fetch each taxonomy sequentially (cache will prevent duplicates)
-		for (const taxonomy of taxonomies) {
-			await this.store.setFilters({
-				taxonomy: taxonomy,
-				page: 1,
-				search: '',
-				parent: 0
-			});
+		if (options.buttonSelector) {
+			this.triggers.add(options.buttonSelector);
 		}
 
-		// Now initialize field displays
-		this.fields.forEach((config, fieldId) => {
-			if (config.selectedTerms.size > 0) {
-				this.initFieldDisplay(fieldId);
-			}
-		});
+		const config = {
+			id: fieldId,
+			input: null,
+			container: options.container || button.closest('.filters') || button.parentElement,
+			taxonomy: button.dataset.taxonomy,
+			name: `filter_${button.dataset.taxonomy}`,
+			maxSelection: parseInt(button.dataset.max) || 0,
+			canSearch: 'search' in button.dataset,
+			hasAutocomplete: false,
+			canCreate: false,
+			isRequired: false,
+			selectedTerms: new Set(options.selectedItems || []),
+			toggle: button,
+			selectedContainer: options.selected || null,
+			isFilterMode: true,
+			...options
+		};
+
+		this.fields.set(fieldId, config);
+
+		if (this.isInitializing) {
+			this.taxonomiesToFetch.add(config.taxonomy);
+		} else {
+			this.store.setFilter('taxonomy', config.taxonomy);
+		}
+
+		return fieldId;
 	}
 
 	/**
@@ -306,21 +370,13 @@
 		if (!field || field.selectedTerms.size === 0) return;
 
 		const selectedIds = Array.from(field.selectedTerms);
-		const cachedTerms = [];
 
 		selectedIds.forEach(termId => {
-			const term = this.store.data.get(termId);
+			const term = this.store.get(termId);  // Changed from getItem
 			if (term) {
-				cachedTerms.push(term);
+				this.addTermToDisplay(fieldId, term.id, term.name, term.path);
 			}
 		});
-
-		// Display all found terms
-		cachedTerms.forEach(term => {
-			this.addTermToDisplay(fieldId, term.id, term.name, term.path);
-		});
-
-		// Don't fetch missing terms here - they should be loaded by batchFetchTaxonomies
 	}
 
 	/**
@@ -346,7 +402,6 @@
 		this.modalInstance.subscribe((event, data) => {
 			switch (event) {
 				case 'modal-open':
-					console.log(data);
 					this.openModal(data);
 					break;
 				case 'modal-close':
@@ -401,7 +456,7 @@
 		// Initialize intersection observer for infinite scroll
 		this.observer = new IntersectionObserver((entries) => {
 			entries.forEach(entry => {
-				if (entry.isIntersecting && this.activeStore) {
+				if (entry.isIntersecting) {
 					this.loadMoreTerms();
 				}
 			});
@@ -424,10 +479,23 @@
 
 	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));
+		// Preload taxonomy data on focus
+		document.addEventListener('focus', (e) => {
+			if (!('autocomplete' in e.target.dataset)) {
+				return;
+			}
+
+			const fieldId = this.getFieldId(e.target);
+			const field = this.fields.get(fieldId);
+
+			if (!field) return;
+
+			// Preload this taxonomy's data
+			this.preloadTaxonomy(field.taxonomy);
+		}, true); // Use capture phase
 	}
 
 	/**
@@ -435,7 +503,8 @@
 	 */
 	handleClick(e) {
 		// Handle taxonomy toggle buttons
-		const toggleButton = window.targetCheck(e, '.taxonomy-toggle');
+		const toggleButton = window.targetCheck(e, Array.from(this.triggers));
+
 		if (toggleButton) {
 			e.preventDefault();
 			this.handleToggleClick(toggleButton);
@@ -496,65 +565,53 @@
 				return;
 			}
 
-			this.setActiveField(fieldId);
-			this.modalInstance.handleOpen();
+
+			this.setActiveField(fieldId, true);
 
 		} catch (error) {
 			console.error('Error handling toggle click:', error);
-			this.error?.handleError(error, {
-				component: 'TaxonomySelector',
-				action: 'handleToggleClick'
-			});
+			if (this.error?.log) {
+				this.error.log(error, {
+					component: 'TaxonomySelector',
+					action: 'handleToggleClick'
+				});
+			}
 		}
 	}
 
 	/**
 	 * Set the active field for modal operations
 	 */
-	setActiveField(fieldId) {
+	setActiveField(fieldId, openModal = false) {
 		this.activeField = fieldId;
 		this.currentConfig = this.fields.get(fieldId);
 
-		this.currentSingular = jvbSettings.labels[this.currentConfig.taxonomy].single;
-		this.currentPlural = jvbSettings.labels[this.currentConfig.taxonomy].plural;
+		this.currentSingular = this.getSingular(this.currentConfig.taxonomy);
+		this.currentPlural = this.getPlural(this.currentConfig.taxonomy);
 
-		// Get or create store for this taxonomy
+		if (openModal) {
+			this.modalInstance.handleOpen();
+		}
+
+		// Set taxonomy filter - store handles the rest
 		this.store.setFilter('taxonomy', this.currentConfig.taxonomy);
 
 		// Clear modal selection state
 		this.selectedTerms.clear();
 
 		// Copy field's current selections to modal state
-		if (this.currentConfig.selectedTerms) {
-			let termsToFetch = [];
-			this.currentConfig.selectedTerms.forEach(termId => {
-				const term = this.store.getItem(termId);
-				if (term) {
-					this.selectedTerms.set(termId, {
-						id: termId,
-						name: term.name,
-						path: term.path
-					});
-				} else {
-					termsToFetch.push(termId);
-				}
-			});
-			if (termsToFetch.length > 0) {
-				let terms = this.fetchSpecificTerms(termsToFetch);
-				terms.forEach(term => {
-					this.selectedTerms.set(term.id, {
-						id: term.id,
-						name: term.name,
-						path: term.path
-					});
+		this.currentConfig.selectedTerms.forEach(termId => {
+			const term = this.store.get(termId);
+			if (term) {
+				this.selectedTerms.set(termId, {
+					id: termId,
+					name: term.name,
+					path: term.path
 				});
 			}
-		}
+		});
 	}
 
-	fetchSpecificTerms(terms) {
-		return [];
-	}
 
 	/**
 	 * Handle clicks within modal
@@ -627,43 +684,70 @@
 			filterCallback: callback // Store the callback
 		});
 
-		this.setActiveField(virtualFieldId);
+		this.setActiveField(virtualFieldId, true);
 		this.modalInstance.handleOpen();
 	}
 
 	/**
 	 * Open modal and initialize
 	 */
-	openModal(config) {
-		this.activeField = config.fieldId;
-		this.currentConfig = config;
+	openModal() {
+		if (!this.currentConfig) {
+			console.error('No active field set');
+			return;
+		}
+
 		// Initialize creator if available
-		if (config.canCreate && 'jvbTaxCreator' in window) {
+		if (!this.creator && this.currentConfig.canCreate && 'jvbTaxCreator' in window) {
 			this.creator = new window.jvbTaxCreator(this);
-		} else if (this.creator) {
-			delete this.creator;
 		}
 
-		// Load selected terms into modal state
-		this.selectedTerms = new Set(config.selectedTerms);
+		// Update modal UI
+		this.updateModalForTaxonomy();
 
-		// Only fetch if taxonomy changed
-		const currentTaxonomy = this.store.filters.taxonomy;
-		if (currentTaxonomy !== config.taxonomy) {
-			this.store.setFilters({
-				taxonomy: config.taxonomy,
-				page: 1,
-				search: '',
-				parent: 0
-			});
-		}
-
-		// Reset UI
-		window.removeChildren(this.ui.termsList);
-		this.ui.search.value = '';
+		// Load selected terms display
+		this.updateModalSelections();
 		this.updateSelectionCount();
 
-		this.modalInstance.open();
+		// Clear terms list and show loading
+		window.removeChildren(this.ui.termsList);
+		this.showLoading();
+	}
+
+	/**
+	 * Update selection count display in modal
+	 */
+	updateSelectionCount() {
+		if (!this.currentConfig) return;
+
+		const count = this.selectedTerms.size;
+		const max = this.currentConfig.maxSelection;
+
+		// Update any count display elements
+		const countElement = this.modal?.querySelector('.selection-count');
+		if (countElement) {
+			if (max > 0) {
+				countElement.textContent = `${count} of ${max} selected`;
+			} else {
+				countElement.textContent = `${count} selected`;
+			}
+		}
+	}
+
+
+
+	/**
+	 * Get singular label for taxonomy
+	 */
+	getSingular(taxonomy) {
+		return jvbSettings.labels[taxonomy]?.single || taxonomy;
+	}
+
+	/**
+	 * Get plural label for taxonomy
+	 */
+	getPlural(taxonomy) {
+		return jvbSettings.labels[taxonomy]?.plural || taxonomy;
 	}
 
 	/**
@@ -673,12 +757,17 @@
 		this.observer.unobserve(this.ui.sentinel);
 		window.removeChildren(this.ui.termsList);
 
+		this.notify('selected-terms', {
+			terms: this.selectedTerms,
+			taxonomy: this.currentConfig.taxonomy
+		});
+
 		if (this.currentConfig?.isFilterMode) {
 			if (this.currentConfig.filterCallback) {
 				const selectedIds = Array.from(this.selectedTerms.keys());
 				this.currentConfig.filterCallback(selectedIds, this.currentConfig.taxonomy);
 			}
-			this.fields.delete(this.activeField);
+			// this.fields.delete(this.activeField);
 		} else if (this.activeField) {
 			this.saveSelectionsToField(this.activeField);
 		}
@@ -688,7 +777,7 @@
 			this.ui.search.input.removeEventListener('input', this.searchHandler);
 		}
 
-		if (this.creator) {
+		if (!this.hasAutocomplete && this.creator) {
 			delete this.creator;
 		}
 
@@ -1087,7 +1176,7 @@
 	 * Navigate to parent term
 	 */
 	navigateToParent() {
-		// Single call instead of two setFilter + manual fetch
+		// Store handles fetch automatically
 		this.store.setFilters({
 			parent: 0,
 			page: 1
@@ -1101,7 +1190,7 @@
 	 * Navigate to child term
 	 */
 	navigateToChild(termId, termName) {
-		// Single call - auto-fetches
+		// Store handles fetch automatically
 		this.store.setFilters({
 			parent: termId,
 			page: 1
@@ -1118,7 +1207,7 @@
 	navigateToPath(pathLevel) {
 		const parentId = parseInt(pathLevel.dataset.id) || 0;
 
-		// Single call - auto-fetches
+		// Store handles fetch automatically
 		this.store.setFilters({
 			parent: parentId,
 			page: 1
@@ -1132,17 +1221,19 @@
 	 * Load more terms (pagination)
 	 */
 	loadMoreTerms() {
-		if (!this.activeStore) return;
-
-		const currentPage = this.activeStore.filters.page || 1;
+		const currentPage = this.store.filters.page || 1;
 		this.store.setFilter('page', currentPage + 1);
-
 	}
 
 	/**
 	 * Render terms list
 	 */
-	renderTerms(terms, append = false, showPath = false) {
+	renderTerms(terms = null, append = false, showPath = false) {
+		// If no terms provided, get from store
+		if (!terms) {
+			terms = this.store.getFiltered();
+		}
+
 		if (!append) {
 			window.removeChildren(this.ui.termsList);
 		}
@@ -1154,10 +1245,10 @@
 			return;
 		}
 
-		// Use this.store instead of this.activeStore
 		const currentParent = this.store.filters.parent || 0;
 		this.ui.breadcrumbs.back.hidden = currentParent === 0;
 
+		const fragment = document.createDocumentFragment();
 		terms.forEach(term => {
 			const element = this.createTermElement({
 				id: parseInt(term.id),
@@ -1168,9 +1259,11 @@
 			});
 
 			if (element) {
-				this.ui.termsList.appendChild(element);
+				fragment.appendChild(element);
 			}
 		});
+
+		this.ui.termsList.appendChild(fragment);
 	}
 
 	/**
@@ -1314,6 +1407,117 @@
 
 		return null;
 	}
+	/********************************************
+	BATCH FETCH: fetches first page for all taxonomies in one call
+	 ********************************************/
+	async batchFetchTaxonomies() {
+		if (this.taxonomiesToFetch.size === 0) return;
+
+		const taxonomies = Array.from(this.taxonomiesToFetch);
+		this.taxonomiesToFetch.clear();
+
+		// Single fetch - the data-loaded event will handle cache splitting
+		this.store.setFilters({
+			taxonomy: taxonomies.join(','),
+			page: 1,
+			search: '',
+			parent: 0
+		});
+	}
+	handleBatchDataLoaded(taxonomyString, data) {
+		const taxonomies = taxonomyString.split(',').map(t => t.trim());
+		const storeInstance = this.store.getStore(); // Access actual store instance
+
+		taxonomies.forEach(taxonomy => {
+			const filters = {
+				taxonomy: taxonomy,
+				page: 1,
+				search: '',
+				parent: 0
+			};
+
+			// Use the internal generateCacheKey method via store instance
+			const cacheKey = this.generateCacheKeyForFilters(filters);
+
+			// Filter items for this specific taxonomy
+			const items = Array.from(this.store.data.values())
+				.filter(item => item.taxonomy === taxonomy)
+				.map(item => item.id);
+
+			const cacheEntry = {
+				key: cacheKey,
+				items: items,
+				timestamp: Date.now(),
+				endpoint: storeInstance.config.endpoint,
+				filters: filters
+			};
+
+			// Set in both memory and IndexedDB cache
+			storeInstance.cache.set(cacheKey, cacheEntry);
+
+			// Persist to IndexedDB (if available)
+			if (storeInstance.db?.objectStoreNames.contains('cache')) {
+				const tx = storeInstance.db.transaction(['cache'], 'readwrite');
+				const objectStore = tx.objectStore('cache');
+				objectStore.put(cacheEntry);
+			}
+
+			// Update button states for this taxonomy
+			this.updateFieldsForTaxonomy(taxonomy);
+		});
+
+		// Initialize field displays
+		this.fields.forEach((config, fieldId) => {
+			if (config.selectedTerms.size > 0) {
+				this.initFieldDisplay(fieldId);
+			}
+		});
+	}
+
+	/**
+	 * Generate cache key for given filters (matching DataStore's internal logic)
+	 */
+	generateCacheKeyForFilters(filters) {
+		const normalized = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+
+		return JSON.stringify(normalized);
+	}
+
+	/**
+	 * Preload taxonomy data on hover
+	 */
+	async preloadTaxonomy(taxonomy) {
+		// Trigger fetch for this taxonomy
+		this.store.setFilters({
+			taxonomy: taxonomy,
+			page: 1,
+			search: '',
+			parent: 0
+		});
+	}
+	/*****************************************
+	SUBSCRIBERS
+	 *****************************************/
+
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data = {}) {
+		this.subscribers.forEach( callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
 
 	/**
 	 * Clean up
@@ -1329,6 +1533,7 @@
 		// Destroy all stores
 		this.store.destroy();
 
+		this.subscribers.clear();
 		// Clear all maps
 		this.fields.clear();
 		this.selectedTerms.clear();
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index b59101c..59a0d5f 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -1,42 +1,51 @@
+/**
+ * UploadManager - Refactored for clarity
+ *
+ * Architecture:
+ * - DataStores (fieldStore, uploadStore) = Recovery cache only, cleared after successful upload
+ * - Maps (uploadElements, fieldElements) = Runtime DOM references
+ * - Upload data flows: File → Process → Queue → Server → Clean up stores
+ */
 class UploadManager {
 	constructor() {
-		//Load dependencies
+		// Load dependencies
 		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
 
-		//Load Datastore
-		this.fieldStore = new window.jvbStore({
-			name: 'upload_fields',
-			storeName: 'fieldStates',
-			keyPath: 'id',
-			version: 2,
+		// RECOVERY STORES - Cleared after successful upload
+		this.fieldStore = window.jvbStore.register(
+			'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' }
+				],
+				TTL: 86400000 * 7, // 1 week
+				delayFetch: true
+			});
 
-			indexes: [
-				{ name: 'fieldId', keyPath: 'fieldId' },
-				{ name: 'timestamp', keyPath: 'timestamp' },
-				{ name: 'content', keyPath: 'content' },
-				{ name: 'itemId', keyPath: 'itemId' },
-				{ name: 'status', keyPath: 'status' }
-			],
-
-			stripDOMReferences: true,
-			TTL: 86400000*7 // 24 hours -> 1 week
-		});
-
-		this.uploadStore = new window.jvbStore({
-			name: 'uploads',
-			storeName: 'uploads',
-			keyPath: 'id',
-			storeBlobs: true,
-
-			indexes: [
-				{ name: 'fieldId', keyPath: 'fieldId' },
-				{ name: 'status', keyPath: 'status' },
-				{ name: 'groupId', keyPath: 'groupId' },
-				{ name: 'attachmentId', keyPath: 'attachmentId' }
-			],
-		});
+		this.uploadStore = window.jvbStore.register(
+			'uploads',
+			{
+				name: 'uploads',
+				storeName: 'uploads',
+				keyPath: 'id',
+				storeBlobs: true,
+				indexes: [
+					{ name: 'fieldId', keyPath: 'fieldId' },
+					{ name: 'status', keyPath: 'status' },
+					{ name: 'groupId', keyPath: 'groupId' },
+					{ name: 'attachmentId', keyPath: 'attachmentId' }
+				],
+				delayFetch: true
+			});
 
 		window.jvbUploadBlobs = this.uploadStore;
 
@@ -44,20 +53,24 @@
 		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
 		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
 
-		//Load Worker
-		this.initWorker();
+		// RUNTIME DATA - DOM references and ephemeral state
+		this.uploadElements = new Map();  // uploadId → { element, preview, location }
+		this.fieldElements = new Map();   // fieldId → { element, ui, config }
+		this.groupElements = new Map();   // groupId → { element, grid, fieldId }
 
-		// Core data structures
-		this.fields = new Map();
-		this.uploads = new Map();
-		this.groups = new Map();
+		// Selection and UI state
 		this.selected = new Map();
 		this.selectionHandlers = new Map();
 		this.previewUrls = new Set();
-		//Notification and Subscribers
+		this.sortableInstances = new Map();
+
+		// Worker for image processing
+		this.initWorker();
+
+		// Notification subscribers
 		this.subscribers = new Set();
 
-		// Controllers (will be initialized based on features)
+		// Controllers
 		this.dragController = null;
 
 		// Selectors
@@ -65,7 +78,6 @@
 			field: {
 				field: '[data-upload-field]',
 				input: 'input[type="file"]',
-				hiddenValue: 'input[type="hidden"]',
 				dropZone: '.file-upload-container',
 				preview: '.item-grid.preview',
 				progress: '.image-progress'
@@ -86,7 +98,6 @@
 			}
 		};
 
-
 		this.statusMapping = {
 			'received': 'Image Received',
 			'local_processing': 'Processing Image...',
@@ -100,66 +111,48 @@
 		};
 
 		// Sortable configuration
-		this.sortableInstances = new Map();
 		this.sortableConfig = {
 			animation: 150,
 			draggable: '.item',
-			handle: '.select-item-label, img', // Can drag by image or checkbox label
+			handle: '.select-item, img',
 			ghostClass: 'sortable-ghost',
 			chosenClass: 'sortable-chosen',
 			dragClass: 'sortable-drag',
-			onEnd: (evt) => {
-				this.handleReorder(evt);
-			}
+			onEnd: (evt) => this.handleReorder(evt)
 		};
 
 		this.init();
 	}
 
 	async init() {
-		// Load existing data
-		await this.loadFields();
-		await this.loadUploads();
-		// Initialize fields
 		this.initializeFields();
-
-		// Set up core listeners
 		this.initListeners();
 
+		// Queue integration - handle completion/failure
 		this.queue.subscribe((event, operation) => {
-			if (operation.endpoint !== 'uploads' && operation.endpoint !== 'uploads/meta') {
+			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
 				return;
 			}
+
 			const fieldId = operation.data instanceof FormData
 				? operation.data.get('fieldId')
-				: operation.data.fieldId;
+				: operation.data?.fieldId;
+
 			switch(event) {
 				case 'cancel-operation':
-					if (fieldId) {
-						this.clearField(fieldId);
-					}
+					if (fieldId) this.handleOperationCancelled(fieldId);
 					break;
 				case 'operation-status':
-					if (fieldId) {
-						this.updateFieldStatus(fieldId, operation.status);
-					}
+					if (fieldId) this.updateFieldStatus(fieldId, operation.status);
 					break;
 				case 'operation-complete':
-					const results = operation.result?.data || [];
-					results.forEach(result => {
-						const upload = this.uploads.get(result.upload_id);
-						if (upload) {
-							upload.attachmentId = result.attachment_id;
-							upload.status = 'completed';
-							this.uploads.set(upload.id, upload);
-						}
-					});
-					if (fieldId) {
-						this.cleanField(fieldId);
-					}
+					this.handleOperationComplete(operation, fieldId);
+					break;
+				case 'operation-failed':
+				case 'operation-failed-permanent':
+					this.handleOperationFailed(operation, fieldId);
 					break;
 			}
-
 		});
 
 		window.addEventListener('beforeunload', () => {
@@ -172,12 +165,9 @@
 			worker: null,
 			timeout: null,
 			tasks: new Map(),
-			restart: {
-				count: 0,
-				max: 3,
-			},
+			restart: { count: 0, max: 3 },
 			settings: {
-				timeout: 10000, //10 seconds per image
+				timeout: 10000,
 				batchSize: 1,
 				maxConcurrent: 3,
 				restartAfterTimeout: true
@@ -185,71 +175,68 @@
 		};
 	}
 
-	/**
-	 * Initialize all upload fields on the page
-	 */
+	/*******************************************************************************
+	 * FIELD MANAGEMENT
+	 *******************************************************************************/
+
 	initializeFields() {
 		const fields = document.querySelectorAll(this.selectors.field.field);
-		fields.forEach(uploader => {
-			this.registerUploader(uploader);
-		});
+		fields.forEach(uploader => this.registerUploader(uploader));
 	}
 
 	scanFields(container) {
 		const fields = container.querySelectorAll(this.selectors.field.field);
-		fields.forEach(uploader => {
-			this.registerUploader(uploader);
-		});
+		fields.forEach(uploader => this.registerUploader(uploader));
 	}
 
 	registerUploader(uploader) {
 		const fieldId = this.determineFieldId(uploader);
 		const config = this.extractFieldConfig(uploader);
+		const ui = this.buildFieldUI(uploader);
 
-		// Create field data structure
-		const field = {
+		// Store field data with Sets for runtime
+		const fieldData = {
 			id: fieldId,
 			config: config,
-			element: uploader,
-			ui: this.buildFieldUI(uploader),
 			uploads: new Set(),
-			groups: new Set(),
+			groups: [],
 			state: 'ready',
+			timestamp: Date.now()
 		};
 
-		this.fields.set(fieldId, field);
+		// Save to store (will convert Sets to Arrays automatically)
+		this.fieldStore.save(fieldData);
+
+		// Store DOM references separately
+		this.fieldElements.set(fieldId, { element: uploader, ui, config });
+
 		uploader.dataset.uploader = fieldId;
 		this.addFieldSelectionHandler(fieldId);
 
 		if (config.destination === 'post_group' && !this.dragController) {
 			this.initGroupFeatures();
 		}
+
 		if (config.type !== 'single') {
-			this.initSortable(field);
+			this.initSortable(fieldId);
 		}
 
 		return fieldId;
 	}
 
-	/**
-	 * Extract configuration from field element
-	 */
 	extractFieldConfig(fieldElement) {
 		return {
 			destination: fieldElement.dataset.destination || 'meta',
 			content: fieldElement.dataset.content || null,
 			mode: fieldElement.dataset.mode || 'direct',
 			type: fieldElement.dataset.type || 'single',
-			name: fieldElement.dataset.field,  // Field name for meta
-			itemID: fieldElement.dataset.itemId || 0,  // Post/term/user ID
+			name: fieldElement.dataset.field,
+			itemID: fieldElement.dataset.itemId || 0,
 			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
 			subtype: fieldElement.dataset.subtype || 'image'
 		};
 	}
 
-	/**
-	 * Build UI element references for a field
-	 */
 	buildFieldUI(fieldElement) {
 		let UI = {
 			field: fieldElement,
@@ -279,92 +266,129 @@
 		return UI;
 	}
 
-	/**
-	 * Set up core event listeners
-	 */
-	initListeners() {
-		this.clickHandler = this.handleClick.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
+	/*******************************************************************************
+	 * SORTABLE INITIALIZATION
+	 *******************************************************************************/
 
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('change', this.changeHandler);
+	initSortable(fieldId) {
+		if (!window.Sortable) return;
 
-		// External file drops
-		this.dragEnterHandler 	= this.handleExternalDragEnter.bind(this);
-		this.dragLeaveHandler 	= this.handleExternalDragLeave.bind(this);
-		this.dragOverHandler 	= this.handleExternalDragOver.bind(this);
-		this.dropHandler 		= this.handleExternalDrop.bind(this);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
 
-		document.addEventListener('dragenter', this.dragEnterHandler);
-		document.addEventListener('dragleave', this.dragLeaveHandler);
-		document.addEventListener('dragover', this.dragOverHandler);
-		document.addEventListener('drop', this.dropHandler);
+		// Find all sortable grids (preview + group grids, but not restore grids)
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+
+		grids.forEach((grid) => {
+			// Skip if already initialized
+			if (grid.sortableInstance) return;
+
+			const isGroupGrid = grid.classList.contains('group');
+			const gridId = isGroupGrid
+				? `${fieldId}-group-${grid.closest('.upload-group')?.dataset.groupId}`
+				: `${fieldId}-preview`;
+
+			const sortableInstance = new Sortable(grid, {
+				...this.sortableConfig,
+				group: {
+					name: fieldId,
+					pull: true,
+					put: true
+				},
+
+				onAdd: (evt) => {
+					// Re-enable when items added
+					this.updateSortableState(evt.to);
+				},
+				onRemove: (evt) => {
+					// Disable source if now empty
+					this.updateSortableState(evt.from);
+				}
+			});
+
+			// Store reference on element for easy access
+			grid.sortableInstance = sortableInstance;
+			this.sortableInstances.set(gridId, sortableInstance);
+		});
 	}
 
 	/**
-	 * Initialize group-specific features (drag & drop for rearranging)
+	 * Update sortable enabled/disabled state based on item count
 	 */
+	updateSortableState(grid) {
+		const sortable = grid?.sortableInstance;
+		if (!sortable) return;
+
+		const hasItems = grid.querySelectorAll('.item').length > 0;
+		sortable.option('disabled', !hasItems);
+	}
+
+	/**
+	 * Refresh sortable for a field (call after adding/removing items dynamically)
+	 */
+	refreshSortable(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => this.updateSortableState(grid));
+	}
+
+	handleReorder(evt) {
+		const grid = evt.to;
+		const fieldWrapper = grid.closest('.field, .upload');
+		if (!fieldWrapper) return;
+
+		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+		let items = Array.from(fieldWrapper.querySelectorAll('.item')).map(upload => upload.dataset.id);
+		console.log(items);
+		hiddenInput.value = items.join(',');
+
+		if (window.jvbA11y) {
+			window.jvbA11y.announce('Item reordered');
+		}
+
+		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
+			detail: { from: evt.from, to: evt.to, oldIndex: evt.oldIndex, newIndex: evt.newIndex },
+			bubbles: true
+		}));
+	}
+
+	/*******************************************************************************
+	 * DRAG & DROP INITIALIZATION
+	 *******************************************************************************/
+
 	initGroupFeatures() {
-		// Initialize drag controller for rearranging items
 		this.dragController = new window.jvbDragHandler({
-			// What can be dragged
 			draggableSelector: this.selectors.items.item,
-
-			// Where items can be dropped
 			dropTargetSelector: `${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,
-
-			// Don't start drag on interactive elements
 			ignoreSelector: 'input:not(.upload-select), button, select, textarea, details, summary, a',
 			previewElement: 'img, video, .icon',
-
-			// Extract upload ID from element
-			getItemId: (element) => {
-				return element.dataset.uploadId;
-			},
-
-			// Get selected items for multi-drag
+			getItemId: (element) => element.dataset.uploadId,
 			getSelectedItems: (element) => {
 				const fieldId = this.getFieldIdFromElement(element);
 				const uploadId = element.dataset.uploadId;
 				const selected = this.getCurrentSelection(fieldId);
-
-				if (selected && selected.includes(uploadId)) {
-					return selected;
-				}
-
-				return [uploadId];
+				return (selected && selected.includes(uploadId)) ? selected : [uploadId];
 			},
-
-			// Validate drop location
 			validateDrop: (itemIds, targetElement) => {
 				const targetFieldId = this.getFieldIdFromElement(targetElement);
 				const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
 				const itemFieldId = this.getFieldIdFromElement(itemElement);
-
 				return targetFieldId === itemFieldId;
 			},
-
-			// Handle successful drop
 			onDrop: (itemIds, targetElement) => {
 				this.handleItemDrop(itemIds, targetElement);
 				targetElement.scrollIntoView({behavior:'smooth', block:'center'});
 			},
-
-			// Optional callbacks
-			onDragStart: (itemIds) => {
-			},
-
 			onDragEnd: (itemIds, success) => {
 				if (success) {
-					// Clear selection after successful move
 					const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
 					const fieldId = this.getFieldIdFromElement(itemElement);
 					const handler = this.selectionHandlers.get(fieldId);
 					handler?.clearSelection();
 				}
 			},
-
-			// Preview options
 			previewOptions: {
 				multiOffset: { x: -60, y: -80 },
 				singleOffset: { x: -50, y: -60 },
@@ -373,93 +397,38 @@
 		});
 	}
 
-	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)
+	 * FILE DROP HANDLERS
 	 *******************************************************************************/
 
+	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);
+	}
+
 	handleExternalDragLeave(e) {
 		const dropZone = e.target.closest(this.selectors.field.dropZone);
 		if (dropZone && !dropZone.contains(e.relatedTarget)) {
 			dropZone.classList.remove('dragover');
 		}
 	}
+
 	handleExternalDragEnter(e) {
-		if (!e.dataTransfer.types.includes('Files')) {
-			return;
-		}
-
+		if (!e.dataTransfer.types.includes('Files')) return;
 		const dropZone = e.target.closest(this.selectors.field.dropZone);
-
 		if (dropZone) {
 			e.preventDefault();
 			dropZone.classList.add('dragover');
@@ -468,7 +437,6 @@
 
 	handleExternalDragOver(e) {
 		if (!e.dataTransfer.types.includes('Files')) return;
-
 		const dropZone = e.target.closest(this.selectors.field.dropZone);
 		if (dropZone) {
 			e.preventDefault();
@@ -478,33 +446,25 @@
 
 	handleExternalDrop(e) {
 		const dropZone = e.target.closest(this.selectors.field.dropZone);
-
 		if (!dropZone) return;
 
 		e.preventDefault();
 		dropZone.classList.remove('dragover');
 
 		const files = Array.from(e.dataTransfer.files);
-
 		if (files.length === 0) return;
 
 		const fieldId = this.getFieldIdFromElement(dropZone);
-
 		if (fieldId) {
 			this.processFiles(fieldId, files);
 			this.a11y.announce(`${files.length} file(s) dropped for upload`);
-		} else {
-			console.error('No field ID found for drop zone');
 		}
 	}
 
 	/*******************************************************************************
-	 * ITEM DROP HANDLER (for rearranging existing uploads)
+	 * ITEM DROP HANDLER (for rearranging)
 	 *******************************************************************************/
 
-	/**
-	 * Handle items being dropped (called by DragController)
-	 */
 	handleItemDrop(itemIds, targetElement) {
 		const isPreviewDrop = targetElement.classList.contains('preview');
 		let actualTarget = targetElement;
@@ -513,43 +473,32 @@
 		if (targetElement.classList.contains('empty-group')) {
 			const fieldId = this.getFieldIdFromElement(targetElement);
 			const group = this.createGroup(fieldId);
-
-			if (!group) {
-				console.error('Failed to create group');
-				return;
-			}
-
+			if (!group) return;
 			actualTarget = group.grid;
 		}
 
-		// Move each item to target
+		// Move each item
 		itemIds.forEach(uploadId => {
 			if (isPreviewDrop) {
-				// Moving back to preview (ungrouping)
 				this.removeFromGroup(uploadId);
 			} else {
-				// Moving to a group
 				this.addToGroup(uploadId, actualTarget);
 			}
 		});
 
-		// Persist state
 		const fieldId = this.getFieldIdFromElement(targetElement);
 		this.schedulePersistance(fieldId);
 
-		// Announce for accessibility
-		const message = itemIds.length > 1
-			? `Moved ${itemIds.length} items`
-			: 'Moved item';
+		const message = itemIds.length > 1 ? `Moved ${itemIds.length} items` : 'Moved item';
 		this.a11y.announce(message);
 	}
 
 	/*******************************************************************************
-	 * CLICK HANDLERS
+	 * CLICK & CHANGE HANDLERS
 	 *******************************************************************************/
 
 	handleClick(e) {
-		// File input triggers
+		// Trigger file input
 		if (e.target.matches(this.selectors.field.dropZone) ||
 			e.target.closest(this.selectors.field.dropZone)) {
 			const dropZone = e.target.closest(this.selectors.field.dropZone);
@@ -568,11 +517,10 @@
 
 	handleChange(e) {
 		const fieldId = this.getFieldIdFromElement(e.target);
+
 		// File input change
 		if (e.target.matches(this.selectors.field.input)) {
-			const fieldId = this.getFieldIdFromElement(e.target);
 			const files = Array.from(e.target.files);
-
 			if (files.length > 0 && fieldId) {
 				this.processFiles(fieldId, files);
 			}
@@ -580,7 +528,8 @@
 
 		// Meta field changes
 		if (fieldId) {
-			if (this.fields.get(fieldId).config.destination === 'post_group') {
+			const fieldData = this.getFieldData(fieldId);
+			if (fieldData?.config.destination === 'post_group') {
 				this.handleGroupMetaChange(e.target);
 			} else {
 				this.queueUploadMeta(e);
@@ -588,363 +537,39 @@
 		}
 	}
 
-	/********************************************************************************
-	 UTILITY
-	********************************************************************************/
-	getCurrentSelection(fieldId) {
-		let selected = [];
-		for (let [key, handler] of this.selectionHandlers) {
-			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
-				selected = selected.concat([... handler.selectedItems]);
-			}
-		}
-		return selected;
-	}
-
-	getSubtypeFromMime(mimeType) {
-		if (mimeType.startsWith('image/')) return 'image';
-		if (mimeType.startsWith('video/')) return 'video';
-		return 'document';
-	}
-
-	getStatusText(status) {
-		return this.statusMapping[status] || status;
-	}
-
-	getStatusIcon(status) {
-		return window.getIcon(this.queue.icons[status]);
-	}
-	getStatusProgress(status) {
-		switch (status) {
-			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(field) {
-		// Return cached value if available
-		if (field._cachedModalType !== undefined) {
-			return field._cachedModalType;
-		}
-
-		// Safety check for field.element
-		if (!field || !field.element) {
-			field._cachedModalType = null;
-			return null;
-		}
-
-		const dialog = field.element.closest('dialog');
-		if (!dialog) {
-			field._cachedModalType = null;
-			return null;
-		}
-
-		let modalType = null;
-		if (dialog.classList.contains('edit')) modalType = 'edit';
-		else if (dialog.classList.contains('create')) modalType = 'create';
-		else if (dialog.classList.contains('bulkEdit')) modalType = 'bulkEdit';
-		else modalType = dialog.className;
-
-		// Cache the result
-		field._cachedModalType = modalType;
-		return modalType;
-	}
-	/*******************************************************************************
-	 * GROUP ACTIONS
-	 *******************************************************************************/
-
-	handleAction(button) {
-		const action = button.dataset.action;
-		const fieldId = this.getFieldIdFromElement(button);
-		switch(action) {
-			case 'add-to-group':
-				this.handleAddToGroup(button);
-				break;
-			case 'delete-group':
-				this.handleDeleteGroup(button);
-				break;
-			case 'delete-upload':
-			case 'remove-from-group':
-				this.handleRemoveItem(button);
-				break;
-			case 'upload':
-				//upload groups
-				let field = this.fields.get(fieldId);
-				field.element.closest('details').open = false;
-				document.body.classList.add('uploading');
-
-				this.submitUploads(fieldId);
-				break;
-			case 'restore':
-				this.handleRestoreUploads().then(()=>{});
-				break;
-			case 'clear-cache':
-				if (!confirm(`Save these uploads for later?`)) {
-					this.cleanupStoredUploads();
-				}
-				this.cleanupRestore();
-				break;
-		}
-	}
-
-	handleAddToGroup(button) {
-		const fieldElement = button.closest(this.selectors.field.field);
-		const fieldId = fieldElement?.dataset.uploader;
-
-		if (!fieldId) return;
-
-		const selected = this.selected.get(fieldId);
-
-		if (!selected || selected.size === 0) {
-			// Create empty group
-			this.createGroup(fieldId);
-		} else {
-			// Create group with selected items
-			const group = this.createGroup(fieldId);
-			if (!group) return;
-
-			selected.forEach(uploadId => {
-				this.addToGroup(uploadId, group.grid);
-			});
-
-			// Clear selection
-			const handler = this.selectionHandlers.get(fieldId);
-			handler?.clearSelection();
-
-			this.a11y.announce(`Created group with ${selected.size} items`);
-		}
-
-		this.schedulePersistance(fieldId);
-	}
-
-	handleDeleteGroup(button) {
-		const group = button.closest(this.selectors.groups.container);
-		if (!group) return;
-
-		const groupId = group.dataset.groupId;
-		const fieldId = this.getFieldIdFromElement(group);
-
-		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
-			return;
-		}
-
-		// Move items back to preview
-		const items = group.querySelectorAll(this.selectors.items.item);
-		items.forEach(item => {
-			const uploadId = item.dataset.uploadId;
-			this.removeFromGroup(uploadId);
-		});
-
-		// Remove group
-		this.deleteGroup(groupId);
-
-		this.a11y.announce('Group deleted, items returned to upload area');
-		this.schedulePersistance(fieldId);
-	}
-
-	handleRemoveItem(button) {
-		const item = button.closest(this.selectors.items.item);
-		if (!item) return;
-
-		const uploadId = item.dataset.uploadId;
-		const fieldId = this.getFieldIdFromElement(item);
-
-		if (!confirm('Remove this item?')) {
-			return;
-		}
-
-		this.removeUpload(fieldId, uploadId);
-		this.a11y.announce('Item removed');
-		this.schedulePersistance(fieldId);
-	}
-
-	/*******************************************************************************
-	 * SELECTION MANAGEMENT
-	 *******************************************************************************/
-
-	/**
-	 * Add selection handler for a field
-	 */
-	addFieldSelectionHandler(fieldId) {
-		if (this.selectionHandlers.has(fieldId)) {
-			return this.selectionHandlers.get(fieldId);
-		}
-
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const container = field.ui.field;
-		if (!container) return;
-
-		const handler = new window.jvbHandleSelection({
-			container: container,
-			ui: {
-				selectAll: container.querySelector('[name="select-all-uploads"]'),
-				bulkControls: container.querySelector('.selection-actions'),
-				count: container.querySelector('.selection-count')
-			},
-			itemSelector: '[data-upload-id]',
-			checkboxSelector: '[name*="select-item"]'
-		});
-
-		// Subscribe to selection changes
-		handler.subscribe((event, data) => {
-			switch(event) {
-				case 'item-selected':
-				case 'item-deselected':
-				case 'range-selected':
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'select-all':
-					this.handleSelectAll(data.container, data.selected);
-					break;
-			}
-		});
-
-		this.selectionHandlers.set(fieldId, handler);
-		return handler;
-	}
-
-	/**
-	 * Add selection handler for a group
-	 */
-	addGroupSelectionHandler(fieldId, groupId) {
-		const handlerKey = `${fieldId}_${groupId}`;
-
-		if (this.selectionHandlers.has(handlerKey)) {
-			return this.selectionHandlers.get(handlerKey);
-		}
-
-		const group = this.groups.get(groupId);
-		if (!group) return;
-
-		const handler = new window.jvbHandleSelection({
-			container: group.element,
-			ui: {
-				selectAll: group.element.querySelector(this.selectors.groups.selectAll),
-				bulkControls: group.element.querySelector(this.selectors.groups.actions),
-				count: group.element.querySelector(this.selectors.groups.count)
-			},
-			itemSelector: '[data-upload-id]',
-			checkboxSelector: '[name*="select-item"]'
-		});
-
-		handler.subscribe((event, data) => {
-			switch(event) {
-				case 'item-selected':
-				case 'item-deselected':
-				case 'range-selected':
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'select-all':
-					this.handleSelectAll(data.container, data.selected);
-					break;
-			}
-		});
-
-		this.selectionHandlers.set(handlerKey, handler);
-		return handler;
-	}
-
-	handleSelectAll(container, selected) {
-	}
-
-	/*******************************************************************************
-	 * HELPER METHODS
-	 *******************************************************************************/
-
-	determineFieldId(fieldElement) {
-		const content = fieldElement.dataset.content ||
-			fieldElement.closest('dialog')?.dataset.content ||
-			fieldElement.closest('form')?.dataset.save || '';
-		const itemID = fieldElement.dataset.itemId ||
-			fieldElement.closest('dialog')?.dataset.itemId || '';
-		const field = fieldElement.dataset.field || '';
-
-		return `${content}_${itemID}_${field}`;
-	}
-
-	getFromElement(element, type) {
-		const map = {
-			'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 }
-		};
-
-		const config = map[type];
-		if (!config) return null;
-
-		const el = element.closest(config.selector);
-		if (!el) return null;
-
-		const id = el.dataset[config.key];
-		return config.store.get(id);
-	}
-	getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
-	getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
-	getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
-
-	getFieldIdFromElement(el) { return this.getFromElement(el, 'field')?.id ?? null};
-	getUploadIdFromElement(el) {return this.getFromElement(el, 'upload')?.id ?? null};
-	getGroupIdFromElement(el) {return this.getFromElement(el, 'group')?.id ?? null};
-
-
 	/*******************************************************************************
 	 * FILE PROCESSING
 	 *******************************************************************************/
-	async processFiles(fieldId, files) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
 
-		// Hide upload container, show group display
-		if (field.ui.dropZone) {
-			field.ui.dropZone.hidden = true;
+	async processFiles(fieldId, files) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Show group display, hide upload zone
+		if (fieldEl.ui.dropZone) {
+			fieldEl.ui.dropZone.hidden = true;
 		}
-		if (field.ui.groups.display) {
-			field.ui.groups.display.hidden = false;
+		if (fieldEl.ui.groups?.display) {
+			fieldEl.ui.groups.display.hidden = false;
 		}
 
 		const totalFiles = files.length;
 		let processedCount = 0;
 
-		// Show initial progress
 		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
 
-		// Initialize field uploads set if needed
-		if (!field.uploads) {
-			field.uploads = new Set();
-		}
-
-		// Process files
-		const processPromises = Array.from(files).map(async (file, index) => {
+		const processPromises = Array.from(files).map(async (file) => {
 			try {
-
-				// Create upload ID
 				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 
-				// Create upload data
+				// Create upload data (without blob initially)
 				const uploadData = {
 					id: uploadId,
-					attachment_id: null,
+					attachmentId: null,
 					fieldId: fieldId,
-					originalFile: file,
-					processedFile: null,
-					preview: null,
 					status: 'local_processing',
-					element: null,
-					location: null,
+					groupId: null,
 					meta: {
 						originalName: file.name,
 						size: file.size,
@@ -952,50 +577,55 @@
 					}
 				};
 
-				// Create preview URL
-				uploadData.preview = this.createPreviewUrl(file);
+				// Process file
+				const preview = this.createPreviewUrl(file);
+				const processedFile = file.type.startsWith('image/')
+					? await this.processImage(file, fieldData.config.subtype)
+					: file;
 
-				// Process the file (resize if image)
-				if (file.type.startsWith('image/')) {
-					uploadData.processedFile = await this.processImage(file, field.subtype);
-				} else {
-					uploadData.processedFile = file;
-				}
-
-				// Store blob data separately in IndexedDB
-				await this.uploadStore.saveBlob(uploadId, uploadData.processedFile || file);
+				// Store blob data as ArrayBuffer
+				await this.saveBlobData(uploadId, processedFile || file);
 
 				// Create DOM element
 				const subtype = this.getSubtypeFromMime(file.type);
-				uploadData.element = this.createUploadElement({
-					...uploadData,
+				const element = this.createUploadElement({
+					id: uploadId,
+					preview: preview,
+					meta: uploadData.meta,
 					subtype: subtype
-				}, field.config.destination === 'post_group');
+				}, fieldData.config.destination === 'post_group');
 
-				// Show progress on the item
+				// Show progress
 				this.showUploadProgress(uploadId, true);
 				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
 
 				// Add to preview grid
-				if (field.ui.preview) {
-					field.ui.preview.appendChild(uploadData.element);
-					uploadData.location = field.ui.preview;
+				if (fieldEl.ui.preview) {
+					fieldEl.ui.preview.appendChild(element);
+
+					// Store runtime element data
+					this.uploadElements.set(uploadId, {
+						element: element,
+						preview: preview,
+						location: fieldEl.ui.preview
+					});
 				}
 
-				// Store upload
-				this.uploads.set(uploadId, uploadData);
-				field.uploads.add(uploadId);
+				// Store persistent data
+				uploadData.status = 'processed';
+				await this.uploadStore.save(uploadData);
+
+				// Add to field
+				fieldData.uploads.add(uploadId);
+				await this.saveFieldData(fieldData);
 
 				// Update progress
 				processedCount++;
 				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
 				this.updateUploadItemProgress(uploadId, 100, 'processed');
-				uploadData.status = 'processed';
 
-				// Fade out item progress after a moment
-				setTimeout(() => {
-					this.showUploadProgress(uploadId, false);
-				}, 1000);
+				// Fade out progress
+				setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
 
 				return uploadId;
 
@@ -1007,192 +637,21 @@
 			}
 		});
 
-		// Wait for all files to process
 		await Promise.all(processPromises);
 
 		this.updateFieldState(fieldId);
-		// Cache the state (now without DOM references)
-		await this.schedulePersistance(fieldId);
+		this.refreshSortable(fieldId);
 
 		// Queue for upload if in direct mode
-		if (field.config.destination !== 'post_group') {
+		if (fieldData.config.destination !== 'post_group') {
 			await this.queueUpload(fieldId);
-			// Lock uploads if max reached
 			this.maybeLockUploads(fieldId);
 		}
-
 	}
 
-	updateFieldState(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.ui.field) return;
-
-		const container = field.ui.field;
-		const uploadCount = field.uploads?.size || 0;
-		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
-
-		// Set data attributes for CSS targeting
-		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
-		container.dataset.uploadCount = uploadCount.toString();
-		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
-
-		// Update ARIA labels for accessibility
-		if (field.ui.preview) {
-			field.ui.preview.setAttribute('aria-label',
-				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
-			);
-		}
-	}
-
-	updateUploadProgress(fieldId, current, total, message) {
-		const field = this.fields.get(fieldId);
-		if (!field?.ui?.progress?.progress) return;
-
-		const progress = field.ui.progress;
-		const percent = total > 0 ? (current / total) * 100 : 0;
-
-		if (progress.fill) {
-			progress.fill.style.width = `${percent}%`;
-		}
-		if (progress.text) {
-			progress.text.textContent = message;
-		}
-		if (progress.count) {
-			progress.count.textContent = `${current}/${total}`;
-		}
-
-		progress.progress.hidden = (current === total);
-	}
-
-	updateFieldStatus(fieldId, status) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		field.state = status;
-		// Update UI based on status
-	}
-
-	updateUploadStatus(uploadId, status) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		upload.status = status;
-		this.updateUploadUI(uploadId);
-	}
-
-	updateUploadUI(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload?.element) return;
-
-		// Update status classes
-		upload.element.className = upload.element.className.replace(/status-[\w-]+/g, '');
-		upload.element.classList.add(`status-${upload.status}`);
-
-		// Update progress if showing
-		const progress = upload.element.querySelector('.progress');
-		if (progress) {
-			this.updateUploadItemProgress(uploadId,
-				this.getStatusProgress(upload.status),
-				upload.status
-			);
-		}
-	}
-
-	/**
-	 * Show/hide progress indicator on individual upload items
-	 */
-	showUploadProgress(uploadId, show = true) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (progressEl) {
-			if (show) {
-				progressEl.style.removeProperty('animation');
-				progressEl.hidden = false;
-			} else {
-				progressEl.style.animation = 'fadeOut var(--transition-base)';
-				setTimeout(() => {
-					progressEl.hidden = true;
-				}, 300);
-			}
-		}
-	}
-
-	/**
-	 * Update individual upload progress bar
-	 */
-	updateUploadItemProgress(uploadId, percent, status = null) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (!progressEl) return;
-
-		const fill = progressEl.querySelector('.fill');
-		const details = progressEl.querySelector('.details');
-		const icon = progressEl.querySelector('.icon');
-
-		if (fill) {
-			fill.style.width = `${percent}%`;
-		}
-
-		if (status && details) {
-			details.textContent = this.getStatusText(status);
-		}
-
-		if (status && icon) {
-			icon.innerHTML = this.getStatusIcon(status).outerHTML;
-		}
-	}
-	checkFieldLimits(fieldId, additionalFiles) {
-		const field = this.fields.get(fieldId);
-		if (!field) return false;
-
-		const currentCount = field.uploads?.size || 0;
-		const totalCount = currentCount + additionalFiles;
-
-		return totalCount <= field.maxFiles;
-
-
-	}
-	validateFile(file, field) {
-		// Type validation
-		if (!this.settings.allowedTypes.includes(file.type)) {
-			this.notify(`Invalid file type: ${file.type}`, 'error');
-			return false;
-		}
-
-		// Size validation
-		if (file.size > this.settings.maxFileSize) {
-			this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
-			return false;
-		}
-
-		return true;
-	}
-
-	formatBytes(bytes, decimals = 2) {
-		if (bytes === 0) return '0 Bytes';
-
-		const k = 1024;
-		const dm = decimals < 0 ? 0 : decimals;
-		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-
-		const i = Math.floor(Math.log(bytes) / Math.log(k));
-
-		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
-	}
-
-	shouldProcessClientSide(file, subtype) {
-		// Only process images client-side
-		if (subtype === 'image' && file.type.startsWith('image/')) {
-			return true;
-		}
-
-		// Videos and documents go straight to server
-		return false;
-	}
+	/*******************************************************************************
+	 * IMAGE PROCESSING (simplified - keeping existing logic)
+	 *******************************************************************************/
 
 	async processImage(file, uploadId) {
 		const timeout = this.worker.settings.timeout;
@@ -1201,27 +660,19 @@
 			let timeoutId;
 			let taskCompleted = false;
 
-			// Set timeout
 			timeoutId = setTimeout(() => {
 				if (!taskCompleted) {
 					taskCompleted = true;
-
-					// Remove from active tasks
 					this.worker.tasks.delete(uploadId);
-
-					// Maybe restart worker if configured
 					if (this.worker.settings.restartAfterTimeout) {
 						this.restartCompressionWorker();
 					}
-
 					reject(new Error(`Processing timeout for ${file.name}`));
 				}
 			}, timeout);
 
-			// Track this task
 			this.worker.tasks.set(uploadId, { file, timeoutId });
 
-			// Process image
 			this.handleProcess(file, uploadId)
 				.then(result => {
 					if (!taskCompleted) {
@@ -1243,7 +694,6 @@
 	}
 
 	async handleProcess(file, uploadId) {
-		// Skip non-images
 		if (!file.type.startsWith('image/')) {
 			return file;
 		}
@@ -1251,14 +701,11 @@
 		const maxDimension = this.getMaxDimension();
 		const quality = 0.85;
 
-		// Try worker first if available
 		if (this.shouldUseWorker(file)) {
 			try {
-				// Ensure worker is initialized
 				if (!this.worker.worker) {
 					this.initCompressionWorker();
 				}
-
 				if (this.worker.worker) {
 					return await this.processWithWorker(file, uploadId, maxDimension, quality);
 				}
@@ -1267,13 +714,9 @@
 			}
 		}
 
-		// Fallback to main thread
 		return await this.processOnMainThread(file, maxDimension, quality);
 	}
 
-	/**
-	 * Process image on main thread with better error handling
-	 */
 	async processOnMainThread(file, maxDimension, quality) {
 		return new Promise((resolve, reject) => {
 			const img = new Image();
@@ -1288,7 +731,6 @@
 					URL.revokeObjectURL(objectUrl);
 					objectUrl = null;
 				}
-				// Explicitly clean up canvas
 				canvas.width = 1;
 				canvas.height = 1;
 				ctx.clearRect(0, 0, 1, 1);
@@ -1300,7 +742,6 @@
 					canvas.width = width;
 					canvas.height = height;
 
-					// Enhanced image smoothing
 					ctx.imageSmoothingEnabled = true;
 					ctx.imageSmoothingQuality = 'high';
 					ctx.drawImage(img, 0, 0, width, height);
@@ -1347,67 +788,41 @@
 		});
 	}
 
-	/**
-	 * Get optimal output format
-	 */
 	getOptimalFormat(file) {
-		// Keep original format for certain types
 		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
 			return file.type;
 		}
-
-		// Use WebP if supported, otherwise JPEG
 		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
 	}
 
-	/**
-	 * Get optimal quality setting
-	 */
 	getOptimalQuality(file, requestedQuality) {
-		// Higher quality for smaller files
 		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
 		if (file.size < 2 * 1024 * 1024) return requestedQuality;
-
-		// Lower quality for very large files
 		return Math.min(requestedQuality, 0.8);
 	}
 
-	/**
-	 * Generate processed file name
-	 */
 	getProcessedFileName(originalFile, outputFormat) {
 		const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
-
 		const extensions = {
 			'image/webp': '.webp',
 			'image/jpeg': '.jpg',
 			'image/png': '.png',
 			'image/gif': '.gif'
 		};
-
 		return baseName + (extensions[outputFormat] || '.jpg');
 	}
 
-	/**
-	 * Get maximum dimension based on device capabilities
-	 */
 	getMaxDimension() {
 		const screenWidth = window.screen.width;
 		const devicePixelRatio = window.devicePixelRatio || 1;
-
-		// Scale based on device capabilities
 		if (screenWidth * devicePixelRatio > 2560) return 2400;
 		if (screenWidth * devicePixelRatio > 1920) return 1920;
 		return 1200;
 	}
 
-	/**
-	 * Determine if we should use Web Worker
-	 */
 	shouldUseWorker(file) {
-		// Use worker for large files or when available
 		return this.worker.worker &&
-			file.size > 1024 * 1024 && // > 1MB
+			file.size > 1024 * 1024 &&
 			typeof OffscreenCanvas !== 'undefined';
 	}
 
@@ -1418,14 +833,11 @@
 				return;
 			}
 
-			// Create unique message ID for this task
 			const messageId = `${uploadId}_${Date.now()}`;
 
-			// Handler for this specific message
 			const messageHandler = (e) => {
 				if (e.data.messageId !== messageId) return;
 
-				// Remove handler
 				this.worker.worker.removeEventListener('message', messageHandler);
 				this.worker.worker.removeEventListener('error', errorHandler);
 
@@ -1447,11 +859,9 @@
 				reject(new Error(`Worker error: ${error.message}`));
 			};
 
-			// Add handlers
 			this.worker.worker.addEventListener('message', messageHandler);
 			this.worker.worker.addEventListener('error', errorHandler);
 
-			// Send message to worker
 			this.worker.worker.postMessage({
 				messageId,
 				file,
@@ -1462,85 +872,45 @@
 		});
 	}
 
-	/**
-	 * Restart compression worker
-	 */
 	restartCompressionWorker() {
-		// Terminate existing worker
 		if (this.worker.worker) {
 			this.worker.worker.terminate();
 			this.worker.worker = null;
 		}
-
-		// Clear active tasks
 		this.worker.tasks.clear();
-
-		// Check restart limit
 		if (this.worker.restart.count >= this.worker.restart.max) {
 			console.error('Max worker restarts reached, disabling worker');
 			return;
 		}
-
 		this.worker.restart.count++;
-
-		// Reinitialize
 		this.initCompressionWorker();
 	}
 
-	/**
-	 * Initialize Web Worker for image compression
-	 */
 	initCompressionWorker() {
 		if (this.worker.worker || typeof Worker === 'undefined') return;
 
 		try {
 			const workerScript = `
-            self.onmessage = async function(e) {
-                const { messageId, file, maxDimension, quality, outputFormat } = e.data;
-
-                try {
-                    // Create ImageBitmap from file
-                    const bitmap = await createImageBitmap(file);
-
-                    // Calculate dimensions
-                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
-                    const width = Math.round(bitmap.width * scale);
-                    const height = Math.round(bitmap.height * scale);
-
-                    // Create OffscreenCanvas
-                    const canvas = new OffscreenCanvas(width, height);
-                    const ctx = canvas.getContext('2d');
-
-                    // Draw and resize
-                    ctx.imageSmoothingEnabled = true;
-                    ctx.imageSmoothingQuality = 'high';
-                    ctx.drawImage(bitmap, 0, 0, width, height);
-
-                    // Clean up bitmap
-                    bitmap.close();
-
-                    // Convert to blob
-                    const blob = await canvas.convertToBlob({
-                        type: outputFormat,
-                        quality: quality
-                    });
-
-                    self.postMessage({
-                        messageId,
-                        success: true,
-                        blob: blob,
-                        format: outputFormat
-                    });
-
-                } catch (error) {
-                    self.postMessage({
-                        messageId,
-                        success: false,
-                        error: error.message
-                    });
-                }
-            };
-        `;
+				self.onmessage = async function(e) {
+					const { messageId, file, maxDimension, quality, outputFormat } = e.data;
+					try {
+						const bitmap = await createImageBitmap(file);
+						const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
+						const width = Math.round(bitmap.width * scale);
+						const height = Math.round(bitmap.height * scale);
+						const canvas = new OffscreenCanvas(width, height);
+						const ctx = canvas.getContext('2d');
+						ctx.imageSmoothingEnabled = true;
+						ctx.imageSmoothingQuality = 'high';
+						ctx.drawImage(bitmap, 0, 0, width, height);
+						bitmap.close();
+						const blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });
+						self.postMessage({ messageId, success: true, blob: blob, format: outputFormat });
+					} catch (error) {
+						self.postMessage({ messageId, success: false, error: error.message });
+					}
+				};
+			`;
 
 			const blob = new Blob([workerScript], { type: 'application/javascript' });
 			this.worker.worker = new Worker(this.createPreviewUrl(blob));
@@ -1551,30 +921,18 @@
 		}
 	}
 
-	/**
-	 * Calculate optimal dimensions with aspect ratio preservation
-	 */
 	calculateOptimalDimensions(img, maxDimension) {
 		let { width, height } = img;
-
-		// Don't upscale
 		if (width <= maxDimension && height <= maxDimension) {
 			return { width, height };
 		}
-
-		// Calculate scale factor
 		const scale = Math.min(maxDimension / width, maxDimension / height);
-
 		return {
 			width: Math.round(width * scale),
 			height: Math.round(height * scale)
 		};
 	}
 
-
-	/**
-	 * Check WebP support
-	 */
 	supportsWebP() {
 		const canvas = document.createElement('canvas');
 		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
@@ -1582,7 +940,6 @@
 
 	createPreviewUrl(file) {
 		const url = URL.createObjectURL(file);
-		// Track for cleanup
 		if (!this.previewUrls) this.previewUrls = new Set();
 		this.previewUrls.add(url);
 		return url;
@@ -1595,131 +952,19 @@
 		}
 	}
 
-	maybeLockUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field?.ui?.dropZone) return;
-
-		if (field.config.destination === 'post_group') {
-			return;
-		}
-
-		const uploadCount = field.uploads?.size || 0;
-		const maxFiles = field.config?.maxFiles || 999;
-
-		// Hide dropzone if at max files
-		field.ui.dropZone.hidden = uploadCount >= maxFiles;
-
-		// Update field state
-		field.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
-	}
-	createUploadElement(upload, draggable = false) {
-		let image = window.getTemplate('uploadItem');
-		if (!image) {
-			console.error('Image template not found');
-			return;
-		}
-		image.dataset.uploadId = upload.id;
-		if (upload.originalFile) {
-			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
-		}
-
-
-		image.querySelector('[name="featured"]').value = upload.id;
-		let [
-			featured,
-			img,
-			video,
-			preview,
-			details
-		] = [
-			image.querySelector('[name="featured"]'),
-			image.querySelector('img'),
-			image.querySelector('video'),
-			image.querySelector('label > span'),
-			image.querySelector('details')
-		];
-		[
-			featured.value,
-			img.src,
-			img.alt
-		] = [
-			upload.id,
-			upload.preview,
-			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
-		];
-
-		switch (image.dataset.subtype) {
-			case 'image':
-				[
-					img.src,
-					img.alt
-				] = [
-					upload.preview,
-					upload.originalFile?.name ?? upload.meta?.originalName?? ''
-				];
-				video.remove();
-				preview.remove();
-				break;
-			case 'video':
-				video.src = upload.preview;
-				img.remove();
-				preview.remove();
-				break;
-			case 'document':
-				const fileName = upload.originalFile?.name ?? upload.meta?.originalName ?? '';
-				const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
-				const iconMap = {
-					'pdf': 'file-pdf',
-					'csv': 'file-csv',
-					'doc': 'file-doc',
-					'docx': 'file-doc',
-					'txt': 'file-txt',
-					'xls': 'file-xls',
-					'xlsx': 'file-xls'
-				};
-
-				const icon = window.getIcon(iconMap[extension] || 'file');
-
-				preview.innerText = upload.originalFile.name;
-				preview.prepend(icon);
-				img.remove();
-				video.remove();
-				break;
-		}
-		if (details) {
-			let template = window.getTemplate('uploadMeta');
-			if (template){
-				details.append(template);
-			}
-		}
-		image.draggable = draggable;
-
-		// Update input IDs safely
-		image.querySelectorAll('input').forEach(input => {
-			let id = input.id;
-			if (id) {
-				let newId = id + upload.id;
-				let label = input.parentNode.querySelector(`label[for="${id}"]`);
-				input.id = newId;
-				if (label) {
-					label.htmlFor = newId;
-				}
-			}
-		});
-
-		return image;
-	}
 	/*******************************************************************************
 	 * QUEUE INTEGRATION
 	 *******************************************************************************/
+
 	async submitUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads || field.uploads.size === 0) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) {
 			return;
 		}
 
-		let uploads = Array.from(field.uploads);
-		if (uploads.length === 0) {
+		let uploadIds = Array.from(fieldData.uploads);
+		if (uploadIds.length === 0) {
 			this.error.log('No uploads to upload', {
 				component: 'UploadManager',
 				action: 'submitGroupedUploads',
@@ -1744,108 +989,88 @@
 		const formData = new FormData();
 		let uploadMap = [];
 
-		uploads = uploads.map((upload) => {
-			return this.uploads.get(upload);
-		});
-
-		fieldGroups.forEach((group, groupIndex) => {
+		// Process each group
+		for (const group of fieldGroups) {
 			const post = {
 				images: [],
 				fields: {}
 			};
+
+			// Add group metadata
 			for (let [name, value] of Object.entries(group.changes)) {
 				post.fields[name] = value;
 			}
 
-			let groupUploads = uploads.filter((upload) => {
-				return upload['groupId'] === group.id;
+			// Get uploads for this group
+			const groupUploadIds = uploadIds.filter(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				return upload?.groupId === group.id;
 			});
 
-			groupUploads.forEach((upload) => {
-				if (upload) {
-					const fileToUpload = upload.processedFile || upload.originalFile;
-					if (fileToUpload) {
-						formData.append('files[]', fileToUpload);
-
-						const imageData = {
-							upload_id: upload.id,
-							index: uploadMap.length
-						};
-						post.images.push(imageData);
-						uploadMap.push(upload.id);
-					}
-				}
-			});
-			// Add images for this group
-			// group.uploads.forEach(uploadId => {
-			// 	const upload = this.uploads.get(uploadId);
-			// 	if (upload) {
-			// 		const fileToUpload = upload.processedFile || upload.originalFile;
-			// 		if (fileToUpload) {
-			// 			formData.append('files[]', fileToUpload);
-			//
-			// 			const imageData = {
-			// 				upload_id: upload.id,
-			// 				index: uploadMap.length
-			// 			};
-			//
-			// 			// Check if this is the featured image
-			// 			const radioInput = upload.element?.querySelector('[name="featured"]');
-			// 			if (radioInput?.checked) {
-			// 				post.fields.featured = upload.id;
-			// 			}
-			//
-			// 			post.images.push(imageData);
-			// 			uploadMap.push(upload.id);
-			// 		}
-			// 	}
-			// });
-
-			posts.push(post);
-		});
-
-		//Each remaining upload (without a groupId) becomes its own post
-		let remainingUploads = uploads.filter((upload) => {
-			return !Object.hasOwn(upload, 'groupId');
-		});
-
-		remainingUploads.forEach((upload) => {
-			if (upload) {
-
-				const post = {
-					images: [],
-					fields: {}
-				};
-				const fileToUpload = upload.processedFile || upload.originalFile;
-				if (fileToUpload) {
-					formData.append('files[]', fileToUpload);
+			// Add files for this group
+			for (const uploadId of groupUploadIds) {
+				const file = await this.getBlobData(uploadId);
+				if (file) {
+					formData.append('files[]', file);
 
 					const imageData = {
-						upload_id: upload.id,
+						upload_id: uploadId,
 						index: uploadMap.length
 					};
+
+					// Check if featured
+					const uploadEl = this.uploadElements.get(uploadId);
+					const radioInput = uploadEl?.element?.querySelector('[name="featured"]');
+					if (radioInput?.checked) {
+						post.fields.featured = uploadId;
+					}
+
 					post.images.push(imageData);
-					uploadMap.push(upload.id);
+					uploadMap.push(uploadId);
 				}
-				posts.push(post);
 			}
+
+			posts.push(post);
+		}
+
+		// Handle remaining uploads (without groupId) - each becomes its own post
+		const remainingUploadIds = uploadIds.filter(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			return !upload?.groupId;
 		});
 
+		for (const uploadId of remainingUploadIds) {
+			const post = {
+				images: [],
+				fields: {}
+			};
+
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				formData.append('files[]', file);
+
+				const imageData = {
+					upload_id: uploadId,
+					index: uploadMap.length
+				};
+				post.images.push(imageData);
+				uploadMap.push(uploadId);
+			}
+
+			posts.push(post);
+		}
 
 		// Add metadata to FormData
-		formData.append('content', field.config.content);
-		formData.append('user', field.config.itemID); // Assuming itemID is user ID
+		formData.append('content', fieldData.config.content);
+		formData.append('user', fieldData.config.itemID);
 		formData.append('posts', JSON.stringify(posts));
 		formData.append('upload_ids', JSON.stringify(uploadMap));
 
-		for (const [key, value] of formData.entries()) {
-			console.log(key, value);
-		}
 		const operation = {
 			endpoint: 'uploads/groups',
 			method: 'POST',
 			data: formData,
-			title: `Creating ${posts.length} ${field.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
+			title: `Creating ${posts.length} ${fieldData.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
 			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
 			canMerge: false,
 			headers: {
@@ -1857,15 +1082,20 @@
 		try {
 			const operationId = await this.queue.addToQueue(operation);
 
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
+			// Update upload statuses
+			uploadIds.forEach(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
 				if (upload) {
 					upload.operationId = operationId;
+					upload.status = 'queued';
+					this.uploadStore.save(upload);
 					this.updateUploadStatus(uploadId, 'queued');
 				}
 			});
 
-			field.operationId = operationId;
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
+
 			this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
 
 			return operationId;
@@ -1876,181 +1106,107 @@
 				fieldId: fieldId
 			});
 			throw error;
-		} finally {
-			this.schedulePersistance(field.id);
 		}
 	}
 
 	async queueUpload(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
 
-		const uploads = Array.from(field.uploads);
-		if (uploads.length === 0) {
-			return;
-		}
+		const uploads = Array.from(fieldData.uploads);
+		const data = this.prepareUploadData(fieldData, uploads);
 
-		const data = this.prepareUploadData(field, uploads);
 		this.a11y.announce('Queuing for upload');
-		let img = (uploads.length === 1) ? 'file' : 'files';
+
 		const operation = {
 			endpoint: 'uploads',
 			method: 'POST',
 			data: data,
-			title: `Uploading ${uploads.length} ${img} to server...`,
-			popup: `Uploading ${uploads.length} ${img}...`,
+			title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''} to server...`,
+			popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
 			canMerge: false,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			},
+			headers: { 'action_nonce': jvbSettings.dash },
 			append: '_upload'
-		}
+		};
+
 		try {
 			const operationId = await this.queue.addToQueue(operation);
 
+			// Update upload statuses
 			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (!upload) {
-					return;
+				const upload = this.uploadStore.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					upload.status = 'queued';
+					this.uploadStore.save(upload);
+					this.updateUploadStatus(uploadId, 'queued');
 				}
-				upload.operationId = operationId;
-				this.updateUploadStatus(uploadId, 'queued');
 			});
-			field.operationId = operationId;
+
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
 
 			return operationId;
 		} catch (error) {
 			throw error;
-		} finally {
-			this.schedulePersistance(field.id);
 		}
 	}
 
-	prepareUploadData(field, uploads) {
-
+	async prepareUploadData(fieldData, uploads) {
 		const formData = new FormData();
-		formData.append('content', field.config.content);
-		formData.append('mode', field.config.mode);
-		formData.append('field_name', field.config.name);
-		formData.append('fieldId', field.id);
-		formData.append('field_type', field.config.type);
-		formData.append('subtype', field.config.subtype);
-		formData.append('item_id', field.config.itemID);		//post, term, or user id
-		formData.append('destination', field.config.destination || 'meta'); //meta, post, post_group
+		formData.append('content', fieldData.config.content);
+		formData.append('mode', fieldData.config.mode);
+		formData.append('field_name', fieldData.config.name);
+		formData.append('fieldId', fieldData.id);
+		formData.append('field_type', fieldData.config.type);
+		formData.append('subtype', fieldData.config.subtype);
+		formData.append('item_id', fieldData.config.itemID);
+		formData.append('destination', fieldData.config.destination || 'meta');
+
 		let uploadMap = [];
 
-		const fieldGroups = this.getFieldGroups(field.id);
-		if (field.config.destination === 'post_group' && fieldGroups.length > 0) {
-			// User has created groups
-			let groups = [];
-			let titles = [];
-			let featuredImages = [];
 
-			fieldGroups.forEach(group => {
-				let groupUploadIndices = [];
-				let featuredIndex = null;
+		const blobPromises = uploads.map(async (uploadId) => {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) return;
 
-				group.uploads.forEach(uploadId => {
-					let upload = this.uploads.get(uploadId);
-					if (upload) {
-						const fileToUpload = upload.processedFile || upload.originalFile;
-						if (fileToUpload) {
-							formData.append('files[]', fileToUpload);
-							const fileIndex = uploadMap.length;
-							uploadMap.push(upload.id);
-							groupUploadIndices.push(upload.id);
-
-							// Check if this is the featured image
-							const radioInput = upload.element?.querySelector('[name="featured"]');
-							if (radioInput?.checked) {
-								featuredIndex = upload.id;
-							}
-						}
-					}
-				});
-
-				groups.push(groupUploadIndices);
-				titles.push(group.title || '');
-				featuredImages.push(featuredIndex);
-			});
-
-			formData.append('groups', JSON.stringify(groups));
-			formData.append('group_titles', JSON.stringify(titles));
-			formData.append('featured_images', JSON.stringify(featuredImages));
-		} else {
-			// No groups - just append all files
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (upload) {
-					const fileToUpload = upload.processedFile || upload.originalFile;
-					if (fileToUpload) {
-						formData.append('files[]', fileToUpload);
-						uploadMap.push(upload.id);
-					}
-				}
-			});
-		}
-		formData.append('upload_ids', JSON.stringify(uploadMap));
-
-		// console.log('Final FormData:');
-		// for (let pair of formData.entries()) {
-		// 	console.log(pair[0], pair[1]);
-		// }
-
-		return formData;
-	}
-
-	getFieldGroups(fieldId) {
-		const groups = [];
-
-		this.groups.forEach((groupData, groupId) => {
-			if (groupData.fieldId === fieldId) {
-				const field = this.fields.get(fieldId);
-				const groupElement = field?.ui?.groups?.groups?.get(groupId);
-
-				groups.push({
-					id: groupId,
-					uploads: Array.from(groupData.uploads || new Set()),
-					changes: groupData.changes || {},
-					element: groupElement || null
-				});
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				formData.append('files[]', file);
+				uploadMap.push(upload.id);
 			}
 		});
 
-		return groups;
+		await Promise.all(blobPromises);
+
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+		return formData;
 	}
 
 	async queueUploadMeta(e) {
-		const upload = this.getUploadFromElement(e.target);
+		const uploadId = this.getUploadIdFromElement(e.target);
+		const upload = this.uploadStore.get(uploadId);
 		if (!upload) return;
 
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		const container = e.target.closest('.upload-meta');
-		if (!container) return;
+		const fieldData = this.getFieldData(upload.fieldId);
+		if (!fieldData) return;
 
 		let data = {};
 		data[e.target.name] = e.target.value;
 
-		upload.meta = {
-			...upload.meta,
-			... data
-		};
+		upload.meta = { ...upload.meta, ...data };
+		await this.uploadStore.save(upload);
 
 		let queueData = {};
-		//If there is an attachment ID, use that: else, use our generated upload id
-		queueData[upload.attachmentId??upload.id] = upload.meta;
+		queueData[upload.attachmentId ?? upload.id] = upload.meta;
 
 		const operation = {
-			endpoint:	'uploads/meta',
-			method:		'POST',
-			data:		queueData,
-			title:		`Updating meta`,
-			canMerge:	true,
-			headers:	{
-				'action_nonce': jvbSettings.dash
-			}
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			data: queueData,
+			title: 'Updating meta',
+			canMerge: true,
+			headers: { 'action_nonce': jvbSettings.dash }
 		};
 
 		try {
@@ -2063,441 +1219,109 @@
 			});
 		}
 	}
+
 	/*******************************************************************************
-	 * GROUP MANAGEMENT
+	 * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
 	 *******************************************************************************/
 
-	createGroup(fieldKey, groupId = null) {
-		const field = this.fields.get(fieldKey);
-		if (!field) {
-			console.error('Field not found:', fieldKey);
-			return null;
-		}
+	/**
+	 * Handle successful operation completion - CLEAR STORES
+	 */
+	async handleOperationComplete(operation, fieldId) {
+		const results = operation.result?.data || operation.serverData?.data || [];
 
-		if (!groupId) {
-			groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-		}
-
-		const groupElement = this.createGroupElement(groupId, fieldKey);
-		if (!groupElement) {
-			console.error('Failed to create group element');
-			return null;
-		}
-
-		// Store in field UI Map
-		if (!field.ui.groups) {
-			field.ui.groups = {
-				groups: new Map(),
-				container: null,
-				empty: null,
-				display: null
-			};
-		}
-
-		field.ui.groups.groups.set(groupId, groupElement);
-
-		// Insert into DOM
-		if (field.ui.groups.container && field.ui.groups.empty) {
-			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-		} else if (field.ui.groups.container) {
-			field.ui.groups.container.appendChild(groupElement);
-		}
-
-		// Create group object
-		const group = {
-			id: groupId,
-			fieldId: fieldKey,
-			element: groupElement,
-			grid: groupElement.querySelector('.item-grid.group'),
-			uploads: new Set(),
-			changes: {}
-		};
-
-		// Store group
-		this.groups.set(groupId, group);
-
-		// Initialize selection handler for this group
-		this.addGroupSelectionHandler(fieldKey, groupId);
-
-		// Persist state
-		this.schedulePersistance(fieldKey);
-
-		return group;
-	}
-
-	createGroupElement(groupId, fieldId) {
-		let groupElement = window.getTemplate('imageGroup');
-		if (!groupElement) return;
-
-		groupElement.dataset.groupId = groupId;
-		groupElement.dataset.fieldId = fieldId;
-
-		let fields = window.getTemplate('groupMetadata');
-		const fieldsContainer = groupElement.querySelector('.fields');
-		if (fieldsContainer && fields) {
-			fieldsContainer.append(fields);
-
-			// Set unique IDs and names for form fields
-			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
-			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
-
-			if (titleInput) {
-				titleInput.id = `${groupId}_title`;
-				titleInput.name = `${groupId}[post_title]`;
+		// Update upload statuses with attachment IDs
+		results.forEach(result => {
+			const upload = this.uploadStore.get(result.upload_id);
+			if (upload) {
+				upload.attachmentId = result.attachment_id;
+				upload.status = 'completed';
+				this.uploadStore.save(upload);
+				this.updateUploadStatus(result.upload_id, 'completed');
 			}
-			if (excerptInput) {
-				excerptInput.id = `${groupId}_excerpt`;
-				excerptInput.name = `${groupId}[post_excerpt]`;
-			}
-			let field = this.fields.get(fieldId);
-			if (field.config.content !== '') {
-				let summary = groupElement.querySelector('summary');
-				summary.textContent = field.config.content + ' Fields';
-			}
+		});
+
+		if (!fieldId) return;
+
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		// Clean up completed uploads from stores
+		const completedUploads = Array.from(fieldData.uploads).filter(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			return upload?.status === 'completed';
+		});
+
+		for (const uploadId of completedUploads) {
+			await this.clearUpload(uploadId, false);
+			fieldData.uploads.delete(uploadId);
+		}
+
+		// If all uploads complete, clear entire field from stores
+		if (fieldData.uploads.size === 0) {
+			await this.clearFieldFromStores(fieldId);
+			this.a11y.announce('All uploads completed successfully');
 		} else {
-			groupElement.querySelector('details').remove();
+			// Otherwise just update field state
+			await this.saveFieldData(fieldData);
 		}
 
-		const gridContainer = groupElement.querySelector('.item-grid.group');
-		if (gridContainer) {
-			gridContainer.dataset.groupId = groupId;
-		}
-
-		return groupElement;
-	}
-
-	deleteGroup(groupId, confirm = true) {
-		let group = this.groups.get(groupId);
-		if (!group) {
-			return;
-		}
-
-		let keepUploads = true;
-		if (confirm && group.uploads && group.uploads.size > 0) {
-			keepUploads = !window.confirm('Delete uploads in group?');
-		}
-
-		if (confirm && keepUploads) {
-			// Move any remaining uploads back to preview
-			if (group.uploads && group.uploads.size > 0) {
-				Array.from(group.uploads).forEach(uploadId => {
-					this.addImageToGroup(uploadId, null, false);
-				});
-			}
-		}
-
-		// Remove from groups Map
-		this.groups.delete(groupId);
-
-		// Remove DOM element
-		let groupElement = group.element;
-		if (groupElement) {
-			groupElement.remove();
-			this.a11y.announce('Group removed');
-		}
-
-		this.schedulePersistance(group.fieldId);
-	}
-
-	addToGroup(uploadId, target = null, persist = true) {
-		let upload = this.uploads.get(uploadId);
-		if(!upload) {
-			return;
-		}
-		let field = this.fields.get(upload.fieldId);
-		if (!field) {
-			return;
-		}
-
-		//Already in the Preview Grid, or already in the group we're moving to
-		if ((!target && upload.location === field.ui.preview) || target === upload.location) {
-			return;
-		}
-
-		// Remove from previous location
-		if (upload.location) {
-			let groupId = upload.location.dataset.groupId;
-			if (groupId) {
-				let group = this.groups.get(groupId);
-				if (group && group.uploads) {
-					group.uploads.delete(uploadId);
-
-					if (group.uploads.size === 0) {
-						this.deleteGroup(groupId);
-					}
-				}
-			}
-		}
-
-		const checkbox = upload.element.querySelector('[name*="select-item"]');
-		if (checkbox) {
-			checkbox.checked = false;
-		}
-
-		let featured = upload.element.querySelector('[name="featured"]');
-		featured.hidden = !target;
-
-
-		//If no target, it's going to the preview grid
-		if (!target) {
-			target = field.ui.preview;
-			upload.groupId = null;
-		} else if (!target.classList.contains('item-grid') || !target.classList.contains('preview')) {
-			// It's a group target
-			let groupId = target.dataset.groupId;
-			featured.name = groupId+'_'+featured.name;
-			let group = this.groups.get(groupId);
-			if (!group) {
-				group = this.createGroup(upload.fieldId);
-				target = group.grid;
-				groupId = group.id;
-			}
-			if (group) {
-				group.uploads.add(uploadId);
-				upload.groupId = groupId;
-			}
-
-		}
-
-		upload.location = target;
-		target.append(upload.element);
-
-		if (persist) {
-			this.schedulePersistance(field.id);
-		}
-	}
-
-	removeFromGroup(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		// Remove from current group if in one
-		if (upload.groupId) {
-			const group = this.groups.get(upload.groupId);
-			if (group?.uploads) {
-				group.uploads.delete(uploadId);
-
-				// Delete empty group
-				if (group.uploads.size === 0) {
-					this.deleteGroup(upload.groupId, false);
-				}
-			}
-			upload.groupId = null;
-		}
-
-		// Move back to preview
-		if (field.ui?.preview) {
-			field.ui.preview.appendChild(upload.element);
-			upload.location = field.ui.preview;
-		}
-
-		// Hide featured radio
-		const featured = upload.element.querySelector('[name="featured"]');
-		if (featured) {
-			featured.hidden = true;
-			featured.checked = false;
-		}
-	}
-
-	removeUpload(fieldId, uploadId) {
-		const field = this.fields.get(fieldId);
-		const upload = this.uploads.get(uploadId);
-
-		if (!field || !upload) return;
-
-		// Remove from field
-		field.uploads?.delete(uploadId);
-
-		// Remove from group if grouped
-		if (upload.groupId) {
-			const group = this.groups.get(upload.groupId);
-			if (group && group.uploads) {
-				group.uploads.delete(uploadId);
-
-				if (group.uploads.size === 0) {
-					this.removeGroup(upload.groupId);
-				}
-			}
-		}
-
-		// Clean up element
-		upload.element?.remove();
-
-		// Clean up memory
-		this.clearUpload(uploadId);
-
-		// Update field state after removal
 		this.updateFieldState(fieldId);
-
-		// Update UI
-		this.maybeLockUploads(fieldId);
-		const handler = this.selectionHandlers.get(field.id);
-		if (handler) {
-			handler.deselect(uploadId);
-		}
-
-		this.a11y.announce('Upload removed');
 	}
 
-	/*******************************************************************************
-	 * STATE MANAGEMENT
-	 *******************************************************************************/
-	schedulePersistance(fieldId) {
-		const key = `persist_${fieldId}`;
-		window.debouncer.schedule(
-			key,
-			() => this.persistFieldState(fieldId),
-			1000
-		);
-	}
+	/**
+	 * Handle operation failure
+	 */
+	handleOperationFailed(operation, fieldId) {
+		const uploadIds = operation.data instanceof FormData
+			? JSON.parse(operation.data.get('upload_ids') || '[]')
+			: operation.data.upload_ids || [];
 
-	async persistFieldState(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
+		uploadIds.forEach(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			if (upload) {
+				upload.status = operation.status === 'operation-failed-permanent'
+					? 'failed_permanent'
+					: 'failed';
+				this.uploadStore.save(upload);
+				this.updateUploadStatus(uploadId, upload.status);
+			}
+		});
 
-		// Convert Sets to Arrays for storage
-		const fieldData = {
-			...field,
-			id: fieldId, // Use as primary key
-			fieldId: fieldId,
-			uploads: Array.from(field.uploads || []).map(uploadId => {
-				return this.uploads.get(uploadId);;
-			}),
-			groups: Array.from(this.groups.entries())
-				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
-				.map(([id, data]) => ({
-					id: data.id,
-					uploads: Array.from(data.uploads),
-					changes: data.changes || {}
-				})),
-
-			// Context for restoration
-			context: {
-				url: this.normalizeUrl(window.location.href),
-				fullUrl: window.location.href,
-				modalType: this.getModalType(field),
-				formId: field.formId,
-				fieldSelector: `.field.upload[data-field="${field.config.name}"]`
-			},
-			timestamp: Date.now()
-		};
-
-		// Save to store
-		await this.fieldStore.save(fieldData);
-	}
-	normalizeUrl(url) {
-		try {
-			const urlObj = new URL(url);
-			// Return just the origin + pathname (no query string or hash)
-			return urlObj.origin + urlObj.pathname;
-		} catch (e) {
-			return url;
+		if (fieldId) {
+			this.updateFieldState(fieldId);
 		}
 	}
 
 	/**
-	 * Get uploads for a field, optionally cleaned for storage
-	 * @param {string} fieldId
-	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
-	 * @returns {Array}
+	 * Handle operation cancellation
 	 */
-	getFieldUploads(fieldId, clean = false) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.uploads) return [];
+	async handleOperationCancelled(fieldId) {
+		const fieldData = this.getFieldData(fieldId);  // ✅
+		if (!fieldData) return;
 
-		return Array.from(field.uploads)
-			.map(uploadId => {
-				const upload = this.uploads.get(uploadId);
-				if (!upload) return null;
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
 
-				if (clean) {
-					// Return cleaned version without DOM references or blob URLs
-					return {
-						id: upload.id,
-						fieldId: upload.fieldId,
-						status: upload.status,
-						// DON'T include preview (blob URL)
-						// DON'T include originalFile or processedFile (in blob storage)
-						attachmentId: upload.attachmentId,
-						operationId: upload.operationId,
-						groupId: upload.groupId || null,
-						changes: upload.changes || {}, // ← ADD: Include changes
-						meta: {
-							originalName: upload.meta?.originalName || upload.originalFile?.name,
-							size: upload.meta?.size || upload.originalFile?.size,
-							type: upload.meta?.type || upload.originalFile?.type,
-							title: upload.meta?.title,
-							alt: upload.meta?.alt,
-							caption: upload.meta?.caption
-						}
-					};
-				}
-
-				// Return full upload object
-				return upload;
-			})
-			.filter(Boolean);
-	}
-
-	async checkForStoredUploads() {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const fieldStore = tx.objectStore('fieldStates');
-
-		const allFieldStates = await new Promise(resolve => {
-			const request = fieldStore.getAll();
-			request.onsuccess = () => resolve(request.result);
-		});
-
-		//
-		// allFieldStates.forEach(field => {
-		// 	console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
-		// 	field.uploads.forEach((upload, idx) => {
-		// 		console.log(`  Upload ${idx}:`, {
-		// 			id: upload.id,
-		// 			status: upload.status,
-		// 			operationId: upload.operationId,
-		// 			hasOperationId: !!upload.operationId
-		// 		});
-		// 	});
-		// });
-
-		// Filter for pending uploads (not yet sent to server)
-		const pendingFields = allFieldStates.filter(field =>
-			field.uploads.some(upload =>
-				// If no operationId, it hasn't been sent to server yet
-				!upload.operationId &&
-				// And it's been processed locally
-				(upload.status === 'completed' ||
-					upload.status === 'processed' ||
-					upload.status === 'local_processing' ||
-					upload.status === 'processed-original')
-			)
-		);
-
-		if (pendingFields.length === 0) return;
-
-		// Show recovery notification
-		this.showRecoveryNotification(pendingFields);
-	}
-
-	async handleRestoreUploads() {
-		let notification = document.querySelector('dialog.restore-uploads');
-		if (!notification) {
-			return;
+		for (const uploadId of uploadsArray) {
+			await this.clearUpload(uploadId, false);
 		}
 
-		const selectedUploads = this.getSelectedRestorationUploads(notification);
-		if (selectedUploads.length === 0) {
-			return;
-		}
-		await this.restoreSelectedUploads(selectedUploads);
+		await this.clearFieldFromStores(fieldId);
+		this.updateFieldState(fieldId);
+		this.a11y.announce('Upload cancelled');
+	}
 
-		this.cleanupRestore();
+	getFieldGroups(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.groups) return [];
+
+		return fieldData.groups.map(group => ({
+			id: group.id,
+			uploads: group.uploads || [],
+			changes: group.changes || {}
+		}));
 	}
 
 	getSelectedRestorationUploads(notificationEl) {
@@ -2517,29 +1341,1347 @@
 		return selected;
 	}
 
-	handleGroupMetaChange(input) {
-		let group = this.getGroupFromElement(input);
-		if (!group) {
-			return;
+	async restoreSelectedUploads(selectedUploads) {
+		const byField = new Map();
+		selectedUploads.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		for (const [fieldId, uploadIds] of byField.entries()) {
+			const fieldState = this.fieldStore.get(fieldId);
+			if (fieldState) {
+				fieldState.uploads = uploadIds;
+				await this.restoreField(fieldState);
+			}
 		}
-		if (!Object.hasOwn(group, 'changes')) {
-			group.changes = {};
-		}
-		let name = input.name;
-		if (name.includes('group')) {
-			let replace = group.id+'_';
-			let replace2 = group.id+'[';
-			name = name.replace(replace, '').replace(replace2,'').replace(']', '');
-		}
-		group.changes[`${name}`] = input.value;
-		this.groups.set(group.id, group);
-		this.schedulePersistance(group.fieldId);
 	}
 
+	async restoreField(fieldState) {
+		const { config, context, uploads, groups, id } = fieldState;
+
+		// If in a modal, open it first
+		if (context?.modalType) {
+			await this.openModalForRestore(context);
+		}
+
+		// Find field element
+		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
+
+		if (!fieldElement) {
+			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
+			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
+		}
+
+		if (!fieldElement) {
+			console.warn(`Field ${config.name} not found for restoration`, config);
+			return;
+		}
+
+		// Register the field if not already registered
+		let fieldKey = fieldElement.dataset.uploader;
+		if (!fieldKey || !this.fieldElements.has(fieldKey)) {
+			fieldKey = this.registerUploader(fieldElement);
+		}
+
+		const fieldEl = this.fieldElements.get(fieldKey);
+		const fieldData = this.getFieldData(fieldKey);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Failed to register field for restoration');
+			return;
+		}
+
+		// Merge saved state back into field
+		fieldData.state = fieldState.state || 'ready';
+
+		// Rebuild UI references if needed
+		if (!fieldEl.ui) {
+			fieldEl.ui = this.buildFieldUI(fieldElement);
+		}
+
+		if (fieldEl.ui.groups?.display) {
+			fieldEl.ui.groups.display.hidden = false;
+		}
+		if (fieldEl.ui.dropZone) {
+			fieldEl.ui.dropZone.hidden = true;
+		}
+
+		// Restore groups first
+		if (groups && groups.length > 0) {
+			await this.restoreGroups(fieldKey, groups);
+		}
+
+		// Handle both Array and Set for uploads
+		const uploadsArray = uploads instanceof Set
+			? Array.from(uploads)
+			: Array.isArray(uploads)
+				? uploads
+				: [];
+
+		// Restore uploads
+		for (const uploadId of uploadsArray) {
+			// Get upload data from store
+			const uploadData = this.uploadStore.get(uploadId);
+			if (uploadData) {
+				await this.restoreUpload(fieldKey, uploadData);
+			}
+		}
+
+		// Update field state
+		await this.saveFieldData(fieldData);
+		this.updateFieldState(fieldKey);
+		this.maybeLockUploads(fieldKey);
+		this.refreshSortable(fieldKey);
+
+		// Queue for upload if needed
+		if (config.mode === 'direct' && config.destination !== 'post_group') {
+			await this.queueUpload(fieldKey);
+		}
+	}
+
+	async restoreUpload(fieldId, uploadData) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Field not found for upload restoration:', fieldId);
+			return;
+		}
+
+		// Get reconstructed File from blob data
+		const file = await this.getBlobData(uploadData.id);
+
+		if (!file) {
+			console.warn('Blob data not found for upload:', uploadData.id);
+			return;
+		}
+
+		// Create preview URL
+		const previewUrl = this.createPreviewUrl(file);
+
+		// Recreate DOM element
+		const subtype = this.getSubtypeFromMime(file.type);
+		const element = this.createUploadElement({
+			id: uploadData.id,
+			preview: previewUrl,
+			meta: uploadData.meta || {
+				originalName: file.name,
+				size: file.size,
+				type: file.type
+			},
+			subtype: subtype
+		}, fieldData.config.destination === 'post_group');
+
+		// Determine correct location
+		let location;
+		if (uploadData.groupId) {
+			// Check if group exists
+			const groupEl = this.groupElements.get(uploadData.groupId);
+			if (groupEl?.grid) {
+				location = groupEl.grid;
+
+				// Add to group's upload list
+				const group = fieldData.groups?.find(g => g.id === uploadData.groupId);
+				if (group) {
+					if (!group.uploads) group.uploads = [];
+					if (!group.uploads.includes(uploadData.id)) {
+						group.uploads.push(uploadData.id);
+					}
+				}
+			} else {
+				// Group doesn't exist, add to preview
+				location = fieldEl.ui.preview;
+				uploadData.groupId = null;
+			}
+		} else {
+			// No group, add to preview
+			location = fieldEl.ui.preview;
+		}
+
+		// Add element to DOM
+		if (location) {
+			location.appendChild(element);
+		} else if (fieldEl.ui.preview) {
+			fieldEl.ui.preview.appendChild(element);
+			location = fieldEl.ui.preview;
+		}
+
+		// Store runtime element data
+		this.uploadElements.set(uploadData.id, {
+			element: element,
+			preview: previewUrl,
+			location: location
+		});
+
+		// Add to field uploads
+		if (!fieldData.uploads) fieldData.uploads = new Set();
+		fieldData.uploads.add(uploadData.id);
+
+		// Update upload data in store
+		uploadData.status = 'processed';
+		await this.uploadStore.save(uploadData);
+
+		// Update sortable state for the grid
+		if (location) {
+			this.updateSortableState(location);
+		}
+	}
+
+	async restoreGroups(fieldId, groups) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Field not found for group restoration:', fieldId);
+			return;
+		}
+
+		for (const groupData of groups) {
+			// Create group using existing method which handles all initialization
+			const group = this.createGroup(fieldId, groupData.id);
+
+			if (!group) {
+				console.warn('Failed to create group:', groupData.id);
+				continue;
+			}
+
+			// Find the group in fieldData (createGroup already added it)
+			const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
+
+			if (storedGroup) {
+				// Restore metadata
+				if (groupData.changes) {
+					storedGroup.changes = { ...groupData.changes };
+				}
+
+				if (groupData.uploads) {
+					storedGroup.uploads = [...groupData.uploads];
+				}
+
+				// Restore form field values if they exist
+				if (groupData.changes) {
+					const titleInput = group.element.querySelector('[name*="post_title"]');
+					const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
+
+					if (titleInput && groupData.changes.post_title) {
+						titleInput.value = groupData.changes.post_title;
+					}
+
+					if (excerptInput && groupData.changes.post_excerpt) {
+						excerptInput.value = groupData.changes.post_excerpt;
+					}
+				}
+			}
+		}
+
+		// Save updated field data
+		await this.saveFieldData(fieldData);
+	}
+
+	async openModalForRestore(context) {
+		if (!context) return;
+
+		const { modalType, itemId } = context;
+
+		// Find and click the appropriate button to open the modal
+		let trigger = null;
+
+		switch(modalType) {
+			case 'create':
+				trigger = document.querySelector('[data-action="create"]');
+				break;
+			case 'edit':
+				// Need to find the specific edit button
+				if (itemId) {
+					trigger = document.querySelector(`[data-action="edit"][data-id="${itemId}"]`);
+				}
+				break;
+			case 'bulkEdit':
+				trigger = document.querySelector('[data-action="bulk-edit"]');
+				break;
+		}
+
+		if (trigger) {
+			trigger.click();
+
+			// Wait for modal to open and render
+			await new Promise(resolve => setTimeout(resolve, 300));
+		} else {
+			console.warn('Modal trigger not found for restoration:', context);
+		}
+	}
+
+	formatBytes(bytes, decimals = 2) {
+		if (bytes === 0) return '0 Bytes';
+		const k = 1024;
+		const dm = decimals < 0 ? 0 : decimals;
+		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+		const i = Math.floor(Math.log(bytes) / Math.log(k));
+		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+	}
 
 	/*******************************************************************************
-	 * RESTORING UPLOADS
+	 * CLEANUP METHODS - AGGRESSIVE CLEANUP AFTER SUCCESS
 	 *******************************************************************************/
+
+	/**
+	 * Clear individual upload from stores (called after successful upload)
+	 */
+	async clearUpload(uploadId, persist = true) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (uploadEl) {
+			this.revokePreviewUrl(uploadEl.preview);
+			if (uploadEl.element) {
+				const previewUrl = uploadEl.element.dataset.previewUrl;
+				this.revokePreviewUrl(previewUrl);
+				delete uploadEl.element.dataset.previewUrl;
+			}
+		}
+
+		// Remove from runtime memory
+		this.uploadElements.delete(uploadId);
+
+		// Remove from store (no separate blob store - it's part of the upload object)
+		await this.uploadStore.delete(uploadId);
+
+		// Update field if needed
+		if (persist) {
+			const upload = this.uploadStore.get(uploadId);
+			if (upload?.fieldId) {
+				await this.schedulePersistance(upload.fieldId);
+			}
+		}
+	}
+
+	/**
+	 * Clear entire field from stores (called when all uploads complete)
+	 */
+	async clearFieldFromStores(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+
+		// Clear all related uploads
+		if (fieldData?.uploads) {
+			const uploadsArray = fieldData.uploads instanceof Set
+				? Array.from(fieldData.uploads)
+				: fieldData.uploads;
+
+			for (const uploadId of uploadsArray) {
+				await this.uploadStore.delete(uploadId);
+			}
+		}
+
+		// Clear field from store
+		await this.fieldStore.delete(fieldId);
+
+		// Keep runtime references (fieldElements, etc) intact for reuse
+	}
+
+	cleanupAllPreviewUrls() {
+		if (this.previewUrls) {
+			this.previewUrls.forEach(url => {
+				try {
+					URL.revokeObjectURL(url);
+				} catch (e) {
+					// Ignore errors during cleanup
+				}
+			});
+			this.previewUrls.clear();
+		}
+	}
+
+	/*******************************************************************************
+	 * UI UPDATE METHODS
+	 *******************************************************************************/
+
+	updateFieldState(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldEl || !fieldData) return;
+
+		const container = fieldEl.element;
+		const uploadCount = fieldData.uploads?.size || 0;
+		const hasGroups = fieldEl.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+		container.dataset.uploadCount = uploadCount.toString();
+		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+		if (fieldEl.ui.preview) {
+			fieldEl.ui.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
+		}
+	}
+
+	updateUploadProgress(fieldId, current, total, message) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.ui?.progress?.progress) return;
+
+		const progress = fieldEl.ui.progress;
+		const percent = total > 0 ? (current / total) * 100 : 0;
+
+		if (progress.fill) progress.fill.style.width = `${percent}%`;
+		if (progress.text) progress.text.textContent = message;
+		if (progress.count) progress.count.textContent = `${current}/${total}`;
+
+		progress.progress.hidden = (current === total);
+	}
+
+	updateFieldStatus(fieldId, status) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		fieldData.state = status;
+		this.saveFieldData(fieldData);
+	}
+
+	updateUploadStatus(uploadId, status) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		upload.status = status;
+		this.uploadStore.save(upload);
+		this.updateUploadUI(uploadId);
+	}
+
+	updateUploadUI(uploadId) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload || !uploadEl?.element) return;
+
+		uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
+		uploadEl.element.classList.add(`status-${upload.status}`);
+
+		const progress = uploadEl.element.querySelector('.progress');
+		if (progress) {
+			this.updateUploadItemProgress(uploadId,
+				this.getStatusProgress(upload.status),
+				upload.status
+			);
+		}
+	}
+
+	showUploadProgress(uploadId, show = true) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!uploadEl?.element) return;
+
+		const progressEl = uploadEl.element.querySelector('.progress');
+		if (progressEl) {
+			if (show) {
+				progressEl.style.removeProperty('animation');
+				progressEl.hidden = false;
+			} else {
+				progressEl.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => { progressEl.hidden = true; }, 300);
+			}
+		}
+	}
+
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!uploadEl?.element) return;
+
+		const progressEl = uploadEl.element.querySelector('.progress');
+		if (!progressEl) return;
+
+		const fill = progressEl.querySelector('.fill');
+		const details = progressEl.querySelector('.details');
+		const icon = progressEl.querySelector('.icon');
+
+		if (fill) fill.style.width = `${percent}%`;
+		if (status && details) details.textContent = this.getStatusText(status);
+		if (status && icon) icon.innerHTML = this.getStatusIcon(status).outerHTML;
+	}
+
+	maybeLockUploads(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldEl?.ui?.dropZone || !fieldData) return;
+
+		if (fieldData.config.destination === 'post_group') return;
+
+		const uploadCount = fieldData.uploads?.size || 0;
+		const maxFiles = fieldData.config?.maxFiles || 999;
+
+		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
+		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+	}
+
+	/*******************************************************************************
+	 * GROUP MANAGEMENT
+	 *******************************************************************************/
+
+	createGroup(fieldId, groupId = null) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData || !fieldEl) return null;
+
+		if (!groupId) {
+			groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+		}
+
+		const groupElement = this.createGroupElement(groupId, fieldId);
+		if (!groupElement) return null;
+
+		// Store in field UI Map
+		if (!fieldEl.ui.groups) {
+			fieldEl.ui.groups = {
+				groups: new Map(),
+				container: null,
+				empty: null,
+				display: null
+			};
+		}
+
+		fieldEl.ui.groups.groups.set(groupId, groupElement);
+
+		// Insert into DOM
+		if (fieldEl.ui.groups.container && fieldEl.ui.groups.empty) {
+			fieldEl.ui.groups.container.insertBefore(groupElement, fieldEl.ui.groups.empty);
+		} else if (fieldEl.ui.groups.container) {
+			fieldEl.ui.groups.container.appendChild(groupElement);
+		}
+
+		// Store group element reference
+		const grid = groupElement.querySelector('.item-grid.group');
+		this.groupElements.set(groupId, {
+			element: groupElement,
+			grid: grid,
+			fieldId: fieldId
+		});
+
+		// Add to field groups
+		if (!fieldData.groups) fieldData.groups = [];
+		fieldData.groups.push({
+			id: groupId,
+			uploads: [],
+			changes: {}
+		});
+		this.saveFieldData(fieldData);
+
+		// Initialize selection handler and sortable
+		this.addGroupSelectionHandler(fieldId, groupId);
+
+		// Initialize sortable for this new group grid
+		if (grid) {
+			const sortableInstance = new Sortable(grid, {
+				...this.sortableConfig,
+				group: { name: fieldId, pull: true, put: true },
+				disabled: true, // Empty initially
+				onAdd: (evt) => this.updateSortableState(evt.to),
+				onRemove: (evt) => this.updateSortableState(evt.from)
+			});
+			grid.sortableInstance = sortableInstance;
+			this.sortableInstances.set(`${fieldId}-group-${groupId}`, sortableInstance);
+		}
+
+		return { id: groupId, element: groupElement, grid: grid };
+	}
+
+	createGroupElement(groupId, fieldId) {
+		let groupElement = window.getTemplate('imageGroup');
+		if (!groupElement) return;
+
+		groupElement.dataset.groupId = groupId;
+		groupElement.dataset.fieldId = fieldId;
+
+		let fields = window.getTemplate('groupMetadata');
+		const fieldsContainer = groupElement.querySelector('.fields');
+		if (fieldsContainer && fields) {
+			fieldsContainer.append(fields);
+
+			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+			if (titleInput) {
+				titleInput.id = `${groupId}_title`;
+				titleInput.name = `${groupId}[post_title]`;
+			}
+			if (excerptInput) {
+				excerptInput.id = `${groupId}_excerpt`;
+				excerptInput.name = `${groupId}[post_excerpt]`;
+			}
+
+			const fieldData = this.getFieldData(fieldId);
+			if (fieldData && fieldData.config.content !== '') {
+				let summary = groupElement.querySelector('summary');
+				if (summary) summary.textContent = fieldData.config.content + ' Fields';
+			}
+		} else {
+			groupElement.querySelector('details')?.remove();
+		}
+
+		const gridContainer = groupElement.querySelector('.item-grid.group');
+		if (gridContainer) {
+			gridContainer.dataset.groupId = groupId;
+		}
+
+		return groupElement;
+	}
+
+	deleteGroup(groupId, confirm = true) {
+		const groupEl = this.groupElements.get(groupId);
+		if (!groupEl) return;
+
+		const fieldData = this.getFieldData(groupEl.fieldId);
+		if (!fieldData) return;
+
+		const group = fieldData.groups?.find(g => g.id === groupId);
+		let keepUploads = true;
+
+		if (confirm && group?.uploads?.length > 0) {
+			keepUploads = !window.confirm('Delete uploads in group?');
+		}
+
+		if (confirm && keepUploads && group?.uploads) {
+			// Move uploads back to preview
+			group.uploads.forEach(uploadId => {
+				this.removeFromGroup(uploadId);
+			});
+		}
+
+		// Remove from field groups
+		if (fieldData.groups) {
+			fieldData.groups = fieldData.groups.filter(g => g.id !== groupId);
+			this.saveFieldData(fieldData);
+		}
+
+		// Remove DOM element
+		if (groupEl.element) {
+			groupEl.element.remove();
+			this.a11y.announce('Group removed');
+		}
+
+		// Remove from maps
+		this.groupElements.delete(groupId);
+
+		// Clean up sortable
+		const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
+		const sortable = this.sortableInstances.get(sortableKey);
+		if (sortable?.destroy) {
+			sortable.destroy();
+		}
+		this.sortableInstances.delete(sortableKey);
+
+		this.schedulePersistance(groupEl.fieldId);
+	}
+
+	addToGroup(uploadId, target = null, persist = true) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		const fieldEl = this.fieldElements.get(upload.fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Already in correct location
+		if ((!target && uploadEl.location === fieldEl.ui.preview) || target === uploadEl.location) {
+			return;
+		}
+
+		// Remove from previous group
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clear selection checkbox
+		const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
+		if (checkbox) checkbox.checked = false;
+
+		let featured = uploadEl.element.querySelector('[name="featured"]');
+		if (featured) featured.hidden = !target;
+
+		// Moving to preview or to group
+		if (!target || target.classList.contains('preview')) {
+			target = fieldEl.ui.preview;
+			upload.groupId = null;
+		} else {
+			// Moving to group
+			const groupId = target.dataset.groupId;
+			if (featured) featured.name = groupId + '_' + featured.name;
+
+			const group = fieldData.groups?.find(g => g.id === groupId);
+			if (group) {
+				if (!group.uploads) group.uploads = [];
+				group.uploads.push(uploadId);
+				upload.groupId = groupId;
+			}
+		}
+
+		// Update location
+		uploadEl.location = target;
+		target.append(uploadEl.element);
+
+		// Update stores
+		this.uploadStore.save(upload);
+		if (persist) {
+			this.saveFieldData(fieldData);
+		}
+
+		// Update sortable state
+		this.updateSortableState(target);
+		if (uploadEl.location && uploadEl.location !== target) {
+			this.updateSortableState(uploadEl.location);
+		}
+	}
+
+	removeFromGroup(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		const fieldEl = this.fieldElements.get(upload.fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Remove from current group
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId, false);
+				}
+			}
+			upload.groupId = null;
+		}
+
+		// Move back to preview
+		if (fieldEl.ui?.preview) {
+			fieldEl.ui.preview.appendChild(uploadEl.element);
+			uploadEl.location = fieldEl.ui.preview;
+		}
+
+		// Hide featured radio
+		const featured = uploadEl.element.querySelector('[name="featured"]');
+		if (featured) {
+			featured.hidden = true;
+			featured.checked = false;
+		}
+
+		this.uploadStore.save(upload);
+		this.updateSortableState(fieldEl.ui.preview);
+	}
+
+	removeUpload(fieldId, uploadId) {
+		const fieldData = this.getFieldData(fieldId);
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+
+		if (!fieldData || !upload) return;
+
+		// Remove from field
+		fieldData.uploads?.delete(uploadId);
+
+		// Remove from group if grouped
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clean up element
+		uploadEl?.element?.remove();
+
+		// Clean up memory
+		this.clearUpload(uploadId);
+
+		// Update field state
+		this.saveFieldData(fieldData);
+		this.updateFieldState(fieldId);
+		this.maybeLockUploads(fieldId);
+
+		const handler = this.selectionHandlers.get(fieldId);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	handleGroupMetaChange(input) {
+		const groupEl = this.getGroupFromElement(input);
+		if (!groupEl) return;
+
+		const fieldData = this.getFieldData(groupEl.fieldId);
+		const group = fieldData?.groups?.find(g => g.id === groupEl.element.dataset.groupId);
+		if (!group) return;
+
+		if (!group.changes) group.changes = {};
+
+		let name = input.name;
+		if (name.includes('group')) {
+			name = name.replace(`${group.id}_`, '').replace(`${group.id}[`, '').replace(']', '');
+		}
+
+		group.changes[name] = input.value;
+		this.saveFieldData(fieldData);
+		this.schedulePersistance(groupEl.fieldId);
+	}
+
+	/*******************************************************************************
+	 * ACTION HANDLERS
+	 *******************************************************************************/
+
+	handleAction(button) {
+		const action = button.dataset.action;
+		const fieldId = this.getFieldIdFromElement(button);
+
+		switch(action) {
+			case 'add-to-group':
+				this.handleAddToGroup(button);
+				break;
+			case 'delete-group':
+				this.handleDeleteGroup(button);
+				break;
+			case 'delete-upload':
+			case 'remove-from-group':
+				this.handleRemoveItem(button);
+				break;
+			case 'upload':
+				const fieldEl = this.fieldElements.get(fieldId);
+				if (fieldEl) {
+					fieldEl.element.closest('details').open = false;
+					document.body.classList.add('uploading');
+					this.submitUploads(fieldId);
+				}
+				break;
+			case 'restore':
+				this.handleRestoreUploads().then(() => {});
+				break;
+			case 'clear-cache':
+				if (!confirm('Save these uploads for later?')) {
+					this.cleanupStoredUploads();
+				}
+				this.cleanupRestore();
+				break;
+		}
+	}
+
+	handleAddToGroup(button) {
+		const fieldElement = button.closest(this.selectors.field.field);
+		const fieldId = fieldElement?.dataset.uploader;
+		if (!fieldId) return;
+
+		const selected = this.selected.get(fieldId);
+
+		if (!selected || selected.size === 0) {
+			this.createGroup(fieldId);
+		} else {
+			const group = this.createGroup(fieldId);
+			if (!group) return;
+
+			selected.forEach(uploadId => {
+				this.addToGroup(uploadId, group.grid);
+			});
+
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+
+			this.a11y.announce(`Created group with ${selected.size} items`);
+		}
+
+		this.schedulePersistance(fieldId);
+	}
+
+	handleDeleteGroup(button) {
+		const group = button.closest(this.selectors.groups.container);
+		if (!group) return;
+
+		const groupId = group.dataset.groupId;
+		const fieldId = this.getFieldIdFromElement(group);
+
+		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+			return;
+		}
+
+		const items = group.querySelectorAll(this.selectors.items.item);
+		items.forEach(item => {
+			const uploadId = item.dataset.uploadId;
+			this.removeFromGroup(uploadId);
+		});
+
+		this.deleteGroup(groupId);
+		this.a11y.announce('Group deleted, items returned to upload area');
+		this.schedulePersistance(fieldId);
+	}
+
+	handleRemoveItem(button) {
+		const item = button.closest(this.selectors.items.item);
+		if (!item) return;
+
+		const uploadId = item.dataset.uploadId;
+		const fieldId = this.getFieldIdFromElement(item);
+
+		if (!confirm('Remove this item?')) return;
+
+		this.removeUpload(fieldId, uploadId);
+		this.a11y.announce('Item removed');
+		this.schedulePersistance(fieldId);
+	}
+
+	/*******************************************************************************
+	 * SELECTION MANAGEMENT
+	 *******************************************************************************/
+
+	addFieldSelectionHandler(fieldId) {
+		if (this.selectionHandlers.has(fieldId)) {
+			return this.selectionHandlers.get(fieldId);
+		}
+
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.element) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: fieldEl.element,
+			ui: {
+				selectAll: fieldEl.element.querySelector('[name="select-all-uploads"]'),
+				bulkControls: fieldEl.element.querySelector('.selection-actions'),
+				count: fieldEl.element.querySelector('.selection-count')
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(fieldId, handler);
+		return handler;
+	}
+
+	addGroupSelectionHandler(fieldId, groupId) {
+		const handlerKey = `${fieldId}_${groupId}`;
+		if (this.selectionHandlers.has(handlerKey)) {
+			return this.selectionHandlers.get(handlerKey);
+		}
+
+		const groupEl = this.groupElements.get(groupId);
+		if (!groupEl?.element) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: groupEl.element,
+			ui: {
+				selectAll: groupEl.element.querySelector(this.selectors.groups.selectAll),
+				bulkControls: groupEl.element.querySelector(this.selectors.groups.actions),
+				count: groupEl.element.querySelector(this.selectors.groups.count)
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(handlerKey, handler);
+		return handler;
+	}
+
+	handleSelectAll(container, selected) {
+		// Can add custom logic here if needed
+	}
+
+	getCurrentSelection(fieldId) {
+		let selected = [];
+		for (let [key, handler] of this.selectionHandlers) {
+			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
+				selected = selected.concat([...handler.selectedItems]);
+			}
+		}
+		return selected;
+	}
+
+	/*******************************************************************************
+	 * HELPER METHODS
+	 *******************************************************************************/
+
+	/**
+	 * Get field data from store and normalize it
+	 * Always use this instead of directly accessing fieldStore.get()
+	 */
+	getFieldData(fieldId) {
+		const fieldData = this.fieldStore.get(fieldId);
+		if (!fieldData) return null;
+
+		// Only convert uploads back to Set (DataStore returns Arrays)
+		if (Array.isArray(fieldData.uploads)) {
+			fieldData.uploads = new Set(fieldData.uploads);
+		} else if (!fieldData.uploads) {
+			fieldData.uploads = new Set();
+		}
+
+		// Ensure groups is an array
+		if (!Array.isArray(fieldData.groups)) {
+			fieldData.groups = [];
+		}
+
+		return fieldData;
+	}
+
+	/**
+	 * Save field data to store, converting Sets to Arrays
+	 */
+	async saveFieldData(fieldData) {
+		await this.fieldStore.save({
+			...fieldData,
+			timestamp: Date.now()
+		});
+	}
+
+	determineFieldId(fieldElement) {
+		const content = fieldElement.dataset.content ||
+			fieldElement.closest('dialog')?.dataset.content ||
+			fieldElement.closest('form')?.dataset.save || '';
+		const itemID = fieldElement.dataset.itemId ||
+			fieldElement.closest('dialog')?.dataset.itemId || '';
+		const field = fieldElement.dataset.field || '';
+
+		return `${content}_${itemID}_${field}`;
+	}
+
+	getFromElement(element, type) {
+		const map = {
+			'field': {
+				selector: this.selectors.field.field,
+				key: 'uploader',
+				getRuntimeData: (id) => this.fieldElements.get(id),
+				getStoreData: (id) => this.getFieldData(id)
+			},
+			'upload': {
+				selector: this.selectors.items.item,
+				key: 'uploadId',
+				getRuntimeData: (id) => this.uploadElements.get(id),
+				getStoreData: (id) => this.uploadStore.get(id)
+			},
+			'group': {
+				selector: this.selectors.groups.container,
+				key: 'groupId',
+				getRuntimeData: (id) => this.groupElements.get(id),
+				getStoreData: (id) => {
+					// Groups are stored in field.groups array
+					const groupEl = this.groupElements.get(id);
+					if (!groupEl) return null;
+					const fieldData = this.getFieldData(groupEl.fieldId);
+					return fieldData?.groups?.find(g => g.id === id);
+				}
+			}
+		};
+
+		const config = map[type];
+		if (!config) return null;
+
+		const el = element.closest(config.selector);
+		if (!el) return null;
+
+		const id = el.dataset[config.key];
+
+		// Return combined runtime + store data for convenience
+		const runtime = config.getRuntimeData(id);
+		const store = config.getStoreData(id);
+
+		return { ...runtime, ...store };
+	}
+
+	getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
+	getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
+	getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
+
+	getFieldIdFromElement(el) {
+		const field = this.getFromElement(el, 'field');
+		return field?.id ?? null;
+	}
+	getUploadIdFromElement(el) {
+		const upload = this.getFromElement(el, 'upload');
+		return upload?.id ?? null;
+	}
+	getGroupIdFromElement(el) {
+		const group = this.getFromElement(el, 'group');
+		return group?.id ?? null;
+	}
+
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
+	}
+
+	getStatusText(status) {
+		return this.statusMapping[status] || status;
+	}
+
+	getStatusIcon(status) {
+		return window.getIcon(this.queue.icons[status]);
+	}
+
+	getStatusProgress(status) {
+		const progress = {
+			'local_processing': 28,
+			'queued': 50,
+			'uploading': 66,
+			'pending': 75,
+			'processing': 89,
+			'completed': 100
+		};
+		return progress[status] || 0;
+	}
+
+	getModalType(fieldEl) {
+		if (!fieldEl?.element) return null;
+		if (fieldEl._cachedModalType !== undefined) {
+			return fieldEl._cachedModalType;
+		}
+
+		const dialog = fieldEl.element.closest('dialog');
+		if (!dialog) {
+			fieldEl._cachedModalType = null;
+			return null;
+		}
+
+		let modalType = null;
+		if (dialog.classList.contains('edit')) modalType = 'edit';
+		else if (dialog.classList.contains('create')) modalType = 'create';
+		else if (dialog.classList.contains('bulkEdit')) modalType = 'bulkEdit';
+		else modalType = dialog.className;
+
+		fieldEl._cachedModalType = modalType;
+		return modalType;
+	}
+
+	createUploadElement(upload, draggable = false) {
+		let image = window.getTemplate('uploadItem');
+		if (!image) return;
+
+		image.dataset.uploadId = upload.id;
+		image.dataset.subtype = upload.subtype || 'image';
+
+		let [featured, img, video, preview, details] = [
+			image.querySelector('[name="featured"]'),
+			image.querySelector('img'),
+			image.querySelector('video'),
+			image.querySelector('label > span'),
+			image.querySelector('details')
+		];
+
+		if (featured) featured.value = upload.id;
+
+		switch (upload.subtype) {
+			case 'image':
+				if (img) {
+					img.src = upload.preview;
+					img.alt = upload.meta?.originalName || '';
+				}
+				video?.remove();
+				preview?.remove();
+				break;
+			case 'video':
+				if (video) video.src = upload.preview;
+				img?.remove();
+				preview?.remove();
+				break;
+			case 'document':
+				const fileName = upload.meta?.originalName || '';
+				const extension = fileName.split('.').pop()?.toLowerCase() || '';
+				const iconMap = {
+					'pdf': 'file-pdf', 'csv': 'file-csv',
+					'doc': 'file-doc', 'docx': 'file-doc',
+					'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
+				};
+				const icon = window.getIcon(iconMap[extension] || 'file');
+				if (preview) {
+					preview.innerText = fileName;
+					preview.prepend(icon);
+				}
+				img?.remove();
+				video?.remove();
+				break;
+		}
+
+		if (details) {
+			let template = window.getTemplate('uploadMeta');
+			if (template) details.append(template);
+		}
+
+		image.draggable = draggable;
+
+		// Update input IDs
+		image.querySelectorAll('input').forEach(input => {
+			let id = input.id;
+			if (id) {
+				let newId = id + upload.id;
+				let label = input.parentNode.querySelector(`label[for="${id}"]`);
+				input.id = newId;
+				if (label) label.htmlFor = newId;
+			}
+		});
+
+		return image;
+	}
+
+	/*******************************************************************************
+	 * PERSISTENCE
+	 *******************************************************************************/
+
+	/**
+	 * Normalize field data loaded from IndexedDB
+	 * Converts Arrays back to Sets, handles missing properties
+	 */
+	normalizeFieldData(fieldData) {
+		if (!fieldData) return null;
+
+		// Convert uploads array back to Set
+		if (Array.isArray(fieldData.uploads)) {
+			fieldData.uploads = new Set(fieldData.uploads);
+		} else if (!fieldData.uploads) {
+			fieldData.uploads = new Set();
+		}
+
+		// Convert groups array, ensure proper structure
+		if (!Array.isArray(fieldData.groups)) {
+			fieldData.groups = [];
+		}
+
+		// Ensure each group has uploads array
+		fieldData.groups = fieldData.groups.map(group => ({
+			...group,
+			uploads: Array.isArray(group.uploads) ? group.uploads : []
+		}));
+
+		return fieldData;
+	}
+
+	schedulePersistance(fieldId) {
+		const key = `persist_${fieldId}`;
+		window.debouncer.schedule(
+			key,
+			() => this.persistFieldState(fieldId),
+			1000
+		);
+	}
+
+	async persistFieldState(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		// Save with updated timestamp
+		await this.saveFieldData(fieldData);
+	}
+
+	// In UploadManager, add blob conversion helpers
+	async saveBlobData(uploadId, file) {
+		const arrayBuffer = await file.arrayBuffer();
+
+		const uploadData = this.uploadStore.get(uploadId) || { id: uploadId };
+
+		// Store blob data as ArrayBuffer with metadata
+		uploadData.blobData = {
+			buffer: arrayBuffer,
+			name: file.name,
+			type: file.type,
+			size: file.size,
+			lastModified: file.lastModified || Date.now()
+		};
+
+		await this.uploadStore.save(uploadData);
+	}
+
+	async getBlobData(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload?.blobData) return null;
+
+		// Reconstruct File from ArrayBuffer
+		const blob = new Blob([upload.blobData.buffer], { type: upload.blobData.type });
+		return new File([blob], upload.blobData.name, {
+			type: upload.blobData.type,
+			lastModified: upload.blobData.lastModified
+		});
+	}
+
+	/*******************************************************************************
+	 * RECOVERY & RESTORATION
+	 *******************************************************************************/
+
+	handleFieldStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				// Check for pending uploads to restore
+				this.checkForStoredUploads();
+				break;
+		}
+	}
+
+	handleUploadStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				break;
+			case 'item-saved':
+				this.showSaveIndicator(data.key);
+				break;
+		}
+	}
+
+	async checkForStoredUploads() {
+		const allFieldStates = this.fieldStore.getAll();
+
+		console.log('Checking for stored uploads...', {
+			fieldStates: allFieldStates.length,
+			uploadStoreSize: this.uploadStore.data.size
+		});
+		const pendingFields = allFieldStates.filter(field => {
+			if (!field.uploads) return false;
+
+			// Handle both Set and Array (from IndexedDB)
+			const uploadsArray = field.uploads instanceof Set
+				? Array.from(field.uploads)
+				: Array.isArray(field.uploads)
+					? field.uploads
+					: [];
+
+			return uploadsArray.some(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				return upload && !upload.operationId &&
+					['completed', 'processed', 'local_processing', 'processed-original'].includes(upload.status);
+			});
+		});
+		console.log('Found pending fields:', pendingFields.length);
+		if (pendingFields.length === 0) return;
+
+		this.showRecoveryNotification(pendingFields);
+	}
+
 	async showRecoveryNotification(pendingFields) {
 		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
 		const totalGroups = pendingFields.reduce((sum, field) =>
@@ -2588,14 +2730,12 @@
 				// 	const imgEl = uploadItem.querySelector('img');
 				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
 				//
-				const blobData = await this.uploadStore.getBlob(upload.id);
+				const file = await this.getBlobData(upload.id);
+				if (file) {
 
-
-				if (blobData) {
 					try {
 						// Create new blob URL from stored data
-						const blob = new Blob([blobData.data], { type: blobData.type });
-						const previewUrl = this.createPreviewUrl(blob);
+						const previewUrl = this.createPreviewUrl(file);
 
 						let [
 							featured,
@@ -2616,7 +2756,7 @@
 
 						uploadItem.dataset.fieldId = field.id;
 
-						let subtype = this.getSubtypeFromMime(blobData.type);
+						let subtype = this.getSubtypeFromMime(file.type);
 						uploadItem.dataset.subtype = subtype;
 						switch (subtype) {
 							case 'image':
@@ -2625,7 +2765,7 @@
 									img.alt
 								] = [
 									previewUrl,
-									upload.originalFile?.name ?? upload.meta?.originalName?? ''
+									file.name ?? upload.meta?.originalName ?? ''
 								];
 								video.remove();
 								preview.remove();
@@ -2720,336 +2860,23 @@
 
 	}
 
-	async restoreSelectedUploads(selectedUploads) {
-		// Group by field
-		const byField = new Map();
-		selectedUploads.forEach(item => {
-			if (!byField.has(item.fieldId)) {
-				byField.set(item.fieldId, []);
-			}
-			byField.get(item.fieldId).push(item.uploadId);
-		});
-
-		// Get full field states from IndexedDB
-		if (!this.db) {
-			// this.notifications.add('Cannot restore: Database not available', 'error');
+	async handleRestoreUploads() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
 			return;
 		}
 
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const store = tx.objectStore('fieldStates');
-
-		for (const [fieldId, uploadIds] of byField.entries()) {
-			const request = store.get(fieldId);
-			const fieldState = await new Promise(resolve => {
-				request.onsuccess = () => resolve(request.result);
-				request.onerror = () => resolve(null);
-			});
-
-			if (fieldState) {
-				// Filter to only selected uploads
-				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
-				await this.restoreField(fieldState);
-			}
-		}
-
-		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
-	}
-
-	async restoreField(fieldState) {
-		const { config, context, uploads, groups, id } = fieldState;  // ← Use 'id'
-
-		// If in a modal, open it first
-		if (context.modalType) {
-			await this.openModalForRestore(context);
-		}
-
-		// Find field element
-		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
-
-		if (!fieldElement) {
-			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
-			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
-		}
-
-		if (!fieldElement) {
-			console.warn(`Field ${config.name} not found for restoration`, config);
+		const selectedUploads = this.getSelectedRestorationUploads(notification);
+		if (selectedUploads.length === 0) {
 			return;
 		}
+		await this.restoreSelectedUploads(selectedUploads);
 
-		// Register the field if not already registered
-		let fieldKey = fieldElement.dataset.uploader;
-		if (!fieldKey || !this.fields.has(fieldKey)) {
-			fieldKey = this.registerUploader(fieldElement, config);
-		}
-
-		const field = this.fields.get(fieldKey);
-		if (!field) {
-			console.error('Failed to register field for restoration');
-			return;
-		}
-
-		// Merge saved state back into field
-		field.state = fieldState.state || 'ready';
-
-		// Rebuild UI references
-		field.ui = this.buildFieldUI(fieldElement);
-
-		if (field.ui.groups?.display) {
-			field.ui.groups.display.hidden = false;
-		}
-
-		// Restore groups
-		if (groups && groups.length > 0) {
-			await this.restoreGroups(fieldKey, groups);
-		}
-
-		// Restore uploads
-		for (const uploadData of uploads) {
-			await this.restoreUpload(field, uploadData);
-		}
-
-		// Update UI
-		this.updateFieldState(fieldKey);
-		this.maybeLockUploads(fieldKey);
-
-		// Queue for upload if needed
-		if (config.mode === 'direct' && config.destination !== 'post_group') {
-			await this.queueUpload(fieldKey);
-		}
+		this.cleanupRestore();
 	}
 
-	async restoreUpload(field, uploadData) {
-		// Try to get blob data from IndexedDB
-		const blobData = await this.uploadStore.getBlob(uploadData.id);
-
-		if (blobData) {
-			const file = blobData.data instanceof File
-				? blobData.data
-				: new File(
-					[blobData.data],
-					blobData.name,
-					{ type: blobData.type, lastModified: blobData.lastModified }
-				);
-
-			uploadData.originalFile = file;
-			uploadData.processedFile = file;
-			uploadData.preview = this.createPreviewUrl(file);
-		} else {
-			console.warn('Blob data not found for upload:', uploadData.id);
-			return; // Skip this upload if we can't restore the file
-		}
-
-		// Add to field
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadData.id);
-
-		// Recreate DOM element
-		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
-		uploadData.element = this.createUploadElement({
-			...uploadData,
-			subtype: subtype
-		}, field.config.destination === 'post_group');
-
-		// Restore to correct location
-		let location;
-		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
-			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
-		} else {
-			location = field.ui.preview;
-		}
-
-		if (location) {
-			location.appendChild(uploadData.element);
-			uploadData.location = location;
-		}
-
-		// Store in memory
-		this.uploads.set(uploadData.id, uploadData);
-		if (uploadData.groupId) {
-			const group = this.groups.get(uploadData.groupId);
-			if (group && group.uploads) {
-				group.uploads.add(uploadData.id);
-			}
-		}
-	}
-
-	async restoreGroups(fieldKey, groups) {
-		for (const groupData of groups) {
-			// Use createGroup which properly initializes EVERYTHING including selection handlers
-			const group = this.createGroup(fieldKey, groupData.id);
-
-			if (group) {
-				// Update the group metadata from saved state
-				if (groupData.meta) {
-					group.meta = { ...groupData.meta };
-				}
-				if (groupData.changes) {
-					group.changes = { ...groupData.changes };
-				}
-
-
-				// If you saved group titles, restore them
-				if (groupData.title) {
-					const titleInput = group.element.querySelector('[name*="post_title"]');
-					if (titleInput) {
-						titleInput.value = groupData.title;
-					}
-				}
-			}
-		}
-	}
-
-	async openModalForRestore(context) {
-		const { modalType, formId } = context;
-
-		// Find and click the appropriate button to open the modal
-		let trigger = null;
-
-		switch(modalType) {
-			case 'create':
-				trigger = document.querySelector('[data-action="create"]');
-				break;
-			case 'edit':
-				// Need to find the specific edit button
-				trigger = document.querySelector(`[data-action="edit"][data-id="${context.itemId}"]`);
-				break;
-			case 'bulkEdit':
-				trigger = document.querySelector('[data-action="bulk-edit"]');
-				break;
-		}
-
-		if (trigger) {
-			trigger.click();
-
-			// Wait for modal to open
-			await new Promise(resolve => setTimeout(resolve, 300));
-		}
-	}
-
-	/*******************************************************************************
-	 INDEXEDDB CACHE FUNCTIONALITY
-	 *******************************************************************************/
-	handleFieldStoreEvent(event, data) {
-		switch(event) {
-			case 'data-loaded':
-
-				break;
-			case 'item-saved':
-				console.log(`Field state saved: ${data.key}`);
-				break;
-		}
-	}
-
-	handleUploadStoreEvent(event, data) {
-		switch(event) {
-			case 'data-loaded':
-				this.checkForStoredUploads();
-				break;
-			case 'item-saved':
-				this.showSaveIndicator(data.key);
-				break;
-		}
-	}
-	async saveUpload(upload) {
-		// Use the processed file if available, otherwise original
-		const fileToStore = upload.processedFile || upload.originalFile || upload.file;
-
-		if (fileToStore instanceof File || fileToStore instanceof Blob) {
-			await this.uploadStore.saveBlob(upload.id, fileToStore);
-
-			// Don't store file objects in main store
-			const { file, originalFile, processedFile, ...cleanUpload } = upload;
-			await this.uploadStore.save(cleanUpload);
-		} else {
-			await this.uploadStore.save(upload);
-		}
-	}
-
-	async loadFields() {
-		// Load all field states from the store
-		const fields = await this.fieldStore.getAll();
-
-		fields.forEach(field => {
-			// Reconstruct upload sets
-			if (field.uploads && Array.isArray(field.uploads)) {
-				field.uploads = new Set(field.uploads.map(u => u.id));
-			}
-			this.fields.set(field.fieldId, field);
-		});
-	}
-
-	async loadUploads() {
-		const uploads = await this.uploadStore.getAll();
-		uploads.forEach(upload => {
-			this.uploads.set(upload.id, upload);
-		});
-	}
-
-	/**************************************************************************
-	 SUBSCRIBERS
-	 **************************************************************************/
-	/**
-	 * Event system
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-	/*******************************************************************************
-	 * CLEANUP
-	 *******************************************************************************/
-
-	destroy() {
-		// Remove core listeners
-		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);
-
-		// Destroy drag controller
-		if (this.dragController) {
-			this.dragController.destroy();
-		}
-
-		// Destroy selection handlers
-		this.selectionHandlers.forEach(handler => handler.destroy());
-		this.selectionHandlers.clear();
-
-		this.cleanupAllPreviewUrls();
-		this.sortableInstances.forEach(instance => {
-			if (instance?.destroy) {
-				instance.destroy();
-			}
-		});
-		this.sortableInstances.clear();
-
-		// Clear data
-		this.fields.clear();
-		this.uploads.clear();
-		this.groups.clear();
-		this.selected.clear();
-		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);
-		});
+	showSaveIndicator(key) {
+		// Optional: show user that state is being saved
 	}
 
 	cleanupRestore() {
@@ -3062,65 +2889,60 @@
 	}
 
 	async cleanupStoredUploads() {
-		this.fieldStore.clear();
-		this.uploadStore.clear();
+		await this.fieldStore.clear();
+		await this.uploadStore.clear();
 	}
 
-	/**
-	 * Clear all uploads for a field and cleanup resources
-	 */
-	async clearField(fieldId) {
-		// Clear from stores
-		await this.fieldStore.delete(fieldId);
+	/*******************************************************************************
+	 * EVENT SYSTEM
+	 *******************************************************************************/
 
-		// Clear related uploads
-		const field = this.fields.get(fieldId);
-		if (field?.uploads) {
-			for (const uploadId of field.uploads) {
-				await this.uploadStore.delete(uploadId);
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data = {}) {
+		this.subscribers.forEach(cb => {
+			try {
+				cb(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
 			}
-		}
-
-		// Clear from memory
-		this.fields.delete(fieldId);
+		});
 	}
 
-	async clearUpload(uploadId, persist = true) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
+	/*******************************************************************************
+	 * DESTROY & CLEANUP
+	 *******************************************************************************/
 
-		// Clean up preview URL using helper
-		this.revokePreviewUrl(upload.preview);
+	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);
 
-		// Clean up element preview URL
-		if (upload.element) {
-			const previewUrl = upload.element.dataset.previewUrl;
-			this.revokePreviewUrl(previewUrl);
-			delete upload.element.dataset.previewUrl;
+		if (this.dragController) {
+			this.dragController.destroy();
 		}
 
-		if (persist) {
-			await this.schedulePersistance(upload.fieldId);
-		}
+		this.selectionHandlers.forEach(handler => handler.destroy());
+		this.selectionHandlers.clear();
 
-		// Remove from memory
-		this.uploads.delete(uploadId);
+		this.cleanupAllPreviewUrls();
 
-		// Remove from IndexedDB
-		this.uploadStore.delete(uploadId);
-		this.uploadStore.delete(uploadId, 'blobs');
-	}
-	cleanupAllPreviewUrls() {
-		if (this.previewUrls) {
-			this.previewUrls.forEach(url => {
-				try {
-					URL.revokeObjectURL(url);
-				} catch (e) {
-					// Ignore errors during cleanup
-				}
-			});
-			this.previewUrls.clear();
-		}
+		this.sortableInstances.forEach(instance => {
+			if (instance?.destroy) instance.destroy();
+		});
+		this.sortableInstances.clear();
+
+		this.uploadElements.clear();
+		this.fieldElements.clear();
+		this.groupElements.clear();
+		this.selected.clear();
+		this.subscribers.clear();
 	}
 }
 
diff --git a/assets/js/concise/View.js b/assets/js/concise/View.js
index e1e5488..ca6c99e 100644
--- a/assets/js/concise/View.js
+++ b/assets/js/concise/View.js
@@ -12,6 +12,8 @@
 
 		this.store = store;
 
+		this.isTimeline = !!document.querySelector('[data-timeline]');
+
 		this.items = {
 			list: new Map(),
 			grid: new Map(),
@@ -19,6 +21,7 @@
 		}
 		this.currentView = 'grid';
 		this.selectedItems = new Set();
+		this.subscribers = new Set();
 
 		this.init();
 	}
@@ -27,8 +30,11 @@
 		this.selectors = {
 			grid: '.item-grid',
 			table: {
-				table: 'table',
+				table: 'form.table',
+				form: 'table',
 				body: 'table body',
+				header: 'table thead',
+				footer: 'table tfoot',
 				selectedColumns: '.all-filters .multi-select',
 				columns: 'thead th'
 			},
@@ -147,6 +153,11 @@
 				this.render();
 			});
 		});
+
+		const checkedView = document.querySelector('[data-view]:checked');
+		if (checkedView) {
+			this.currentView = checkedView.dataset.view;
+		}
 	}
 
 	/**
@@ -162,16 +173,15 @@
 	 * Handle items update
 	 */
 	handleItemsUpdate() {
-		console.log(this.store.data);
-		this.render(this.store.data);
+		this.render();
 	}
 
-	render(items = []) {
+	render() {
 		if (!this.store) {
 			console.error('No store connected to renderer');
 			return;
 		}
-		console.log(items);
+		const items = this.store.getFiltered();
 
 		// Handle empty state
 		if (items.length === 0) {
@@ -216,24 +226,15 @@
 		const fragment = document.createDocumentFragment();
 
 		items.forEach(item => {
-			let card;
-			if (this.store.renderOrRetrieve) {
-				card = this.store.renderOrRetrieve(item, 'grid', this.renderGridItem.bind(this));
-			} else {
-				// Fallback to local cache
-				if (this.items.grid.has(item.id)) {
-					card = this.items.grid.get(item.id);
-				} else {
-					card = this.renderGridItem(item);
-					this.items.grid.set(item.id, card);
-				}
-			}
-
+			let card = this.renderGridItem(item);
 			fragment.appendChild(card);
 		});
 		this.ui.grid.appendChild(fragment);
 	}
 	renderGridItem(item) {
+		if (this.items.grid.has(item.id)) {
+			return this.items.grid.get(item.id);
+		}
 		const card = window.getTemplate('gridView');
 		card.dataset.id = item.id;
 
@@ -284,7 +285,10 @@
 				item.images[item.fields.post_thumbnail]?.medium??'',
 				item.images[item.fields.post_thumbnail]?.alt??'',
 			];
+
 		// }
+
+		this.items.grid.set(item.id, card);
 		return card;
 	}
 
@@ -294,12 +298,24 @@
 			let table = window.getTemplate('contentTable');
 			this.container.append(table);
 			this.ui.table.table = this.container.querySelector('form.table');
-			this.ui.table.body = this.ui.table.table.querySelector('tbody');
+			this.ui.table.form  = this.ui.table.table.querySelector('table');
+			this.ui.table.header = this.ui.table.form.querySelector('thead');
+			this.ui.table.footer = this.ui.table.form.querySelector('tfoot');
+			this.ui.table.body = this.ui.table.form.querySelector('tbody');
 			this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
 		}
+
 		if (this.ui.table.table) {
 			this.ui.table.table.hidden = !on;
-			window.removeChildren(this.ui.table.body);
+			if (on) {
+				this.notify('table-view', this.ui.table.table);
+			}else {
+				this.notify('not-table-view', this.ui.table.table);
+			}
+
+			if (this.ui.table.body){
+				window.removeChildren(this.ui.table.body);
+			}
 		}
 		this.ui.table.selectedColumns.hidden = !on;
 	}
@@ -313,22 +329,29 @@
 		this.toggleGrid();
 
 		items.forEach(item => {
-			let row;
-			if (this.items.table.has(item.id)) {
-				row = this.items.table.get(item.id);
+
+			let row = (this.isTimeline) ? this.renderTimelineTableItem(item) : this.renderTableItem(item);
+
+			if (this.ui.table.body) {
+				this.ui.table.body.append(row);
 			} else {
-				row = this.store.renderOrRetrieve(item, 'table', this.renderTableItem.bind(this));
-				this.items.table.set(item.id, row);
+				if (!this.ui.table.footer) {
+					this.ui.table.footer = this.ui.table.table.querySelector('tfoot');
+				}
+				this.ui.table.form.insertBefore(row, this.ui.table.footer);
 			}
 
-			this.ui.table.body.append(row);
 		});
 
 		window.jvbSelector.scanExistingFields();
+
 	}
 
 	renderTableItem(item) {
-		let empty = ['',0];
+		if (this.items.table.has(item.id)) {
+			return this.items.table.get(item.id);
+		}
+
 		const row = window.getTemplate('tableView');
 		row.dataset.id = item.id;
 
@@ -346,87 +369,142 @@
 			item.status
 		];
 
+		// Let jvbPopulate do its thing - NO prefixing needed!
+		new window.jvbPopulate(row, item.fields, item.images);
 
-		row.querySelectorAll('td[data-field]').forEach(field => {
-			let value = item.fields[field.dataset.field];
-			// field.querySelectorAll('label').forEach(label => {
-			// 	label.hidden = true;
-			// });
+		// Clean up after population
+		this.cleanupTableRow(row);
 
-			let label = field.querySelector('label');
-			let isEmpty = (empty.includes(value));
-			let temp;
-			switch (field.dataset.fieldType) {
-				case 'text':
-				case 'number':
-				case 'url':
-				case 'tel':
-				case 'email':
-					if (!isEmpty) {
-						field.querySelector('input').value = value;
-					}
-					label.remove();
-					break;
-				case 'textarea':
-					if (!isEmpty) {
-						field.querySelector('textarea').value = value;
-					}
-					label.remove();
-					break;
-				case 'taxonomy':
-					label.remove();
-					if (!isEmpty) {
-						temp = field.querySelector('input[type=hidden]');
-						temp.value = value;
-					}
-					break;
-				case 'image':
-					if (!isEmpty) {
-						let image = window.getTemplate('uploadItem');
-						let img = image.querySelector('img');
-						[
-							img.src,
-							img.alt
-						] = [
-							item.images[value].medium??'',
-							item.images[value].alt??'',
-						];
-						field.querySelector('.item-grid').append(image);
-						field.querySelector('input[type=hidden]').value = value;
-					}
-					field.querySelectorAll('.progress,label,.upload-select,.status,details').forEach(item => {
-						item.remove();
-					});
-					break;
-				case 'true_false':
-					if (!isEmpty) {
-						field.querySelector('input').checked = parseInt(value) === 1;
-					}
-					field.querySelector('.toggle-label')?.remove();
-					break;
-				case 'select':
-					label.remove();
-				case 'radio':
-				case 'checkbox':
-					field.querySelector('.label')?.remove();
-					if (!isEmpty) {
-						value = value.split(',');
-						value.forEach(v => {
-							temp = field.querySelector(`[value="${v}"]`);
-							if (temp) {
-								temp.checked = true;
-							}
-						});
-					}
-					break;
-				default:
-					if (!isEmpty) {
-						console.log(value);
-					}
-					break;
+		this.items.table.set(item.id, row);
+		return row;
+	}
+
+	renderTimelineTableItem(item) {
+		if (this.items.table.has(item.id)) {
+			return this.items.table.get(item.id);
+		}
+
+		const row = window.getTemplate('tableView');
+		row.dataset.id = item.id;
+
+		[
+			row.querySelector('.select-item').id,
+			row.querySelector('.select-item').value,
+			row.querySelector('.select-item').checked,
+			row.querySelector('.select-item + label').htmlFor,
+		] = [
+			item.id,
+			item.id,
+			this.selectedItems.has(`${item.id}`),
+			item.id,
+		];
+
+		let timelinePoint = row.querySelector('.timeline-point');
+		let tbody = row;
+
+		// Populate shared fields - NO prefixing!
+		let sharedRow = row.querySelector('tr.shared');
+		new window.jvbPopulate(sharedRow, item.fields, item.images);
+		this.prefixTimelineFieldNames(sharedRow, item.id);
+		this.cleanupTableRow(sharedRow);
+
+		// Handle timeline points - NO prefixing!
+		if (item.fields.timeline && typeof item.fields.timeline === 'object') {
+			const timelineArray = Object.entries(item.fields.timeline);
+
+			timelineArray.forEach(([imgId, timeline], index) => {
+
+				let point = timelinePoint.cloneNode(true);
+				point.dataset.index = index;
+				point.dataset.imageId = imgId;
+
+
+				new window.jvbPopulate(point, timeline, item.images);
+
+				this.cleanupTableRow(point);
+				let imgdata = item.images[timeline.post_thumbnail];
+				if (imgdata) {
+					point.querySelector('.field.upload').title = imgdata['image-title'];
+				}
+
+				this.prefixTimelineFieldNames(point,timeline.id);
+
+				tbody.insertBefore(point, timelinePoint);
+			});
+		}
+
+		timelinePoint.remove();
+
+		this.items.table.set(item.id, row);
+		return row;
+	}
+
+	/**
+	 * Timeline uses bracket notation: [postId]fieldName
+	 * This matches the collectTimeline() method in FormController
+	 */
+	prefixTimelineFieldNames(row, postId) {
+		row.querySelectorAll('input, textarea, select').forEach(field => {
+			const currentName = field.name;
+
+			if (!currentName || currentName.startsWith('[') ||
+				currentName === 'form-id' || currentName.startsWith('_')) {
+				return;
+			}
+
+			// Use bracket notation for timeline
+
+			let label = field.nextElementSibling;
+			field.name = `[${postId}]${currentName}`;
+			if (label && label.tagName === 'LABEL') {
+				field.id = `[${postId}]${field.id}`;
+				label.htmlFor = field.id;
 			}
 		});
-		return row;
+	}
+
+	cleanupTableRow(row) {
+		row.querySelectorAll('td[data-field]').forEach(field => {
+			// Remove labels (they're in the header)
+			field.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach(label => {
+				if (!label.closest('.radio-options')) {
+					label.remove();
+				}
+			});
+
+			// Special handling for image/upload fields
+			// if (field.dataset.fieldType === 'image' || field.dataset.fieldType === 'upload') {
+			// 	const itemGrid = field.querySelector('.item-grid');
+			// 	const uploadContainer = field.querySelector('.file-upload-container');
+			//
+			// 	// If grid has items (populated), just remove upload UI
+			// 	if (itemGrid && itemGrid.children.length > 0) {
+			// 		// Remove upload controls but keep the populated items
+			// 		field.querySelectorAll('.progress, .upload-select, .status, details:not(.item-grid details)').forEach(el => {
+			// 			el.remove();
+			// 		});
+			// 		// Keep upload container hidden if it was hidden
+			// 		if (uploadContainer && uploadContainer.hidden) {
+			// 			uploadContainer.hidden = true;
+			// 		}
+			// 	} else {
+			// 		// No items, remove all upload UI
+			// 		field.querySelectorAll('.file-upload-wrapper, .progress, .upload-select, .status, details').forEach(el => {
+			// 			el.remove();
+			// 		});
+			// 	}
+			// }
+
+			// Remove toggle labels for true_false fields
+			if (field.dataset.fieldType === 'true_false') {
+				field.querySelector('.toggle-label')?.remove();
+			}
+
+			// Remove field labels for checkbox/radio groups
+			if (['checkbox', 'radio', 'select'].includes(field.dataset.fieldType)) {
+				field.querySelector('.label')?.remove();
+			}
+		});
 	}
 
 	renderList(items) {
@@ -437,19 +515,15 @@
 		this.ui.grid.classList.add('list-view');
 
 		items.forEach(item => {
-			let row;
-			if (this.items.list.has(item.id)) {
-				row = this.items.list.get(item.id);
-			} else {
-				row = this.store.renderOrRetrieve(item, 'list', this.renderListItem.bind(this));
-				this.items.list.set(item.id, row);
-			}
-
+			let row = this.renderListItem(item);
 			this.ui.grid.appendChild(row);
 		});
 	}
 
 	renderListItem(item) {
+		if (this.items.list.has(item.id)) {
+			return this.items.list.get(item.id);
+		}
 		const row = window.getTemplate('listView');
 		row.dataset.id = item.id;
 
@@ -498,9 +572,158 @@
 				item.images[item.fields.post_thumbnail]?.alt??'',
 			]
 		}
+		this.items.list.set(item.id, row);
 		return row;
 	}
 
+	setupTimelineDragHandler() {
+		if (!this.isTimeline || this.currentView !== 'table') return;
+
+		// Clean up existing handler if any
+		if (this.timelineDragHandler) {
+			this.timelineDragHandler.destroy();
+		}
+
+		this.timelineDragHandler = new window.jvbDragHandler({
+			draggableSelector: '.timeline-point',
+			dropTargetSelector: '.timeline-point',
+			handleSelector: '.drag-handle',
+
+			getItemId: (element) => {
+				return element.dataset.imageId;
+			},
+
+			getSelectedItems: () => {
+				return [];
+			},
+
+			validateDrop: (itemIds, dropTarget) => {
+				const draggedRow = document.querySelector(`.timeline-point[data-image-id="${itemIds[0]}"]`);
+				if (!draggedRow) return false;
+
+				const draggedTbody = draggedRow.closest('tbody');
+				const targetTbody = dropTarget.closest('tbody');
+
+				return draggedTbody === targetTbody;
+			},
+
+			onDragStart: (itemIds, element) => {
+				element.classList.add('is-dragging');
+			},
+
+			onDrop: (itemIds, dropTarget) => {
+				const draggedRow = document.querySelector(`.timeline-point[data-image-id="${itemIds[0]}"]`);
+				if (!draggedRow) return;
+
+				// Remove all drop indicators
+				document.querySelectorAll('.drop-above, .drop-below').forEach(el => {
+					el.classList.remove('drop-above', 'drop-below');
+				});
+
+				const tbody = draggedRow.closest('tbody');
+				const dropPosition = dropTarget.dataset.dropPosition;
+
+				// Insert based on drop position
+				if (dropPosition === 'above') {
+					tbody.insertBefore(draggedRow, dropTarget);
+				} else {
+					tbody.insertBefore(draggedRow, dropTarget.nextSibling);
+				}
+
+				draggedRow.classList.remove('is-dragging');
+				this.updateTimelineOrder(tbody);
+			},
+
+			onDragEnd: (itemIds, success) => {
+				// Clean up all drag classes
+				document.querySelectorAll('.is-dragging, .drop-above, .drop-below').forEach(el => {
+					el.classList.remove('is-dragging', 'drop-above', 'drop-below');
+				});
+			},
+
+			previewElement: '.drag-handle',
+			previewOptions: {
+				offset: { x: -20, y: -20 },
+				showCount: false
+			}
+		});
+
+		// Add custom hover logic for better drop positioning
+		this.addTimelineDragHoverLogic();
+	}
+
+	addTimelineDragHoverLogic() {
+		let currentHover = null;
+
+		document.addEventListener('pointermove', (e) => {
+			if (!document.querySelector('.timeline-point.is-dragging')) return;
+
+			const target = e.target.closest('.timeline-point:not(.is-dragging)');
+			if (!target) {
+				if (currentHover) {
+					currentHover.classList.remove('drop-above', 'drop-below');
+					delete currentHover.dataset.dropPosition;
+					currentHover = null;
+				}
+				return;
+			}
+
+			// Determine if we're in the top or bottom half
+			const rect = target.getBoundingClientRect();
+			const midpoint = rect.top + (rect.height / 2);
+			const isTopHalf = e.clientY < midpoint;
+
+			// Update classes
+			if (currentHover && currentHover !== target) {
+				currentHover.classList.remove('drop-above', 'drop-below');
+				delete currentHover.dataset.dropPosition;
+			}
+
+			target.classList.remove('drop-above', 'drop-below');
+			target.classList.add(isTopHalf ? 'drop-above' : 'drop-below');
+			target.dataset.dropPosition = isTopHalf ? 'above' : 'below';
+
+			currentHover = target;
+		});
+	}
+	updateTimelineOrder(tbody) {
+		const postId = parseInt(tbody.dataset.id);
+		const rows = Array.from(tbody.querySelectorAll('.timeline-point'));
+
+		const item = this.store.get(postId);
+		if (!item) return;
+
+		let timeline = {};
+		// Update menu_order for each timeline point
+		rows.forEach((row, index) => {
+			const imgID = row.dataset.imageId;
+			timeline[imgID] = item.fields.timeline[imgID];
+		});
+		item.fields.timeline = timeline;
+
+		// Update store (triggers autosave)
+		this.store.save(item);
+		this.notify('order-changed', postId);
+		this.a11y?.announce(`Timeline order updated. ${rows.length} steps reordered.`);
+	}
+
+	extractRowFields(row) {
+		const fields = {};
+		row.querySelectorAll('[data-field]').forEach(cell => {
+			const fieldName = cell.dataset.field;
+			const input = cell.querySelector('input, textarea, select');
+
+			if (input) {
+				if (input.type === 'checkbox') {
+					fields[fieldName] = input.checked;
+				} else {
+					fields[fieldName] = input.value;
+				}
+			}
+		});
+		return fields;
+	}
+
 	toggleSelection(id) {
 		if (this.selectedItems.has(id)) {
 			this.selectedItems.delete(id);
@@ -545,6 +768,19 @@
 			this.ui.bulk.count.textContent = count === 0 ? '' : `${count} ${item} selected`;
 		}
 	}
+
+
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
 }
 
 window.jvbViews = ViewController;
diff --git a/assets/js/dash/A11yHelper.js b/assets/js/dash/A11yHelper.js
index 02be1a0..9723d6f 100644
--- a/assets/js/dash/A11yHelper.js
+++ b/assets/js/dash/A11yHelper.js
@@ -132,4 +132,6 @@
     }
 }
 
-window.jvbA11y = new A11yHelper();
+document.addEventListener('DOMContentLoaded', function() {
+	window.jvbA11y = new A11yHelper();
+});
diff --git a/assets/js/dash/CRUD.js b/assets/js/dash/CRUD.js
index dfb568b..6780203 100644
--- a/assets/js/dash/CRUD.js
+++ b/assets/js/dash/CRUD.js
@@ -4,7 +4,6 @@
 class CRUDManager {
 	constructor(config) {
 		this.queue = window.jvbQueue;
-		console.log(this.queue);
 		this.config = config;
 		this.content = config.content || false;
 		this.settings = window.jvbUserSettings;
@@ -13,37 +12,71 @@
 			return;
 		}
 		this.isTimeline = false;
+		this.currentItemID = null;
 
 		this.initElements();
 		this.updateBulkOptions();
 
 		// Initialize components
-		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: 3600000,
-			cacheDOM: true
-		});
+		this.store = window.jvbStore.register(
+			this.content,
+			{
+				keyPath: 'id',
+				endpoint: 'content',
+				headers: {
+					'action_nonce': jvbSettings.dash,
+				},
+				indexes: [
+					{name: 'id', keyPath: 'id'},
+					{ name: 'status', keyPath: 'status'},
+					{ name: 'date', keyPath: 'date'},
+					{ name: 'modified', keyPath: 'modified'},
+					{ name: 'title', keyPath: 'title'}
+				],
+				filters: {
+					content: this.content,
+					user: jvbSettings.currentUser,
+					page: 1,
+					status: 'all',
+					orderby: 'modified', //or title
+					order: 'desc'
+				},
+				TTL: 30 * 60 * 1000,	//30 minutes cache
+			});
 
 		this.status = 'all';
 		this.filterTimeout = null;
 
 		this.viewController = new window.jvbViews(this.ui.container, this.store);
-		this.formController = new window.jvbForm();
+		this.tableForm = null;
+		this.tableChanges = new Map();
+
+
+		this.formController = (this.isTimeline) ? new window.jvbForm({collectFormData: () => this.collectTimelineData.bind(this)}) : new window.jvbForm();
+		this.viewController.subscribe((event, form) => {
+			if (event === 'table-view' && !this.tableForm) {
+				if (!this.tableForm) {
+					this.tableForm = this.formController.registerForm(form, {
+						autosave: false,
+						formStatus: false,
+						isTable: true,
+					});
+				}
+
+			} else if (event === 'not-table-view') {
+				if (this.tableForm) {
+
+				}
+			} else if (event === 'order-changed') {
+				let data = this.store.get(form);
+				if (!data) {
+					return;
+				}
+				let changes = {};
+				changes[form] = data;
+				this.savePosts(changes, `Updating progression order`);
+			}
+		});
 
 		this.formController.subscribe((event, data) => {
 			switch(event) {
@@ -54,15 +87,16 @@
 			}
 		});
 
-		if (window.jvbQueue) {
-			window.jvbQueue.subscribe((event, data) => {
-				if (event === 'operation-completed' && data.source === 'form') {
-					this.handleQueueSuccess(event, data);
-				} else if (event === 'operation-failed-permanent' && data.source === 'form') {
-					this.handleQueueFailure(event, data);
-				}
-			});
-		}
+		this.queue.subscribe((event, data) => {
+			if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'content') return;
+			console.log('Queue Subscription in CRUD.js: ', data);
+			if (event === 'operation-completed') {
+				this.handleQueueSuccess(event, data);
+			} else if (event === 'operation-failed-permanent') {
+				this.handleQueueFailure(event, data);
+			}
+		});
+
 
 		// Track initialization
 		this.initialized = false;
@@ -70,35 +104,32 @@
 		this.init();
 	}
 	handleFormChange(event, data) {
+		let title = data.fullData.post_title;
+		let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
+
+		let theChanges = {};
 		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`);
+			theChanges[this.currentItemID] = changes;
+			this.savePosts(theChanges, title);
 			return;
 		}
-		data.changes.content = this.content;
-		let changes = {};
-		let title = '';
+
 		let itemsToRemove = [];
 		switch (true) {
 			case data.config.element === this.ui.forms.edit:
-				let postID = data.config.id.replace('edit-', '');
-				console.log(postID);
-				changes[postID] = data.changes;
-				title = `Saving ${data.fullData['post_title']} Changes`;
+				theChanges[this.currentItemID] = changes;
+				title = `Saving ${title} Changes`;
 				// Check if status change requires removal
-				if (data.changes.post_status && this.shouldRemoveItem(data.changes.post_status)) {
-					itemsToRemove.push(postID);
+				if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
+					itemsToRemove.push(this.currentItemID);
 				}
 				break;
 			case data.config.element === this.ui.forms.bulkEdit:
 				let selected = data.config.element.querySelectorAll('.selected input:checked');
 				selected.forEach(sel => {
-					changes[sel.value] = data.changes;
+					theChanges[sel.value] = changes;
 					// Check if status change requires removal
-					if (data.changes.post_status && this.shouldRemoveItem(data.changes.post_status)) {
+					if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
 						itemsToRemove.push(sel.value);
 					}
 				});
@@ -107,8 +138,8 @@
 				break;
 			case data.config.element === this.ui.forms.create:
 				if (event === 'form-submit') {
-					changes[data.config.data['form-id']] = data.fullData;
-					title = `Saving ${data.fullData['post_title']} Changes`;
+					theChanges[data.config.data['form-id']] = changes;
+					title = `Saving ${title} Changes`;
 				}
 				break;
 		}
@@ -134,11 +165,11 @@
 			}
 		}
 
-		if (window.isEmptyObject(changes)) {
+		if (window.isEmptyObject(theChanges)) {
 			return;
 		}
 
-		this.savePosts(changes, title);
+		this.savePosts(theChanges, title);
 	}
 
 	shouldRemoveItem(newStatus) {
@@ -150,6 +181,13 @@
 		if (window.isEmptyObject(changes)) {
 			return;
 		}
+
+		//ensure content is in each post
+		for (let postId in changes) {
+			if (!changes[postId]['content']) {
+				changes[postId]['content'] = this.content;
+			}
+		}
 		let operation = {
 			endpoint: 'content',
 			headers: {
@@ -165,15 +203,15 @@
 		this.queue.addToQueue(operation);
 
 	}
-	handleQueueSuccess(event, data) {
-		console.log('Handling queue success...');
-		console.log('Event', event);
-		console.log('Data', data);
+	async handleQueueSuccess(event, data) {
+		this.store.clearCache();
+		this.store.clearHttpHeaders();
+		this.store.fetch();
 	}
 	handleQueueFailure(event, data) {
-		console.log('Handling queue failure...');
-		console.log('Event', event);
-		console.log('Data', data);
+		console.error('Operation failed permanently:', data);
+		// Optionally show error notification to user
+		this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
 	}
 
 	initElements() {
@@ -194,14 +232,11 @@
 			uploader: 'details.uploader'
 		};
 		this.ui = window.uiFromSelectors(this.elements);
-		this.isTimeline = Object.hasOwn(this.ui.forms.edit.dataset, 'timeline');
+		this.isTimeline = !!document.querySelector('[data-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');
 		});
 
@@ -219,9 +254,9 @@
 			this.modals[name].subscribe((event, data) => {
 				switch (event) {
 					case 'modal-close':
+						this.currentItemID = null;
 						this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
 						//double check we have finished saving
-						console.log('Data on modal close: ', data);
 						break;
 					case 'modal-open':
 						//probably not needed in this class
@@ -235,15 +270,6 @@
 
 		this.setupFilters();
 
-
-		this.queue.subscribe((event, data) => {
-			switch (event) {
-				case 'operation-status':
-					//update items?
-					break;
-			}
-		});
-
 		this.initialized = true;
 	}
 
@@ -347,6 +373,14 @@
 		});
 	}
 	handleChange(e) {
+		if (e.target.closest('[data-id]')) {
+			if (this.isTimeline) {
+				this.handleTimelineTableChange(e);
+			} else {
+				this.handleTableChange(e);
+			}
+			return;
+		}
 		if (e.target.classList.contains('bulk-action-select')) {
 			if (e.target.value.startsWith('tax-')) {
 				const taxonomy = e.target.value.replace('tax-', '');
@@ -378,6 +412,73 @@
 			}
 		}
 	}
+	handleTableChange(e) {
+		const row = e.target.closest('tr[data-id]');
+		if (!row) return;
+
+		const input = e.target;
+		const postID = parseInt(row.dataset.id);
+		const fieldName = input.closest(['data-field'])?.dataset.field;
+		if (!fieldName) return;
+
+		const item = this.store.get(postID);
+		if (!item) return;
+
+		item.fields[fieldName] = this.getInputValue(input);
+
+		this.store.save(item);
+
+		let post = {};
+		post[postID] = item.fields;
+		this.savePosts(post, `Saving changes to ${this.content}`);
+	}
+	handleTimelineTableChange(e) {
+		const tbody = e.target.closest('tbody[data-id]');
+		if (!tbody) return;
+
+		const input = e.target;
+		const fieldName = input.closest('[data-field]')?.dataset.field;
+
+		if (!fieldName) return;
+
+		const parentID = parseInt(tbody.dataset.id);
+		const timelinePoint = input.closest('tr.timeline-point');
+
+		const item = this.store.get(parentID);
+		if (!item) return;
+
+		const value = this.getInputValue(input);
+
+		// Check if this is a specific point, or a shared value
+		if (timelinePoint) {
+			const imgID = timelinePoint.dataset.imageId;
+			if (!item.fields.timeline) {
+				item.fields.timeline = {};
+			}
+			if (!item.fields.timeline[imgID]) {
+				item.fields.timeline[imgID] = {};
+			}
+			item.fields.timeline[imgID][fieldName] = value;
+		} else {
+			item.fields[fieldName] = value;
+		}
+
+		//Update store directly
+		this.store.save(item);
+
+		let changes = {};
+		changes[parentID] = item.fields;
+		this.savePosts(changes, 'Updating progress post');
+	}
+	getInputValue(input) {
+		if (input.type === 'checkbox') {
+			return input.checked ? (input.value || '1') : '';
+		}
+		if (input.type === 'radio') {
+			return input.checked ? input.value : null;
+		}
+		return input.value;
+	}
 
 	openTaxonomyModal(taxonomy) {
 		// Check if jvbSelector exists
@@ -393,14 +494,13 @@
 		);
 	}
 	handleBulkTaxonomy(selectedIds, taxonomy) {
-		console.log(taxonomy, selectedIds);
 		// Callback when terms are selected
 		if (selectedIds.length > 0) {
 			selectedIds = selectedIds.join(',');
 			let changes = {};
 			let selected = Array.from(this.viewController.selectedItems);
 
-			console.log('selected',selected);
+
 			selected.forEach(sel => {
 				changes[sel] = {
 					content: this.content
@@ -408,7 +508,6 @@
 				changes[sel][taxonomy] = selectedIds;
 			});
 
-			console.log('Taxonomy changes: ', changes);
 
 			let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
 			this.viewController.clearSelection();
@@ -420,7 +519,7 @@
 		if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
 			return;
 		}
-		console.log(`Setting status: ${status}`);
+
 		let changes = {};
 		for (let selected of this.viewController.selectedItems) {
 			changes[selected] = {
@@ -436,7 +535,7 @@
 			default:
 				title = window.uppercaseFirst(status)+'ing';
 		}
-		console.log(this.status);
+
 		if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) {
 			let delay = 0;
 			for (let selected of this.viewController.selectedItems) {
@@ -503,9 +602,8 @@
 
 		window.removeChildren(container);
 		for (let selected of this.viewController.selectedItems) {
-			console.log(selected);
 			let item = this.store.get(selected);
-			console.log(item);
+
 			const img = window.getTemplate('bulkItem');
 			if (!img) return;
 
@@ -533,14 +631,12 @@
 		];
 
 		this.formController.registerForm(this.ui.forms.bulkEdit);
-		console.log('Bulk Edit form registered');
 	}
 
 	populateEditForm(itemID) {
-		console.log(itemID);
+		this.currentItemID = itemID;
 
 		let item = this.store.get(parseInt(itemID));
-		console.log(item);
 		if (item) {
 			this.ui.modals.edit.dataset.itemID = itemID;
 			this.ui.modals.edit.dataset.content = this.content;
@@ -552,10 +648,8 @@
 				`Editing ${item.fields.post_title}`
 			];
 			form.dataset.formId = `edit-${itemID}`;
-			console.log(form.dataset.formId);
 			new window.jvbPopulate(form, item.fields, item.images);
 			this.formController.registerForm(this.ui.forms.edit);
-			console.log('Edit form registered');
 		}
 	}
 
@@ -593,7 +687,6 @@
 		document.querySelectorAll('[data-filter]').forEach(filter => {
 			filter.removeEventListener('change', this.filterHandler);
 		});
-		this.store.subscribers.clear();
 	}
 }
 
diff --git a/assets/js/dash/Modal.js b/assets/js/dash/Modal.js
index 346dc03..39d345f 100644
--- a/assets/js/dash/Modal.js
+++ b/assets/js/dash/Modal.js
@@ -51,6 +51,7 @@
 
 		this.a11y.announce(this.options.closeMessage);
 		this.modal.close();
+		this.isOpen = false;
 
 
 		// Remove this modal from the stack
diff --git a/assets/js/dash/TaxonomyCreator.js b/assets/js/dash/TaxonomyCreator.js
index 8f27b17..37b5960 100644
--- a/assets/js/dash/TaxonomyCreator.js
+++ b/assets/js/dash/TaxonomyCreator.js
@@ -8,19 +8,19 @@
 	constructor(selector) {
 		this.selector = selector;
 
-		// Get taxonomy from current active field config
-		this.taxonomy = selector.currentConfig?.taxonomy;
-		if (!this.taxonomy) {
-			console.error('TaxonomyCreator: No active field or taxonomy found');
-			return;
+		// Only initialize modal elements if modal exists
+		if (selector.modal) {
+			this.createNew = selector.modal.querySelector('.create-new-term');
+			this.toggle = selector.modal.querySelector('.new-term-toggle');
+			this.form = this.createNew?.querySelector('.create-new-term-section');
 		}
 
-		this.createNew = selector.modal.querySelector('.create-new-term');
-		this.toggle = selector.modal.querySelector('.new-term-toggle');
-		this.form = this.createNew.querySelector('.create-new-term-section');
-
 		this.initListeners();
-		this.initTermCreation();
+
+		// Only init term creation UI if we have modal elements
+		if (this.form) {
+			this.initTermCreation();
+		}
 	}
 
 	initListeners() {
@@ -47,6 +47,9 @@
 	}
 
 	async handleTermCreation(e) {
+		const taxonomy = this.selector.currentConfig?.taxonomy;
+		if (!taxonomy) return;
+
 		const termName = this.form.querySelector('input[name="term_name"]').value.trim();
 		const parentId = parseInt(this.form.querySelector('input#select_parent')?.value) || 0;
 
@@ -54,98 +57,127 @@
 
 		try {
 			this.form.querySelector('button').disabled = true;
-			const response = await this.createTerm(termName, parentId);
+			const response = await this.createTerm(termName, parentId, taxonomy);
 
 			if (response.success && response.term) {
 				let term = response.term;
 
-				// Close the create new section
 				this.createNew.open = false;
+				await this.selector.store.clearCache();
 
-				// Invalidate the cache for this taxonomy
-				await this.selector.store.invalidate({ taxonomy: this.taxonomy });
+				// Add to store's data BEFORE display update
+				this.selector.store.data.set(term.id, {
+					id: term.id,
+					name: term.name,
+					path: termPath,
+					taxonomy: field.taxonomy,
+					parent: 0,
+					count: 0,
+					hasChildren: false,
+					slug: term.slug || termName.toLowerCase().replace(/\s+/g, '-')
+				});
 
-				// Add to current modal selection
 				this.selector.addSelectedTermToModal(term.id, term.name, term.path || term.name);
 
-				// If we're viewing the parent category where this was created, refresh the list
 				const currentParent = this.selector.store.filters.parent || 0;
 				if (currentParent === parentId) {
 					await this.selector.store.setFilters({
-						taxonomy: this.taxonomy,
+						taxonomy,
 						parent: parentId,
 						page: 1,
 						search: ''
 					});
 				}
 
-				// Clear the form
 				this.form.querySelector('input[name="term_name"]').value = '';
-
-				// Clear suggestions
 				const suggestionContainer = this.createNew.querySelector('.term-suggestions');
 				if (suggestionContainer) {
 					suggestionContainer.hidden = true;
 				}
+				this.selector.store.cache.clear();
 			}
 		} catch (error) {
 			console.error('Error creating term:', error);
 			this.selector.error?.log(error, {
 				component: 'TaxonomyCreator',
 				action: 'handleTermCreation'
-			}) || console.error('Failed to create term');
+			});
 		} finally {
 			this.form.querySelector('button').disabled = false;
 		}
 	}
 
 	async handleAutocompleteCreate(e) {
-		const button = e.target;
+		const button = e.target.closest('.create-term');
 		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;
 
+		const originalHTML = button.innerHTML;
+
 		try {
 			button.disabled = true;
-			const originalText = button.innerHTML;
 			button.textContent = 'Creating...';
 
-			const response = await this.createTerm(termName, 0);
+			const response = await this.createTerm(termName, 0, field.taxonomy);
 
 			if (response.success && response.term) {
 				const term = response.term;
+				const termPath = term.path || term.name;
 
-				// Add term to field
 				field.selectedTerms.add(parseInt(term.id));
-				this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name);
+				// Add to store's data BEFORE display update
+				this.selector.store.data.set(term.id, {
+					id: term.id,
+					name: term.name,
+					path: termPath,
+					taxonomy: field.taxonomy,
+					parent: 0,
+					count: 0,
+					hasChildren: false,
+					slug: term.slug || termName.toLowerCase().replace(/\s+/g, '-')
+				});
+				this.selector.addTermToDisplay(field.id, term.id, term.name, termPath);
 
-				// 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;
+				this.selector.store.clearCache();
+				await this.selector.store.setFilters({
+					taxonomy: field.taxonomy,
+					page: 1,
+					search: '',
+					parent: 0
+				});
+			} else if (response.reason === 'exists' && response.term) {
+				// Term already exists - just add it
+				const term = response.term;
+				field.selectedTerms.add(parseInt(term.id));
+				this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name);
+
+				field.input.value = Array.from(field.selectedTerms).join(',');
+				field.input.dispatchEvent(new Event('change', { bubbles: true }));
+
+				field.autocompleteDropdown.hidden = true;
+				if (input) input.value = '';
 			}
 		} catch (error) {
 			console.error('Error creating term:', error);
-			button.textContent = 'Failed - Try again';
-			setTimeout(() => {
-				button.innerHTML = `<strong>Create:</strong> "${termName}"`;
-				button.disabled = false;
-			}, 2000);
+			button.innerHTML = originalHTML;
+			button.disabled = false;
+			this.selector.error?.log(error, {
+				component: 'TaxonomyCreator',
+				action: 'handleAutocompleteCreate'
+			});
 		}
 	}
 
@@ -161,6 +193,9 @@
 	}
 
 	resetParentOptions() {
+		const taxonomy = this.selector.currentConfig?.taxonomy;
+		if (!taxonomy) return;
+
 		let select = this.createNew.querySelector('#select_parent');
 		if (!select) return;
 
@@ -188,7 +223,7 @@
 		// Add all terms currently visible in the taxonomy (from store cache)
 		const visibleTerms = [];
 		this.selector.store.data.forEach(term => {
-			if (term.taxonomy === this.taxonomy && term.parent === currentParent) {
+			if (term.taxonomy === taxonomy && term.parent === currentParent) {
 				visibleTerms.push(term);
 			}
 		});
@@ -206,45 +241,32 @@
 		});
 	}
 
-	async createTerm(name, parent = 0) {
-		let loadingMessage = this.createNew.querySelector('.loading-message.create-term');
-		let text = loadingMessage?.querySelector('span');
-
+	async createTerm(name, parent = 0, taxonomy) {
 		try {
-			if (loadingMessage) {
-				loadingMessage.hidden = false;
-			}
+			// Search for the exact term first
+			await this.selector.store.setFilters({
+				taxonomy: taxonomy,
+				search: name,
+				page: 1,
+				parent: 0
+			});
 
-			if (text && window.typeText) {
-				window.typeText(text, 'Checking term...');
-			} else if (text) {
-				text.textContent = 'Checking term...';
-			}
+			// Check if exact match exists in results
+			const exactMatch = Array.from(this.selector.store.data.values())
+				.find(term =>
+					term.taxonomy === taxonomy &&
+					term.name.toLowerCase() === name.toLowerCase()
+				);
 
-			// Search for existing terms with this name
-			const searchResults = await this.searchExistingTerms(name);
-
-			// Check for exact matches
-			const exactMatches = searchResults.filter(term =>
-				term.name.toLowerCase() === name.toLowerCase()
-			);
-
-			if (exactMatches.length > 0) {
-				this.showTermSuggestions(exactMatches, true);
-				return { success: false, reason: 'exists' };
-			}
-
-			// Show similar terms if found
-			if (searchResults.length > 0) {
-				this.showTermSuggestions(searchResults, false);
-				return { success: false, reason: 'similar' };
+			if (exactMatch) {
+				// For modal context, show suggestions
+				if (this.createNew) {
+					this.showTermSuggestions([exactMatch], true);
+				}
+				return { success: false, reason: 'exists', term: exactMatch };
 			}
 
 			// Term doesn't exist, create it
-			if (text) {
-				text.textContent = 'Creating term...';
-			}
-
 			const response = await fetch(`${jvbSettings.api}terms`, {
 				method: 'POST',
 				headers: {
@@ -252,7 +274,7 @@
 					'X-WP-Nonce': jvbSettings.nonce
 				},
 				body: JSON.stringify({
-					taxonomy: this.taxonomy,
+					taxonomy: taxonomy,
 					name: name,
 					parent: parent
 				})
@@ -267,18 +289,13 @@
 		} catch (error) {
 			console.error('Error creating term:', error);
 			throw error;
-		} finally {
-			this.form.querySelector('button').disabled = false;
-			if (loadingMessage) {
-				loadingMessage.hidden = true;
-			}
 		}
 	}
 
 	/**
 	 * Search for existing terms using the store
 	 */
-	async searchExistingTerms(searchQuery) {
+	async searchExistingTerms(searchQuery, taxonomy) {
 		return new Promise((resolve) => {
 			// Set up a one-time listener for the search results
 			const handleSearchResults = (event, data) => {
@@ -292,7 +309,7 @@
 
 			// Trigger search
 			this.selector.store.setFilters({
-				taxonomy: this.taxonomy,
+				taxonomy: taxonomy,
 				search: searchQuery,
 				page: 1,
 				parent: 0
@@ -365,71 +382,6 @@
 		return container;
 	}
 
-	/**
-	 * Create "Create new term" option for autocomplete dropdown
-	 */
-	createAutocompleteOption(query, field) {
-		const button = document.createElement('button');
-		button.type = 'button';
-		button.className = 'autocomplete-item create-term';
-		button.innerHTML = `<span>Create "${query}"</span>`;
-		button.dataset.query = query;
-		button.dataset.fieldId = field.id;
-
-		button.addEventListener('click', async () => {
-			await this.handleAutocompleteCreate(button, query, field);
-		});
-
-		return button;
-	}
-
-
-	/**
-	 * Handle term creation from autocomplete
-	 */
-	async handleAutocompleteCreate(button, termName, field) {
-		if (!field) return;
-
-		const originalHTML = button.innerHTML;
-
-		try {
-			button.disabled = true;
-			button.innerHTML = '<span>Creating...</span>';
-
-			const parentId = 0; // Autocomplete always creates at root level
-			const result = await this.createTerm(termName, parentId);
-
-			if (result.success && result.term) {
-				const term = result.term;
-
-				// Add 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 }));
-
-				// Invalidate cache
-				await this.selector.store.invalidate({ taxonomy: field.taxonomy });
-
-				// Clear and hide dropdown
-				field.autocompleteDropdown.hidden = true;
-				const input = field.container.querySelector('input[data-autocomplete]');
-				if (input) input.value = '';
-			}
-			// If result.success is false, suggestions are already shown
-
-		} catch (error) {
-			console.error('Error creating term:', error);
-			button.innerHTML = originalHTML;
-			button.disabled = false;
-			this.selector.error?.log(error, {
-				component: 'TaxonomyCreator',
-				action: 'handleAutocompleteCreate'
-			});
-		}
-	}
 
 	/**
 	 * Clean up when modal closes
diff --git a/assets/js/dash/UtilityFunctions.js b/assets/js/dash/UtilityFunctions.js
index 23f0737..596d6f4 100644
--- a/assets/js/dash/UtilityFunctions.js
+++ b/assets/js/dash/UtilityFunctions.js
@@ -127,7 +127,7 @@
  */
 window.getTemplate = function (template){
 	if (window.templates.size === 0) {
-		loadTemplates();
+		window.loadTemplates();
 	}
 	if(window.templates.has(template)){
 		return window.templates.get(template).cloneNode(true);
@@ -189,29 +189,25 @@
  * @param icon
  * @returns {Node | ActiveX.IXMLDOMNode}
  */
-window.icon = document.querySelector('.icon').cloneNode(true);
-window.icon.className = 'icon';
-window.getIcon = function getIcon(icon){
+window.icon = null;
+window.getIcon = function getIcon(icon, style = ''){
 	if (typeof icon === 'undefined') {
 		return '';
 	}
-	console.log('Getting icon: '+icon);
 
+	if (!window.icon) {
+		window.icon = document.createElement('i');
+		window.icon.className = 'icon';
+		window.icon.ariaHidden = true;
+	}
 	let theIcon = window.icon.cloneNode(true);
-	theIcon.classList.add('icon-'+icon);
+	if (style !== '' && ['regular', 'bold', 'duotone', 'fill', 'light', 'thin' ].includes('style')) {
+		style = `-${style.slice(0, 2)}`;
+	} else {
+		style = '';
+	}
+	theIcon.classList.add(`icon-${icon}${style}`);
 	return theIcon;
-
-
-	if(!window.jvbIcons){
-		window.jvbIcons = new Map();
-	}
-	if(!window.jvbIcons.has(icon) && jvbSettings.icons[icon]){
-		let temp = document.createElement('div');
-		temp.innerHTML = jvbSettings.icons[icon];
-		window.jvbIcons.set(icon, temp.firstElementChild.cloneNode(true));
-		temp.remove();
-	}
-	return window.jvbIcons.get(icon)?.cloneNode(true);
 }
 
 /**
@@ -535,6 +531,9 @@
 }
 
 window.targetCheck = function (e, selector) {
+	if (Array.isArray(selector)) {
+		selector = selector.join(',');
+	}
 	if (typeof selector !== 'string') {
 		return false;
 	}
diff --git a/assets/js/min/a11y.min.js b/assets/js/min/a11y.min.js
index 4b339dd..e4c0032 100644
--- a/assets/js/min/a11y.min.js
+++ b/assets/js/min/a11y.min.js
@@ -1 +1 @@
-window.jvbA11y=new class{constructor(){this.liveRegion=document.querySelector(".screen-reader-text.live-region"),this.liveRegion||console.log("No accessibility region.")}announce(e,t="polite"){this.liveRegion&&(this.liveRegion.setAttribute("aria-live",t),this.liveRegion.textContent="",setTimeout((()=>{this.liveRegion.textContent=e}),50))}announceItems(e,t=!1,n=!0){const o=t?"Added":"Loaded",i=n?". More available to load.":". No more available to load.";this.announce(`${o} ${e} new items${i}`)}announceEmpty(e=!1){const t=e?"No favourites found. Try adding some items to your collection.":"No items found matching your current filters.";this.announce(t)}announceNavigation(e,t,n=!1,o=!1){n?this.announce("At first image"):o?this.announce("At last image"):this.announce(`Image ${e} of ${t}`)}trapFocus(e,t=null){if(!e)return()=>{};const n=e.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');if(!n.length)return()=>{};const o=n[0],i=n[n.length-1],a=t||document.activeElement;o.focus();const r=e=>{"Tab"===e.key&&(e.shiftKey&&document.activeElement===o?(i.focus(),e.preventDefault()):e.shiftKey||document.activeElement!==i||(o.focus(),e.preventDefault()))};return e.addEventListener("keydown",r),()=>{e.removeEventListener("keydown",r),a&&"function"==typeof a.focus&&a.focus()}}makeNavigable(e,t){e&&e.length&&Array.from(e).forEach((e=>{e.getAttribute("data-keyboard-nav")||(e.setAttribute("data-keyboard-nav","true"),e.setAttribute("tabindex","0"),e.addEventListener("keydown",(n=>{if("Enter"===n.key||" "===n.key)if(n.preventDefault(),t)t(e);else{const t=e.querySelector("a");t&&t.click()}})))}))}};
\ No newline at end of file
+(()=>{class e{constructor(){this.liveRegion=document.querySelector(".screen-reader-text.live-region"),this.liveRegion||console.log("No accessibility region.")}announce(e,t="polite"){this.liveRegion&&(this.liveRegion.setAttribute("aria-live",t),this.liveRegion.textContent="",setTimeout((()=>{this.liveRegion.textContent=e}),50))}announceItems(e,t=!1,n=!0){const o=t?"Added":"Loaded",i=n?". More available to load.":". No more available to load.";this.announce(`${o} ${e} new items${i}`)}announceEmpty(e=!1){const t=e?"No favourites found. Try adding some items to your collection.":"No items found matching your current filters.";this.announce(t)}announceNavigation(e,t,n=!1,o=!1){n?this.announce("At first image"):o?this.announce("At last image"):this.announce(`Image ${e} of ${t}`)}trapFocus(e,t=null){if(!e)return()=>{};const n=e.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');if(!n.length)return()=>{};const o=n[0],i=n[n.length-1],a=t||document.activeElement;o.focus();const r=e=>{"Tab"===e.key&&(e.shiftKey&&document.activeElement===o?(i.focus(),e.preventDefault()):e.shiftKey||document.activeElement!==i||(o.focus(),e.preventDefault()))};return e.addEventListener("keydown",r),()=>{e.removeEventListener("keydown",r),a&&"function"==typeof a.focus&&a.focus()}}makeNavigable(e,t){e&&e.length&&Array.from(e).forEach((e=>{e.getAttribute("data-keyboard-nav")||(e.setAttribute("data-keyboard-nav","true"),e.setAttribute("tabindex","0"),e.addEventListener("keydown",(n=>{if("Enter"===n.key||" "===n.key)if(n.preventDefault(),t)t(e);else{const t=e.querySelector("a");t&&t.click()}})))}))}}document.addEventListener("DOMContentLoaded",(function(){window.jvbA11y=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/creator.min.js b/assets/js/min/creator.min.js
index b5672e4..056e9f3 100644
--- a/assets/js/min/creator.min.js
+++ b/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),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)}};
\ No newline at end of file
+window.jvbTaxCreator=class{constructor(e){this.selector=e,e.modal&&(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.form&&this.initTermCreation()}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.selector.currentConfig?.taxonomy;if(!t)return;const r=this.form.querySelector('input[name="term_name"]').value.trim(),s=parseInt(this.form.querySelector("input#select_parent")?.value)||0;if(r)try{this.form.querySelector("button").disabled=!0;const e=await this.createTerm(r,s,t);if(e.success&&e.term){let o=e.term;this.createNew.open=!1,await this.selector.store.clearCache(),this.selector.store.data.set(o.id,{id:o.id,name:o.name,path:termPath,taxonomy:field.taxonomy,parent:0,count:0,hasChildren:!1,slug:o.slug||r.toLowerCase().replace(/\s+/g,"-")}),this.selector.addSelectedTermToModal(o.id,o.name,o.path||o.name),(this.selector.store.filters.parent||0)===s&&await this.selector.store.setFilters({taxonomy:t,parent:s,page:1,search:""}),this.form.querySelector('input[name="term_name"]').value="";const a=this.createNew.querySelector(".term-suggestions");a&&(a.hidden=!0),this.selector.store.cache.clear()}}catch(e){console.error("Error creating term:",e),this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleTermCreation"})}finally{this.form.querySelector("button").disabled=!1}}async handleAutocompleteCreate(e){const t=e.target.closest(".create-term"),r=this.selector.getFieldId(t),s=this.selector.fields.get(r);if(!s)return;const o=s.container.querySelector("input[data-autocomplete]"),a=o?.value.trim()||t.dataset.query;if(!a)return;const n=t.innerHTML;try{t.disabled=!0,t.textContent="Creating...";const e=await this.createTerm(a,0,s.taxonomy);if(e.success&&e.term){const t=e.term,r=t.path||t.name;s.selectedTerms.add(parseInt(t.id)),this.selector.store.data.set(t.id,{id:t.id,name:t.name,path:r,taxonomy:s.taxonomy,parent:0,count:0,hasChildren:!1,slug:t.slug||a.toLowerCase().replace(/\s+/g,"-")}),this.selector.addTermToDisplay(s.id,t.id,t.name,r),s.input.value=Array.from(s.selectedTerms).join(","),s.input.dispatchEvent(new Event("change",{bubbles:!0})),s.autocompleteDropdown.hidden=!0,o&&(o.value=""),this.selector.store.clearCache(),await this.selector.store.setFilters({taxonomy:s.taxonomy,page:1,search:"",parent:0})}else if("exists"===e.reason&&e.term){const t=e.term;s.selectedTerms.add(parseInt(t.id)),this.selector.addTermToDisplay(s.id,t.id,t.name,t.path||t.name),s.input.value=Array.from(s.selectedTerms).join(","),s.input.dispatchEvent(new Event("change",{bubbles:!0})),s.autocompleteDropdown.hidden=!0,o&&(o.value="")}}catch(e){console.error("Error creating term:",e),t.innerHTML=n,t.disabled=!1,this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleAutocompleteCreate"})}}initTermCreation(){this.form&&this.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}resetParentOptions(){const e=this.selector.currentConfig?.taxonomy;if(!e)return;let t=this.createNew.querySelector("#select_parent");if(!t)return;let r=t.querySelector("option");if(!r)return;window.removeChildren(t),t.append(r.cloneNode(!0));const s=this.selector.store.filters.parent||0;if(0!==s){const e=this.selector.store.data.get(s);if(e){let s=r.cloneNode(!0);s.value=e.id,s.textContent=e.name,t.append(s)}}const o=[];this.selector.store.data.forEach((t=>{t.taxonomy===e&&t.parent===s&&o.push(t)})),o.sort(((e,t)=>e.name.localeCompare(t.name))),o.forEach((e=>{let s=r.cloneNode(!0);s.id=`select-parent-${e.id}`,s.value=e.id,s.textContent="  — "+e.name,t.append(s)}))}async createTerm(e,t=0,r){try{await this.selector.store.setFilters({taxonomy:r,search:e,page:1,parent:0});const s=Array.from(this.selector.store.data.values()).find((t=>t.taxonomy===r&&t.name.toLowerCase()===e.toLowerCase()));if(s)return this.createNew&&this.showTermSuggestions([s],!0),{success:!1,reason:"exists",term:s};const o=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({taxonomy:r,name:e,parent:t})});if(!o.ok)throw new Error(`Server error: ${o.status}`);return await o.json()}catch(e){throw console.error("Error creating term:",e),e}}async searchExistingTerms(e,t){return new Promise((r=>{const s=(e,t)=>{"data-loaded"===e&&(this.selector.store.unsubscribe(s),r(t.data?.items||[]))};this.selector.store.subscribe(s),this.selector.store.setFilters({taxonomy:t,search:e,page:1,parent:0})}))}showTermSuggestions(e,t=!1){const r=this.createNew.querySelector(".term-suggestions")||this.createSuggestionContainer();window.removeChildren(r);const s=document.createElement("h4");s.textContent=t?"This term already exists:":"Similar terms already exist:",r.appendChild(s);const o=document.createElement("ul");o.className="term-suggestion-list",e.forEach((e=>{const t=document.createElement("li"),s=document.createElement("button");s.type="button",s.className="use-existing-term",s.setAttribute("data-id",e.id),s.textContent=e.path||e.name,s.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(s),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}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)}};
\ No newline at end of file
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index ce4c697..3b0a07f 100644
--- a/assets/js/min/crud.min.js
+++ b/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.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}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e){this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.content&&(this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions(),this.store=window.jvbStore.register(this.content,{keyPath:"id",endpoint:"content",headers:{action_nonce:jvbSettings.dash},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let i={};i[t]=e,this.savePosts(i,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"content"===t.endpoint&&(console.log("Queue Subscription in CRUD.js: ",t),"operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init())}handleFormChange(e,t){let i=t.fullData.post_title,s=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=s,void this.savePosts(l,i);let o=[];switch(!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=s,i=`Saving ${i} Changes`,s.post_status&&this.shouldRemoveItem(s.post_status)&&o.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let a=t.config.element.querySelectorAll(".selected input:checked");a.forEach((e=>{l[e.value]=s,s.post_status&&this.shouldRemoveItem(s.post_status)&&o.push(e.value)})),i=`Updating ${a.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=s,i=`Saving ${i} 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(l)||this.savePosts(l,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;for(let t in e)e[t].content||(e[t].content=this.content);let i={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(i)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}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=!!document.querySelector("[data-timeline]")}init(){this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{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,i)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),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 i=t.dataset.action,s=t.dataset.id;switch(i){case"edit":this.populateEditForm(s),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(s)}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 i=Array.from(this.viewController.selectedItems);i.length>0&&confirm(`Delete ${i.length} items?`)&&(i.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.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else 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")}}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const i=e.target,s=parseInt(t.dataset.id),l=i.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(s);if(!o)return;o.fields[l]=this.getInputValue(i),this.store.save(o);let a={};a[s]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const i=e.target,s=i.closest("[data-field]")?.dataset.field;if(!s)return;const l=parseInt(t.dataset.id),o=i.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(i);if(o){const e=o.dataset.imageId;a.fields.timeline||(a.fields.timeline={}),a.fields.timeline[e]||(a.fields.timeline[e]={}),a.fields.timeline[e][s]=n}else a.fields[s]=n;this.store.save(a);let r={};r[l]=a.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let i={},s=Array.from(this.viewController.selectedItems);s.forEach((s=>{i[s]={content:this.content},i[s][t]=e}));let l=`Adding ${s.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(i,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,i={};for(let t of this.viewController.selectedItems)i[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("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(i)||this.savePosts(i,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,i=t.dataset.filter;if("taxonomies"===i){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,i.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){let i=this.store.get(t);const s=window.getTemplate("bulkItem");if(!s)return;const l=s.querySelector("input[type=checkbox]"),o=s.querySelector("img");l&&(l.id=`bulk_${i.id}`,l.value=i.id,l.checked=!0),o&&i.thumbnail&&(o.src=i.thumbnail,o.alt=i.alt||""),e.append(s)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemID=e,this.ui.modals.edit.dataset.content=this.content;let i=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],i.dataset.formId=`edit-${e}`,new window.jvbPopulate(i,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit)}}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)}))}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 6ddf722..a84b6e9 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-window.jvbStore=class{constructor(e={}){this.config={name:"default",version:1,storeName:"items",keyPath:"id",indexes:[],endpoint:null,saveToServer:!1,apiBase:jvbSettings.api,headers:{},filters:{},required:null,icon:null,getBlobs:null,TTL:36e5,useHttpCaching:!0,cacheKeyStrategy:"filters",showLoading:!0,stripDOMReferences:!0,storeBlobs:!1,...e},this.db=null,this.data=new Map,this.cache=new Map,this.isFetching=!1,this.pendingFetch=null,this.httpHeaders=new Map,this.subscribers=new Set,this.currentRequest=null,this.filters=this.config.filters??{},this.headers={"X-WP-Nonce":jvbSettings?.nonce,...this.config.headers},this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.initDB(),window.addEventListener("beforeunload",(()=>this.destroy()))}async initDB(){if(!("indexedDB"in window))return void console.warn("IndexedDB not supported");const e=`jvb_${this.config.name}_db`,t=indexedDB.open(e,this.config.version);t.onupgradeneeded=e=>{const t=e.target.result;if(!t.objectStoreNames.contains(this.config.storeName)){const e=t.createObjectStore(this.config.storeName,{keyPath:this.config.keyPath});this.config.indexes.forEach((t=>{e.createIndex(t.name,t.keyPath||t.name,{unique:t.unique||!1})}))}if(this.config.endpoint&&!t.objectStoreNames.contains("cache")){const e=t.createObjectStore("cache",{keyPath:"key"});e.createIndex("timestamp","timestamp",{unique:!1}),e.createIndex("endpoint","endpoint",{unique:!1}),e.createIndex("filters","filters",{unique:!1})}this.config.useHttpCaching&&!t.objectStoreNames.contains("headers")&&t.createObjectStore("headers",{keyPath:"key"}),this.config.storeBlobs&&!t.objectStoreNames.contains("blobs")&&t.createObjectStore("blobs",{keyPath:"uploadId"}),this.config.onUpgrade&&this.config.onUpgrade(t,e.oldVersion,e.newVersion)},t.onsuccess=async e=>{this.db=e.target.result;const t=[this.loadFromDB()];this.db.objectStoreNames.contains("cache")&&t.push(this.loadCache()),this.config.useHttpCaching&&this.db.objectStoreNames.contains("headers")&&t.push(this.loadHeaders()),await Promise.all(t),this.notify("db-init"),this.config.endpoint&&this.fetch()},t.onerror=t=>{console.error(`IndexedDB error for ${e}:`,t),this.config.onError&&this.config.onError(t)}}async loadFromDB(){if(this.db)return new Promise((async(e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).getAll();s.onsuccess=async t=>{const s=t.target.result;for(const e of s){e.data?._isFormData&&this.config.getBlobs&&(e.data=await this.objectToFormData(e.data));const t=this.getItemKey(e);this.data.set(t,e)}this.notify("data-loaded",{count:s.length}),e(s)},s.onerror=e=>t(e)}))}async loadData(){if(this.db)return new Promise(((e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).getAll();s.onsuccess=t=>{t.target.result.forEach((e=>{const t=this.config.stripDOMReferences?this.stripDOMReferences(e):e,s=this.getItemKey(t);this.data.set(s,t)})),e()},s.onerror=e=>t(e)}))}stripDOMReferences(e){if(!e||"object"!=typeof e)return e;if(Array.isArray(e))return e.map((e=>this.stripDOMReferences(e)));const t={};for(const[s,i]of Object.entries(e))this.isDOMReference(s,i)||(i instanceof Set?t[s]=Array.from(i):i instanceof Map?t[s]=Object.fromEntries(i):t[s]="object"==typeof i&&null!==i?this.stripDOMReferences(i):i);return t}isDOMReference(e,t){if(t instanceof HTMLElement||t instanceof NodeList||t instanceof HTMLCollection||t&&void 0!==t.nodeType)return!0;const s=["element","el","dom","node","ui","container","wrapper"],i=e.toLowerCase();return!(!s.includes(i)&&!s.some((e=>i===e||i.startsWith(e+"_")||i.endsWith("_"+e))))}getItemKey(e){if("function"==typeof this.config.keyPath)return this.config.keyPath(e);const t=this.config.keyPath.split(".");let s=e;for(const e of t)s=s?.[e];return s}async save(e){const t=this.getItemKey(e);this.data.set(t,e);let s={...e};return s.data instanceof FormData&&(s.data=this.formDataToObject(s.data)),this.config.stripDOMReferences&&(s=this.stripDOMReferences(s)),await this.saveToDB(s),this.config.endpoint&&this.saveToServer(e),this.notify("item-saved",{item:s,key:t}),s}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,i]of e.entries())i instanceof File||i instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(i)):t.entries[s]=i);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,i]of Object.entries(e.entries))Array.isArray(i)?i.forEach((e=>t.append(s,e))):t.append(s,i);if(this.config.getBlobs&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids),i=await this.config.getBlobs(s);for(const e of i)if(e){const s=new File([e.data],e.name,{type:e.type,lastModified:e.lastModified});t.append("files[]",s)}}return t}async saveToDB(e){if(this.db)return new Promise(((t,s)=>{const i=this.db.transaction([this.config.storeName],"readwrite").objectStore(this.config.storeName).put(e);i.onsuccess=()=>t(),i.onerror=e=>s(e)}))}async saveMany(e){if(!this.db)return;const t=this.db.transaction([this.config.storeName],"readwrite").objectStore(this.config.storeName),s=e.map((e=>{const s=this.config.stripDOMReferences?this.stripDOMReferences(e):e,i=this.getItemKey(s);return this.data.set(i,s),t.put(s)}));await Promise.all(s),this.notify("items-saved",{count:e.length})}get(e){return this.data.get(e)}getAll(){return Array.from(this.data.values())}async delete(e,t=null){if(this.data.delete(e),t||(t=this.config.storeName),this.db){const s=this.db.transaction([t],"readwrite").objectStore(t);await s.delete(e)}this.notify("item-deleted",{key:e})}async saveBlob(e,t){if(!this.db)return;const s=this.db.transaction(["blobs"],"readwrite").objectStore("blobs");await s.put({uploadId:e,data:t,type:t.type,name:t.name,lastModified:t.lastModified||Date.now()})}async getBlob(e){return this.db?new Promise((t=>{const s=this.db.transaction(["blobs"],"readonly").objectStore("blobs").get(e);s.onsuccess=()=>t(s.result),s.onerror=()=>t(null)})):null}async clear(){if(this.data.clear(),this.cache.clear(),this.httpHeaders.clear(),this.domCache&&this.domCache.clear(),this.db){const e=[this.config.storeName];this.config.endpoint&&e.push("cache"),this.config.useHttpCaching&&e.push("headers");const t=this.db.transaction(e,"readwrite");e.forEach((e=>{this.db.objectStoreNames.contains(e)&&t.objectStore(e).clear()}))}this.notify("data-cleared")}async fetch(e={}){if(!this.config.endpoint)throw new Error("No endpoint configured for fetch");const{filters:t=this.filters,headers:s={}}=e;if(this.config.required&&""===this.filters[this.config.required])return void console.log(this.config.storeName+": Not fetch as we don't have the required items");const i=this.generateCacheKey(t);if(console.log("CacheKey: ",i),this.isFetching&&this.currentCacheKey===i)return new Promise((e=>{this.pendingFetches||(this.pendingFetches=[]),this.pendingFetches.push(e)}));this.isFetching=!0,this.currentCacheKey=i;let n=null;this.config.showLoading&&this.setLoading(!0);const o=this.cache.get(i);if(console.log("Cached Data: ",o),o&&this.isCacheValid(o))return console.log("Returning cached data: "),this.isFetching=!1,this.currentCacheKey=null,this.config.showLoading&&this.setLoading(!1),o.data;const r={...this.headers,...s};if(this.config.useHttpCaching){const e=this.httpHeaders.get(i);e&&(e.etag&&(r["If-None-Match"]=e.etag),e.lastModified&&(r["If-Modified-Since"]=e.lastModified))}const a=this.cleanFilters(t),c=new URLSearchParams(a),h=`${this.config.apiBase}${this.config.endpoint}${c.toString()?"?"+c:""}`;try{const e=await fetch(h,{method:"GET",headers:r});if(304===e.status&&o)return o.timestamp=Date.now(),o.fromCache=!0,o.isError=!1,this.saveCache(i,o),console.log(this.config.storeName+" Data loaded from cache"),this.notify("data-loaded",o),n=o.data,o.data;if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);const s=await e.json();this.config.useHttpCaching&&this.storeResponseHeaders(i,e);const a={key:i,data:s,timestamp:Date.now(),endpoint:this.config.endpoint,filters:t};console.log(this.config.storeName+"Fetched fresh from server"),this.cache.set(i,a),this.saveCache(i,a);let c=Array.isArray(s)?s:s.items;return await this.saveMany(c),this.notify("data-loaded",{data:{items:c,...s},count:c.length,filters:t,fromCache:!1,isError:!1}),n=s,s}catch(e){if(console.error("Fetch error:",e),o)return console.warn("Using stale cache due to fetch error"),o.isError=!0,this.notify("data-loaded",o),n=o.data,o.data;throw e}finally{this.config.showLoading&&this.setLoading(!1),this.isFetching=!1,this.currentCacheKey=null,this.pendingFetches&&this.pendingFetches.length>0&&(this.pendingFetches.forEach((e=>e(n))),this.pendingFetches=[])}}async saveToServer(e){if(!this.config.saveToServer||!jvbSettings.currentUser)return;if(!this.config.endpoint&&this.config.saveToServer)throw new Error("No endpoint configured for saving to server");let t,s=this.config.headers;s["X-WP-Nonce"]=jvbSettings.nonce,e instanceof FormData?(e.append("user",jvbSettings.currentUser),t=e):(t=JSON.stringify({...e,user:jvbSettings.currentUser}),s["Content-Type"]="application/json");const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:s,body:t}),n=await i.json();this.notify("saved-to-server",{success:n.ok&&n.success})}cleanFilters(e){const t={};return Object.entries(e).forEach((([e,s])=>{null!=s&&""!==s&&("taxonomies"===e&&"object"==typeof s?Object.entries(s).forEach((([e,s])=>{Array.isArray(s)&&s.length>0?t[`tax_${e}`]=s.join(","):s&&(t[`tax_${e}`]=s)})):"date"===e&&"object"==typeof s?(s.after&&(t.after=s.after),s.before&&(t.before=s.before)):t[e]=s)})),t}generateCacheKey(e){if("custom"===this.config.cacheKeyStrategy&&this.config.generateCacheKey)return this.config.generateCacheKey(e);const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}setFilter(e,t){this.filters||(this.filters={});const s=this.filters[e];s!==t&&(""===t||null==t?delete this.filters[e]:this.filters[e]=t,this.notify("filters-changed",{filters:this.filters,changed:{key:e,oldValue:s,newValue:t}}),this.config.endpoint&&window.debouncer.schedule(this.config.endpoint,this.fetch.bind(this),100))}removeFilter(e){const t=this.filters[e];void 0!==t&&(delete this.filters[e],this.notify("filters-changed",{filters:this.filters,removed:{key:e,oldValue:t}}),this.config.endpoint&&window.debouncer.schedule(this.config.endpoint,this.fetch.bind(this),100))}clearFilters(){const e={...this.filters};this.filters=this.config.filters,this.notify("filters-cleared",{oldFilters:e,filters:this.filters}),this.config.endpoint&&this.fetch()}async setFilters(e){Object.keys(e).some((t=>this.filters[t]!==e[t]))&&(this.filters={...this.filters,...e},this.notify("filters-changed",{filters:this.filters,changed:e}),this.config.endpoint&&window.debouncer.schedule(this.config.endpoint,this.fetch.bind(this),100))}isCacheValid(e){return!(!e||!e.timestamp)&&Date.now()-e.timestamp<this.config.TTL}storeResponseHeaders(e,t){const s={key:e,etag:t.headers.get("ETag"),lastModified:t.headers.get("Last-Modified"),timestamp:Date.now()};this.httpHeaders.set(e,s),this.db&&this.db.objectStoreNames.contains("headers")&&this.db.transaction(["headers"],"readwrite").objectStore("headers").put(s)}async saveCache(e,t){if(!this.db||!this.db.objectStoreNames.contains("cache"))return;const s=this.db.transaction(["cache"],"readwrite").objectStore("cache");await s.put(t)}async loadCache(){if(this.db)return new Promise((e=>{this.db.transaction(["cache"],"readonly").objectStore("cache").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.isCacheValid(e)&&this.cache.set(e.key,e)})),e()}}))}async loadHeaders(){if(this.db)return new Promise((e=>{this.db.transaction(["headers"],"readonly").objectStore("headers").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.httpHeaders.set(e.key,e)})),e()}}))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}async query(e,t){return this.db?new Promise(((s,i)=>{const n=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName);if(!n.indexNames.contains(e))return void i(new Error(`Index ${e} does not exist`));const o=n.index(e),r=void 0!==t?o.getAll(t):o.getAll();r.onsuccess=e=>{const t=e.target.result.map((e=>this.config.stripDOMReferences?this.stripDOMReferences(e):e));s(t)},r.onerror=e=>i(e)})):[]}async count(){return this.db?new Promise(((e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).count();s.onsuccess=t=>e(t.target.result),s.onerror=e=>t(e)})):this.data.size}setLoading(e){console.log("on"),this.body.classList.toggle("loading",e),e?this.loading.showModal():this.loading.close()}destroy(){this.currentRequest&&this.currentRequest.abort(),this.subscribers.clear(),this.data.clear(),this.cache.clear(),this.httpHeaders.clear(),this.db&&(this.db.close(),this.db=null)}clearCache(){this.cache.clear(),this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").clear(),this.notify("cache-cleared")}};
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init(),window.addEventListener("beforeunload",(()=>this.destroy()))}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t={}){if(this.stores.has(e))return console.warn(`Store "${e}" already registered`),this.getStoreAPI(e);if(!t.keyPath)throw new Error(`Store "${e}" requires a keyPath`);const s={name:e,config:{dbName:`jvb_${e}_db`,version:1,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},db:null,data:new Map,cache:new Map,httpHeaders:new Map,filters:{...t.filters},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};return s.config.headers={"X-WP-Nonce":jvbSettings?.nonce,...s.config.headers},this.stores.set(e,s),this.subscribers.set(e,new Set),this.initStoreDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)})),this.getStoreAPI(e)}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),clearHttpHeaders:t=>this.clearHttpHeaders(e,t),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}normalizeForStorage(e){if(null==e)return e;if(e instanceof Set)return Array.from(e);if(e instanceof Map)return Object.fromEntries(e);if(Array.isArray(e))return e.map((e=>this.normalizeForStorage(e)));if("object"==typeof e){const t={};for(const[s,r]of Object.entries(e))t[s]=this.normalizeForStorage(r);return t}return e}stripDOMReferences(e,t=new WeakSet){if(null==e)return e;const s=typeof e;if("string"===s||"number"===s||"boolean"===s)return e;if("object"===s&&t.has(e))return"[Circular]";if(e instanceof HTMLElement||e instanceof NodeList||e instanceof HTMLCollection||void 0!==e.nodeType)return null;if(e instanceof Date)return e;if(Array.isArray(e))return t.add(e),e.map((e=>this.stripDOMReferences(e,t))).filter((e=>null!==e));if("object"===s){t.add(e);const s={};for(const[r,i]of Object.entries(e)){const e=this.stripDOMReferences(i,t);null!==e&&(s[r]=e)}return s}return e}async initStoreDB(e){const t=this.stores.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performStoreInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performStoreInit(e){const t=this.stores.get(e),{dbName:s,version:r}=t.config;try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{this.setupStores(e,t.config)}));this.databases.set(s,e)}t.db=this.databases.get(s),this.loadStoreDataInBackground(e),this.notify(e,"db-init")}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}t.useHttpCaching&&!e.objectStoreNames.contains("headers")&&e.createObjectStore("headers",{keyPath:"key"})}loadStoreDataInBackground(e){const t=this.stores.get(e);if(!t?.db)return;const s=[this.loadStoreData(e),this.loadStoreCache(e),this.loadStoreHeaders(e)];Promise.all(s).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async loadStoreData(e){const t=this.stores.get(e);if(t?.db)return new Promise((s=>{const r=t.db.transaction([t.config.storeName],"readonly").objectStore(t.config.storeName).getAll();r.onsuccess=r=>{const i=r.target.result||[];i.forEach((e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.notify(e,"data-loaded",{count:i.length}),s(i)},r.onerror=()=>s([])}))}async loadStoreCache(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("cache"))return new Promise((e=>{const s=t.db.transaction(["cache"],"readonly").objectStore("cache").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)})),e()},s.onerror=()=>e()}))}async loadStoreHeaders(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("headers"))return new Promise((e=>{const s=t.db.transaction(["headers"],"readonly").objectStore("headers").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{t.httpHeaders.set(e.key,e)})),e()},s.onerror=()=>e()}))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initStoreDB(e)}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers},o=t.httpHeaders.get(s);t.config.useHttpCaching&&o&&(o.etag&&(a["If-None-Match"]=o.etag),o.lastModified&&(a["If-Modified-Since"]=o.lastModified));const n=new AbortController;t.currentRequest=n;const c=await fetch(i,{method:"GET",headers:a,signal:n.signal});if(304===c.status&&r)return this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r;if(!c.ok)throw new Error(`HTTP ${c.status}: ${c.statusText}`);const d=await c.json();return t.config.useHttpCaching&&this.storeResponseHeaders(e,s,c),await this.processFetchedData(e,d,s),this.notify(e,"data-loaded",{cached:!1,items:d.items||[]}),d}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s){const r=this.stores.get(e),i=t.items||[];for(const t of i)await this.save(e,t);const a={key:s,items:i.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters}};r.cache.set(s,a),await this.saveToCache(e,s,a),r.lastResponse={has_more:t.has_more||!1,total:t.total||i.length,pages:t.pages||1}}async save(e,t){const s=this.stores.get(e);let r=this.normalizeForStorage(t);if(r=this.stripDOMReferences(r),s.config.validateData){const t=this.validateSerializable(r);if(!t.valid)throw console.error(`Cannot save non-serializable data to store "${e}":`,t.error),new Error(`Non-serializable data: ${t.error}`)}const i=this.getItemKey(r,s.config.keyPath);if(s.data.set(i,t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.put(r)}return this.notify(e,"item-saved",{item:t,key:i}),i}validateSerializable(e,t="root"){if(null==e)return{valid:!0};const s=typeof e;if("string"===s||"number"===s||"boolean"===s)return{valid:!0};if("function"===s)return{valid:!1,error:`Function at ${t}`};if(e instanceof Date)return{valid:!0};if(e instanceof HTMLElement||e instanceof NodeList||e instanceof HTMLCollection||void 0!==e.nodeType)return{valid:!1,error:`DOM element at ${t}`};if(e instanceof FormData)return{valid:!1,error:`FormData at ${t}. Convert to object first.`};if(e instanceof Blob||e instanceof File)return{valid:!1,error:`Blob/File at ${t}. Handle file uploads separately.`};if(Array.isArray(e)){for(let s=0;s<e.length;s++){const r=this.validateSerializable(e[s],`${t}[${s}]`);if(!r.valid)return r}return{valid:!0}}if("object"===s){if(e instanceof Set)return{valid:!1,error:`Set at ${t}. Convert to Array first: Array.from(set)`};if(e instanceof Map)return{valid:!1,error:`Map at ${t}. Convert to Object first: Object.fromEntries(map)`};for(const[s,r]of Object.entries(e)){const e=this.validateSerializable(r,`${t}.${s}`);if(!e.valid)return e}return{valid:!0}}return{valid:!1,error:`Unknown type at ${t}: ${s}`}}async delete(e,t){const s=this.stores.get(e);if(s.data.delete(t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.delete(t)}this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);return r&&r.items?r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);if(t.data.clear(),t.cache.clear(),t.db){const e=t.db.transaction([t.config.storeName],"readwrite").objectStore(t.config.storeName);await e.clear()}this.notify(e,"data-cleared")}setFilter(e,t,s){const r=this.stores.get(e),i=r.filters[t];null==s||""===s?delete r.filters[t]:r.filters[t]=s,this.notify(e,"filters-changed",{filters:r.filters,changed:{key:t,oldValue:i,newValue:s}}),r.config.endpoint&&this.fetch(e)}async setFilters(e,t){const s=this.stores.get(e);Object.keys(t).some((e=>s.filters[e]!==t[e]))&&(s.filters={...s.filters,...t},this.notify(e,"filters-changed",{filters:s.filters,changed:t}),s.config.endpoint&&await this.fetch(e))}removeFilter(e,t){const s=this.stores.get(e),r=s.filters[t];void 0!==r&&(delete s.filters[t],this.notify(e,"filters-changed",{filters:s.filters,removed:{key:t,oldValue:r}}),s.config.endpoint&&this.fetch(e))}clearFilters(e){const t=this.stores.get(e),s={...t.filters};t.filters={...t.config.filters},this.notify(e,"filters-cleared",{oldFilters:s,filters:t.filters}),t.config.endpoint&&this.fetch(e)}clearCache(e){const t=this.stores.get(e);if(t.cache.clear(),t.db&&t.db.objectStoreNames.contains("cache")){t.db.transaction(["cache"],"readwrite").objectStore("cache").clear()}this.notify(e,"cache-cleared")}clearHttpHeaders(e,t=null){const s=this.stores.get(e);if(t){if(s.httpHeaders.delete(t),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").delete(t)}}else if(s.httpHeaders.clear(),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").clear()}}subscribe(e,t){const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}storeResponseHeaders(e,t,s){const r=this.stores.get(e),i={key:t,etag:s.headers.get("ETag"),lastModified:s.headers.get("Last-Modified"),timestamp:Date.now()};if(r.httpHeaders.set(t,i),r.db&&r.db.objectStoreNames.contains("headers")){r.db.transaction(["headers"],"readwrite").objectStore("headers").put(i)}}async saveToCache(e,t,s){const r=this.stores.get(e);if(!r.db||!r.db.objectStoreNames.contains("cache"))return;const i=r.db.transaction(["cache"],"readwrite").objectStore("cache");await i.put(s)}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbStore=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/favourites.min.js b/assets/js/min/favourites.min.js
index c166151..47d780e 100644
--- a/assets/js/min/favourites.min.js
+++ b/assets/js/min/favourites.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(){this.store=new window.jvbStore({name:"favourites",storeName:"favourites",endpoint:"favourites",indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:864e5,showLoading:!1,filters:{user:jvbSettings.currentUser,content:"all",order:"desc",orderby:"date",page:1,all:!0}}),this.listStore=new window.jvbStore({name:"favourites_lists",storeName:"lists",keyPath:"listId",endpoint:"favourites/lists",TTL:864e5}),this.store.subscribe(((t,e)=>{t})),this.store.fetch()}toggleFavourite(t){if(!jvbSettings.currentUser)return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,""!==jvbSettings.currentUser&&(window.jvbFavourites=new t)})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
+(()=>{class t{constructor(){this.store=window.jvbStore.register("favourites",{storeName:"favourites",endpoint:"favourites",indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:36e4,showLoading:!1,filters:{user:jvbSettings.currentUser,content:"all",order:"desc",orderby:"date",page:1,all:!0}}),this.listStore=window.jvbStore.register("favourites_lists",{storeName:"lists",keyPath:"listId",endpoint:"favourites/lists",TTL:36e4}),this.store.subscribe(((t,e)=>{t})),this.store.fetch()}toggleFavourite(t){if(!jvbSettings.currentUser)return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",i=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(i),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){const i=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(i)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,""!==jvbSettings.currentUser&&(window.jvbFavourites=new t)})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index 63e4b33..d564fba 100644
--- a/assets/js/min/form.min.js
+++ b/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.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}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){this.config={collectFormData:!1,...e},this.store=window.jvbStore.register("forms",{storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4,validateData:!0,delayFetch:!0}),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.isTimeline=!1,window.crudManager&&window.crudManager.isTimeline&&(this.isTimeline=!0),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.processRepeaterField=this.processRepeaterField.bind(this),this.processGroupField=this.processGroupField.bind(this),this.processLocationField=this.processLocationField.bind(this),this.processRegularField=this.processRegularField.bind(this),this.init()}async init(){this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners(),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.type&&this.handleOperationComplete(t)}))}async handleOperationComplete(e){if(e.formId)try{await this.store.delete(e.formId)}catch(e){console.warn("Failed to clear form cache:",e)}const t=this.forms.get(e.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}checkPendingForms(){this.store.getAll().filter((e=>"draft"===e.status)).forEach((e=>{const t=this.forms.get(e.formId);if(t?.element){const s=t.element.querySelector(".restore-form");s&&(s.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,t){const s=document.querySelector(`[data-form-id="${e}"]`);if(!s)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore-changes" data-form-id="${e}">Restore</button>\n        <button class="discard-changes" data-form-id="${e}">Discard</button>\n    `,s.insertBefore(r,s.firstChild),r.querySelector(".restore-changes").addEventListener("click",(async()=>{await this.restorePendingForm(e,t),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(async()=>{await this.discardPendingForm(e),r.remove()}))}async restorePendingForm(e,t){const s=document.querySelector(`[data-form-id="${e}"]`);s&&(new this.populateForm(s,t),await this.store.save({formId:e,data:t,status:"restored",timestamp:Date.now()}),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){try{await this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}catch(e){console.error("Failed to discard pending form:",e)}}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 s=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=s,e.addEventListener("submit",this.submitHandler);const r={element:e,id:s,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,!0)};if(this.initializeFormFields(e,r),this.forms.set(s,r),this.store&&r.options.cache){const e=this.store.get(s);e&&e.formData&&this.showPendingNotification(e)}return r}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),s=t.element,r=t.tabs,a=s.querySelectorAll(".tab-content").length,i=s.querySelector(".form-progress .fill"),n=s.querySelector(".step-text .current"),o=s.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,s)=>{const r=s+1;t.classList.remove("current","completed","pending"),r<e?t.classList.add("completed"):r===e?t.classList.add("current"):t.classList.add("pending")}))};s.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=s.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;r.switchTab(e,!0),l(i+1),s.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=s.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;r.switchTab(e,!0),l(i-1),s.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=r.switchTab.bind(r);r.switchTab=(e,t)=>{c(e,t);const r=s.querySelector(`.tab-content[data-tab="${e}"]`);if(r){const e=parseInt(r.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let s=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(s=!1)}})),s}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const s=e.querySelector(".add-repeater-row"),r=e.querySelector(".repeater-items"),a=e.querySelector("template");s&&a&&r&&(window.Sortable&&new Sortable(r,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),s.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),r.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const s=e.querySelector(".repeater-items"),r=e.querySelector("template"),a=s.children.length,i=e.dataset.field,n=r.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 s=e.nextElementSibling;s&&"LABEL"===s.tagName&&(s.htmlFor=e.id)})),s.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 s=e.closest(".repeater"),r=s.dataset.field;e.remove(),this.updateRepeaterOrder(s,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:r,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const s=e.querySelector(".repeater-items"),r=e.dataset.field;Array.from(s.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const s=e.name.split(":");if(3===s.length){const a=s[2];e.name=`${r}:${t}:${a}`,e.id=`${r}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:r,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((s=>{const r=s.dataset.dependsOn,a=s.dataset.dependsValue,i=s.dataset.dependsOperator||"==";t.dependencies.has(r)||t.dependencies.set(r,[]),t.dependencies.get(r).push({field:s,requiredValue:a,operator:i}),this.checkFieldDependency(e,s,r,a,i)}))}checkFieldDependency(e,t,s,r,a){const i=e.querySelector(`[name="${s}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,r,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,s){const r=String(e||""),a=String(t||"");switch(s){case"==":default:return r==a;case"!=":return r!=a;case">":return parseFloat(r)>parseFloat(a);case"<":return parseFloat(r)<parseFloat(a);case">=":return parseFloat(r)>=parseFloat(a);case"<=":return parseFloat(r)<=parseFloat(a);case"contains":return r.includes(a);case"empty":return""===r;case"not_empty":return""!==r}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.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),s=e.closest(".field");let r=s?.querySelector(".char-count");!r&&s&&(r=document.createElement("div"),r.className="char-count",r.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,s.appendChild(r));const a=()=>{const s=e.value.length;r&&(r.querySelector(".current").textContent=s,r.classList.toggle("exceeded",s>t)),s>t&&(e.value=e.value.substring(0,t),r&&(r.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 s=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const r=this.collectFormData(t);this.notify("form-submit",{formId:t.dataset.formId,fullData:r,config:s})}else;}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 s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const r=window.getIcon?.("check-circle");r&&(r.classList.add("success-icon"),s.prepend(r))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}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 s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),this.touchedFields.add(t.field),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const r=window.getIcon?.("close-circle");r&&(r.classList.add("error-icon"),s.prepend(r)),e.insertBefore(s,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 s=0;if(e.target.closest(".increase")?s+=1:e.target.closest(".decrease")&&(s-=1),0!==s){let r=parseFloat(t.step);r=Math.max(r,1),e.ctrlKey&&e.shiftKey?r*=50:e.ctrlKey?r*=5:e.shiftKey&&(r*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+r*s,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,s,r,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>s?(e.value=s,r.disabled=!1):r.disabled?r.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,s=t.form||t.closest("form");if(!s)return;const r=this.forms?.get(s.dataset.formId);if(r&&(r.options.autosave||this.subscribers.size>0)){const e=r.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(s,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(r,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,s=t.form||t.closest("form");if(!s)return;const r=e.target.closest("input, textarea, select");if(r){const e=this.findFieldWrapper(r);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(r)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(r,e)}const a=this.forms?.get(s.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 s=t.closest("form");this.showFormStatus(s.dataset.formId,"pending");const r=this.findFieldWrapper(t);if(!r)return;const a=r.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 s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const r=t.dataset.min,a=t.dataset.max;return void 0!==r&&s<parseFloat(r)?`Value must be at least ${r}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,r=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(r&&e.length>parseInt(r))||`Must be no more than ${r} 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 s=this.getFieldValue(e),r=t.dataset.field;if(!this.touchedFields.has(r)&&!e.required)return!0;if(!s&&!e.required)return this.clearValidation(t),!0;if(e.required&&!s)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&&s){if(!new RegExp(a).test(s)){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(s))return this.showError(t,e.message),!1;if(e.test){const r=e.test(s,t);if(!0!==r)return this.showError(t,r),!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 s=e.querySelector(".validation-icon.success"),r=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"),s&&(s.hidden=!1),r&&(r.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const s=e.querySelector(".validation-icon.success"),r=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"),s&&(s.hidden=!0),r&&(r.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),s=e.querySelector(".validation-message"),r=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),r?.classList.remove("error"),t&&(t.hidden=!0),s&&(s.hidden=!0,s.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let s=!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 r=e.dataset.field;r&&this.touchedFields.add(r);this.validateField(t,e)||(s=!1,!1===s&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),s}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 s=t.target.closest("input, select");if(!s)return;const r=s.name;if(!r)return;e.querySelectorAll(`[data-show-if*="${r}"]`).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 s=e.dataset.field,r=e.querySelector(".validation-message");s&&r&&(t[s]=r.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 s=`autosave_${e.id}`;this.debouncer.schedule(s,(()=>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")})).catch((t=>{console.error("Autosave failed:",t),this.showFormStatus(e.id,"error","Failed to save changes")}));const s=this.getChangedFields(e.data,t);if(0!==Object.keys(s).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,r]of Object.entries(t))"object"==typeof r&&(s[e]=r);this.notify("form-autosave",{formId:e.id,changes:s,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const s=this.collectFormData(t.element),r=this.getChangedFields(t.data,s);return Object.keys(r).length>0}showFormStatus(e,t,s=""){let r=this.forms.get(e);if(!r.options.formStatus)return;if(r.status===t)return;r.status=t;const a=r.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),""===s&&(s=n[t]||t),i.textContent=s,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,t=!1){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);if(e.classList.contains("table")&&"FORM"===e.tagName)return{};const s=new FormData(e);let r={};const a={},i={};for(let[t,n]of s.entries()){if(this.ignore.includes(t)||t.endsWith("_temp"))continue;this.getFieldProcessor(t)(t,n,r,a,i,e)}return window.isEmptyObject(i)?this.mergeRepeaterData(r,a):(r=this.mergeRepeaterData(r,a),this.mergePostData(r,i))}collectTimeline(e){let t={},s={},r=[],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,o]=a;if(s[t]||(s[t]={id:parseInt(t)},r.push(t)),"post_thumbnail"===o)s[t].post_thumbnail=parseInt(e.querySelector(`[name="${i}"]`).closest(".item")?.dataset.id);else{this.getFieldProcessor(o)(o,n,s[t],{},{},e)}}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=r.map((e=>s[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],t}getFieldProcessor(e){return e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((s=>{const r={};Object.keys(t[s]).forEach((e=>{const a=t[s][e];Object.keys(a).length>0&&(r[e]=a)})),e[s]=Object.values(r)})),e}mergePostData(e,t){for(let[s,r]of Object.entries(t))e[s]=r;return e}processRepeaterField(e,t,s,r,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),r[n]||(r[n]={}),r[n][o]||(r[n][o]={}),c||r[n][o][l]?(r[n][o][l]?Array.isArray(r[n][o][l])||(r[n][o][l]=[r[n][o][l]]):r[n][o][l]=[],r[n][o][l].push(t)):r[n][o][l]=t}processGroupField(e,t,s,r,a,i){const n=e.split("::"),o=n[0];s[o]||(s[o]={});let l=s[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,s,r,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(s,n)||(s[n]={},Object.hasOwn(s,"sendAll")?s.sendAll.includes(n)||s.sendAll.push(n):s.sendAll=[n]),s[n][o]=t}processRegularField(e,t,s,r,a,i){s[e=e.replace("[]","")]?(Array.isArray(s[e])||(s[e]=[s[e]]),s[e].push(t)):s[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 s=this.forms.get(e);if(!s)return;const r=s.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(s.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const s=this.getFieldInfo(r,e);if(!s.label)continue;const a=this.createResultElement(o,s,t,r);a&&n.appendChild(a)}o.remove(),(t="form"!==t?r.closest(t)??r:r).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 s=e.querySelector(`label[for="${t}"]`),r=null,a=null;if(r||(r=e.querySelector(`[name="${t}"]`)),r||(r=e.querySelector(`[name="${t}[]"]`)),!r){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(s=a.querySelector("legend"),r=a.querySelector("input, select, textarea"))}if(!s&&r){const e=r.closest(".field, fieldset");e&&(s=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:r&&(i="checkbox"===r.type&&r.name.endsWith("[]")?"checkbox":"checkbox"===r.type?"true_false":"SELECT"===r.tagName&&r.multiple?"select":r.type||"text"),{label:s?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:r}}createResultElement(e,t,s,r){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(s,t.type,r);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,s){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,s,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,s,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,s)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${s+1}:</strong><ul>`;for(const[s,r]of Object.entries(e))if(!this.isEmptyValue(r)){const e=s.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${r}</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[s,r]of Object.entries(e))if(!this.isEmptyValue(r)){const e=s.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof r||Array.isArray(r)?t+=`<li><strong>${e}:</strong> ${r}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(r)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((s=>{e[s]&&t.push(e[s])})),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,s=null){if(0===e.length)return"<em>None selected</em>";if(t&&s&&s.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,s.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,s){if("select"===s){const s=t.querySelector(`option[value="${e}"]`);return s?.textContent||e}if("radio"===s){const s=t.querySelector(`input[type="radio"][value="${e}"]`),r=s?.nextElementSibling;return r?.textContent||e}if("checkbox"===s){const s=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(s){const e=t.querySelector(`label[for="${s.id}"]`);if(e)return e.textContent.trim();const r=s.nextElementSibling;if("LABEL"===r?.tagName)return r.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((s=>s(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}))})();
\ No newline at end of file
diff --git a/assets/js/min/gallery.min.js b/assets/js/min/gallery.min.js
index 6f7511b..c5de416 100644
--- a/assets/js/min/gallery.min.js
+++ b/assets/js/min/gallery.min.js
@@ -1 +1 @@
-(()=>{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))})))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.index=0,this.images=[],this.zoom={scale:1,min:1,max:4,threshold:50,x:0,y:0,startX:0,startY:0,ease:.2,panning:!1},this.swipe=this.resetSwipe(),this.activePointers=new Map,this.lastTap=0,this.initElements(),this.initModal(),this.initListeners(),this.initSubscribers()}initElements(){this.elements={imageSelector:"a.open-gallery",gallery:{modal:"dialog.gallery",wrap:".wrap",nextButton:".next",prevButton:".prev",image:".image",leftImage:".image-left",rightImage:".image-right",counter:".counter"}},this.ui=window.uiFromSelectors(this.elements)}initModal(){this.modal=new window.jvbModal(this.ui.gallery.modal,{openMessage:"Opened Gallery",closeMessage:"Closed Gallery"}),this.modal.subscribe(((e,t)=>{"modal-close"===e&&this.toggleGallery(!1)}))}buildGalleryItems(e=null){let t=e?`[data-opens="${e}"]`:this.elements.imageSelector;this.items=Array.from(document.querySelectorAll(t)).map(((e,t)=>{let i=e.querySelector("img");return{id:e.dataset.id||t,small:i.dataset.small||e.src,medium:i.dataset.medium||e.src,full:i.dataset.full||e.src,alt:i.alt||"",element:i}}))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.pointerDownHandler=this.onPointerDown.bind(this),this.pointerMoveHandler=this.onPointerMove.bind(this),this.pointerUpHandler=this.onPointerUp.bind(this),this.wheelHandler=this.onWheel.bind(this),this.keyHandler=this.handleKeys.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){let t=window.targetCheck(e,this.elements.imageSelector);t&&!this.modal.isOpen?(e.preventDefault(),this.buildGalleryItems(Object.hasOwn(t.dataset,"opens")?t.dataset.opens:null),this.index=this.items.findIndex((e=>e.element===t.querySelector("img"))),this.toggleGallery(!0)):this.modal.isOpen&&(window.targetCheck(e,this.elements.gallery.nextButton)?(console.log("Next"),this.nextElement()):window.targetCheck(e,this.elements.gallery.prevButton)&&(console.log("Previous"),this.prevElement()))}handleKeys(e){if(this.modal.isOpen){switch(e.key){case"ArrowLeft":e.preventDefault(),this.prevElement();break;case"ArrowRight":e.preventDefault(),this.nextElement()}e.ctrlKey&&("+"!==e.key&&"="!==e.key||(e.preventDefault(),this.handleZoom(.2)),"-"===e.key&&(e.preventDefault(),this.handleZoom(-.2)),"0"===e.key&&(e.preventDefault(),this.resetZoom()))}}onPointerDown(e){this.swipe.startX=e.clientX,this.swipe.startY=e.clientY,this.ui.gallery.image.setPointerCapture(e.pointerId),this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=performance.now();if(t-this.lastTap<300&&1===this.activePointers.size)return this.zoom.scale>1?this.resetZoom():this.handleZoom(1,e.clientX,e.clientY),void(this.lastTap=0);if(this.lastTap=t,2===this.activePointers.size){const e=[...this.activePointers.values()];return this.pinchStartDist=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),void(this.pinchStartScale=this.zoom.scale)}this.zoom.scale>1&&(this.zoom.panning=!0,this.zoom.startX=e.clientX-this.zoom.x,this.zoom.startY=e.clientY-this.zoom.y)}onPointerMove(e){if(this.activePointers.has(e.pointerId))if(this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),2!==this.activePointers.size)this.zoom.panning&&(this.zoom.x=e.clientX-this.zoom.startX,this.zoom.y=e.clientY-this.zoom.startY,this.applyTransform());else{const e=[...this.activePointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),i=this.pinchStartScale*(t/this.pinchStartDist)-this.zoom.scale;this.handleZoom(i)}}onPointerUp(e){if(this.activePointers.delete(e.pointerId),this.activePointers.size<2&&(this.pinchStartDist=0),!this.zoom.panning&&0===this.activePointers.size){this.swipe.endX=e.clientX,this.swipe.endY=e.clientY;const t=this.swipe.endX-this.swipe.startX;this.swipe.endY,this.swipe.startY;Math.abs(t)>this.zoom.threshold&&(t>0?(console.log("Swipe right"),this.prevElement()):(console.log("Swipe left"),this.nextElement())),this.zoom.panning=!1}}onWheel(e){if(!e.ctrlKey)return;e.preventDefault();const t=e.deltaY<0?.2:-.2;this.handleZoom(t,e.clientX,e.clientY)}clampPan(){const e=this.ui.gallery.wrap;if(!e)return;const t=e.getBoundingClientRect(),i=Math.min(t.width/1920,t.height/1920),s=1920*i,n=1920*i*this.zoom.scale,o=s*this.zoom.scale,l=t.width-n-32,a=t.height-o-32;this.zoom.x=Math.min(32,Math.max(l,this.zoom.x)),this.zoom.y=Math.min(32,Math.max(a,this.zoom.y))}handleZoom(e,t=null,i=null){const s=this.zoom.scale;let n=s+e;if(n=Math.min(this.zoom.max,Math.max(this.zoom.min,n)),n===s)return;const o=n/s;let l=this.ui.gallery.image.getBoundingClientRect();null!==t&&null!==i||(t=l.left+l.width/2,i=l.top+l.height/2);const a=t-l.left,r=i-l.top;this.zoom.x=(this.zoom.x-a)*o+a,this.zoom.y=(this.zoom.y-r)*o+r,this.zoom.scale=n,this.applyTransform(),this.notify("zoom",{scale:this.zoom.scale})}applyTransform(){this.ui.gallery.image.style.transform=`translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`}resetZoom(){this.zoom.scale=1,this.zoom.x=0,this.zoom.y=0,this.zoom.startX=0,this.zoom.startY=0,this.zoom.panning=!1,this.applyTransform()}resetSwipe(){return{startX:null,startY:null,endX:null,endY:null}}toggleGallery(e,t=null){e?(this.ui.gallery.image.addEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.addEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.addEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.addEventListener("pointercancel",this.pointerUpHandler),window.addEventListener("wheel",this.wheelHandler,{passive:!1}),window.addEventListener("keydown",this.keyHandler),this.moveIntoView()):(this.ui.gallery.image.removeEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.removeEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.removeEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.removeEventListener("pointercancel",this.pointerUpHandler),window.removeEventListener("wheel",this.wheelHandler),window.removeEventListener("keydown",this.keyHandler),this.resetZoom(),this.resetSwipe(),this.activePointers.clear(),this.lastTap=0),e&&!this.modal.isOpen&&this.modal.handleOpen()}moveIntoView(e=0){let t=this.index+e;t<0?t=this.items.length-1:t>=this.items.length?t=0:t===this.items.length-3&&this.notify("load-more"),this.index=t,this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}`)}nextElement(){this.resetZoom(),this.moveIntoView(1)}prevElement(){this.resetZoom(),this.moveIntoView(-1)}updateDisplay(){const e=this.items[this.index];e&&(this.ui.gallery.image.src=e.full,this.ui.gallery.image.alt=e.alt,this.ui.gallery.counter.textContent=`${this.index+1} / ${this.items.length}`,this.ui.gallery.prevButton.disabled=this.items.length<=1,this.ui.gallery.nextButton.disabled=this.items.length<=1)}preloadAdjacent(){[-1,1].forEach((e=>{const t=this.index+e;if(t>0&&t<this.items.length){const i=this.items[t];(e<0?this.ui.gallery.leftImage:this.ui.gallery.rightImage).src=i.full}}))}initSubscribers(){this.subscribers=new Set}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.toggleGallery(!1),document.removeEventListener("click",this.clickHandler)}}document.addEventListener("DOMContentLoaded",(function(){document.querySelector("dialog.gallery")&&(window.jvbGallery=new e)}))})();
\ No newline at end of file
diff --git a/assets/js/min/media.min.js b/assets/js/min/media.min.js
index 84dab73..71ef1d6 100644
--- a/assets/js/min/media.min.js
+++ b/assets/js/min/media.min.js
@@ -1 +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)}))})();
\ No newline at end of file
+(()=>{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?"medium":this.currentWidth<1200?"large":"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)}))})();
\ No newline at end of file
diff --git a/assets/js/min/modal.min.js b/assets/js/min/modal.min.js
index 0bc2e51..a9358c5 100644
--- a/assets/js/min/modal.min.js
+++ b/assets/js/min/modal.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(t,s){this.modal=t,this.a11y=window.jvbA11y,this.options={openMessage:"Opened modal",closeMessage:"Closed modal",open:null,close:"button.cancel",save:'button[type="submit"]',...s},this.subscribers=new Set,e.modalStack||(e.modalStack=[]),this.boundEscapeListener=this.handleEscapeListener.bind(this),this.boundBackdropListener=this.handleBackdropListener.bind(this),this.init()}init(){this.isOpen=!1,this.hasChanges=!1,this.initElements(),this.initEvents()}initElements(){this.elements={open:this.options.open,close:this.options.close,save:this.options.save}}handleClose(){if(e.modalStack[e.modalStack.length-1]!==this)return;this.notify("modal-close",this.modal),this.a11y.announce(this.options.closeMessage),this.modal.close();const t=e.modalStack.indexOf(this);-1!==t&&e.modalStack.splice(t,1),this.showBody(),this.removeCloseListeners()}handleOpen(t){this.addCloseListeners(),this.hideBody(),this.isOpen=!0,this.modal.showModal(),this.a11y||(this.a11y=window.jvbA11y),this.a11y.trapFocus(this.modal),this.a11y.announce(this.options.openMessage),e.modalStack.push(this),this.notify("modal-open",{modal:this.modal,event:t})}addCloseListeners(){document.addEventListener("keydown",this.boundEscapeListener),document.addEventListener("click",this.boundBackdropListener)}removeCloseListeners(){document.removeEventListener("keydown",this.boundEscapeListener),document.removeEventListener("click",this.boundBackdropListener)}handleEscapeListener(t){"Escape"===t.key&&e.modalStack[e.modalStack.length-1]===this&&(t.preventDefault(),this.handleClose())}handleBackdropListener(t){t.target===this.modal&&e.modalStack[e.modalStack.length-1]===this&&this.handleClose()}hideBody(){0===e.modalStack.length&&(document.body.style.overflow="hidden")}showBody(){0===e.modalStack.length&&(document.body.style.overflow="")}initEvents(){document.addEventListener("click",this.handleClick.bind(this))}handleClick(t){this.elements.open&&window.targetCheck(t,this.elements.open)?this.handleOpen(t):this.modal.contains(t.target)&&(this.elements.save&&window.targetCheck(t,this.elements.save)||this.elements.close&&window.targetCheck(t,this.elements.close)&&e.modalStack[e.modalStack.length-1]===this)&&this.handleClose()}static getTopModal(){return e.modalStack[e.modalStack.length-1]||null}static getAllModals(){return[...e.modalStack]}static closeTopModal(){const t=e.getTopModal();t&&t.handleClose()}static closeAllModals(){for(;e.modalStack.length>0;)e.closeTopModal()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.subscribers.clear(),e.closeAllModals(),this.showBody(),this.removeCloseListeners()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbModal=e}))})();
\ No newline at end of file
+(()=>{class e{constructor(t,s){this.modal=t,this.a11y=window.jvbA11y,this.options={openMessage:"Opened modal",closeMessage:"Closed modal",open:null,close:"button.cancel",save:'button[type="submit"]',...s},this.subscribers=new Set,e.modalStack||(e.modalStack=[]),this.boundEscapeListener=this.handleEscapeListener.bind(this),this.boundBackdropListener=this.handleBackdropListener.bind(this),this.init()}init(){this.isOpen=!1,this.hasChanges=!1,this.initElements(),this.initEvents()}initElements(){this.elements={open:this.options.open,close:this.options.close,save:this.options.save}}handleClose(){if(e.modalStack[e.modalStack.length-1]!==this)return;this.notify("modal-close",this.modal),this.a11y.announce(this.options.closeMessage),this.modal.close(),this.isOpen=!1;const t=e.modalStack.indexOf(this);-1!==t&&e.modalStack.splice(t,1),this.showBody(),this.removeCloseListeners()}handleOpen(t){this.addCloseListeners(),this.hideBody(),this.isOpen=!0,this.modal.showModal(),this.a11y||(this.a11y=window.jvbA11y),this.a11y.trapFocus(this.modal),this.a11y.announce(this.options.openMessage),e.modalStack.push(this),this.notify("modal-open",{modal:this.modal,event:t})}addCloseListeners(){document.addEventListener("keydown",this.boundEscapeListener),document.addEventListener("click",this.boundBackdropListener)}removeCloseListeners(){document.removeEventListener("keydown",this.boundEscapeListener),document.removeEventListener("click",this.boundBackdropListener)}handleEscapeListener(t){"Escape"===t.key&&e.modalStack[e.modalStack.length-1]===this&&(t.preventDefault(),this.handleClose())}handleBackdropListener(t){t.target===this.modal&&e.modalStack[e.modalStack.length-1]===this&&this.handleClose()}hideBody(){0===e.modalStack.length&&(document.body.style.overflow="hidden")}showBody(){0===e.modalStack.length&&(document.body.style.overflow="")}initEvents(){document.addEventListener("click",this.handleClick.bind(this))}handleClick(t){this.elements.open&&window.targetCheck(t,this.elements.open)?this.handleOpen(t):this.modal.contains(t.target)&&(this.elements.save&&window.targetCheck(t,this.elements.save)||this.elements.close&&window.targetCheck(t,this.elements.close)&&e.modalStack[e.modalStack.length-1]===this)&&this.handleClose()}static getTopModal(){return e.modalStack[e.modalStack.length-1]||null}static getAllModals(){return[...e.modalStack]}static closeTopModal(){const t=e.getTopModal();t&&t.handleClose()}static closeAllModals(){for(;e.modalStack.length>0;)e.closeTopModal()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.subscribers.clear(),e.closeAllModals(),this.showBody(),this.removeCloseListeners()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbModal=e}))})();
\ No newline at end of file
diff --git a/assets/js/min/populate.min.js b/assets/js/min/populate.min.js
index eaaad5a..4ffdb88 100644
--- a/assets/js/min/populate.min.js
+++ b/assets/js/min/populate.min.js
@@ -1 +1 @@
-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||"")}}};
\ No newline at end of file
+window.jvbPopulate=class{constructor(e,t={},a={},r={}){for(let[l,o]of Object.entries(t)){let t=e.querySelector(`[data-field="${l}"]`);t&&this.populateField(t,l,o,a,r)}}populateField(e,t,a,r={},l={}){if(e&&null!=a)switch(this.getFieldType(e)){case"upload":case"gallery":case"image":this.populateUploadField(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){if(e.dataset.fieldType)return e.dataset.fieldType;if(e.dataset.type)return e.dataset.type;const t=["upload","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;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||"file"!==r.type)&&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 o=e.querySelector(`input[type="radio"][name="${t}"][value="${r}"]`);o&&(o.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 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=l,o.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),o.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)}populateUploadField(e,t,a,r={}){if("timeline"===e.dataset.subtype||"timeline"===t)return void this.populateTimelineGallery(e,t,a,r);if(!a)return;const l=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===l.length)return;const o=e.querySelector(`input[type="hidden"][name="${t}"]`);o&&(o.value=l.join(","));const i=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),i&&(window.removeChildren(i),l.forEach((e=>{const t=window.getTemplate("uploadItem");if(!t)return void console.warn("uploadItem template not found");let a=t.querySelector('input[name="select-item"]'),l=t.querySelector('label[for="select-item"]');t.dataset.id=e,a.name=a.name+`-${e}`,a.id=a.name,l.htmlFor=a.name;const o=t.querySelector("img");t.querySelector("video").remove();const n=t.querySelector("details");if(r[e]){const a=r[e];o&&(o.src=a.medium||a.small||a.large||"",o.alt=a["image-alt-text"]||a.alt||"");const l=t.querySelector('[name="image-title"]'),i=t.querySelector('[name="image-alt-text"]'),n=t.querySelector('[name="image-caption"]');l&&(l.value=a["image-title"]||a.title||""),i&&(i.value=a["image-alt-text"]||a.alt||""),n&&(n.value=a["image-caption"]||a.caption||"")}else console.warn("No image data found for ID:",e);n?.querySelector(".upload-meta > .hint")?.remove(),i.append(t)})),l.length>0&&n&&(n.hidden=!0))}populateTimelineGallery(e,t,a,r){if(!a||"object"!=typeof a)return;const l=Object.values(a);if(0===l.length)return;const o=e.querySelector(".item-grid"),i=e.querySelector(".file-upload-container");if(e.querySelector(".progress")?.remove(),o){window.removeChildren(o),console.log(l);for(let[e,t]of Object.entries(a)){let e=t.post_thumbnail;const a=window.getTemplate("timelineItem");if(!a)return;const l=a.querySelector("img");a.querySelector("video")?.remove(),a.querySelector(".select-item span")?.remove();let i=a.querySelector('input[name="select-item"]'),n=a.querySelector('label[for="select-item"]');a.dataset.id=e,a.dataset.postId=t.id,i.name=i.name+`-${e}`,i.id=i.name,n.htmlFor=i.name;const c=r[e];l&&c&&(l.src=c.medium||c.small||c.large||"",l.title=c["image-title"]);const s={...t,...c};a.querySelectorAll(".field").forEach((e=>{if(e.classList.contains("group"))return;const a=e.querySelector('input:not([type="file"]), textarea');if(!a)return;const l=e.querySelector("label"),o=a.name.replace("upload_data::",""),i=s[o];this.populateField(e,o,i,r);const n=`[${t.id}]${o}`;a.name=n,a.id=n,l&&(l.htmlFor=n)})),o.append(a)}l.length>0&&i&&(i.hidden=!0)}}populateRepeaterField(e,t,a,r={}){if(!a||!Array.isArray(a))return;const l=e.querySelector(".repeater-items"),o=e.querySelector("template");l&&o?(window.removeChildren(l),a.forEach(((a,r)=>{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-${r}`,i.dataset.index=r;const n=i.querySelector(".row-number");n&&(n.textContent=`#${r+1}`),i.querySelectorAll("input, select, textarea").forEach((e=>{const l=e.name,o=`${t}:${r}:${l}`,i=`${t}-${r}-${l}-${e.value}`;e.name=o,e.id=i;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=i),void 0!==a[l]&&this.populateRepeaterFieldValue(e,l,a[l])})),l.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||"")}}};
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index 2139ecc..ad79130 100644
--- a/assets/js/min/queue.min.js
+++ b/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:"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}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.user=jvbSettings.currentUser,this.headers={"X-WP-Nonce":jvbSettings.nonce,...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=window.jvbStore.register("queue",{keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1}),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(),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 e=this.getOperationsByStatus(["completed","failed_permanent"],!1);e.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":if(t.item){const e=this.store.data.get(t.item.id);e&&e.status!==t.item.status&&this.handleOperationStatusChange(t.item,e.status)}this.hasQueuedOperations()&&this.startPolling();break;default:this.updateUI()}})),this.notify("queue-initialized",{operations:e})}handleOperationStatusChange(e,t){if(e&&t!==e.status)switch(e.status){case"completed":this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={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,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}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:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0===e.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading"),e=this.getQueue(e.id);const t=`${this.config.apiBase}${e.endpoint}`;let s;console.log(e.data),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),s=e.data):(s=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const i=await fetch(t,{method:e.method,headers:e.headers,body:s}),a=await i.json();if(!i.ok||!1===a.success)throw new Error(a.message||`HTTP ${i.status}`);if(a.id&&e.id!==a.id){const t=this.getQueue(a.id);t?(t.data=window.deepMerge(t.data,e.data),t.status="pending",t.serverData=a,this.updateOperationStatus(t.id,t.status),this.setQueue(t),this.removeOperationFromUI(e.id),e=t):(this.clearQueue(e.id),e.id=a.id,e.status="pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e))}else e.status="pending",e.serverData=a,this.updateOperationStatus(e.id,"pending"),this.setQueue(e);this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),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(e,t){if(0!==(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length){["cancel","dismiss"].includes(t)&&e.forEach((e=>{this.removeOperationFromUI(e)}));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:e,action:t,user:jvbSettings.currentUser})});if(!i.ok){const e=await i.json().catch((()=>{}));throw new Error(e.message||`${t} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return["cancel","dismiss"].includes(t)?e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-operation`,s),this.clearQueue(e)})):(e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-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:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t)));if(i.retried)return i;throw s}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}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=e=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return e.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationsByStatus("completed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"dismiss")}}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationsByStatus("failed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"retry")}}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}handleChange(e){}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[e,t]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(e))if("object"==typeof t){this.ui[e]={};for(let[s,i]of Object.entries(t))this.ui[e][s]=this.ui.panel.querySelector(i)}else this.ui[e]=this.ui.panel.querySelector(t)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const e=Array.from(this.store.data.values()),t=this.store.lastResponse?.queue_stats||{queued:0,localProcessing:0,uploading:0,pending:0,processing:0,completed:0,failed:0,failed_permanent:0};if(this.ui.count){const s=e.length-t.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>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)}this.ui.clearButton.disabled=0===this.getOperationsByStatus("completed").length,this.ui.retryButton.disabled=0===this.getOperationsByStatus("failed").length&&0===this.getOperationsByStatus("failed_permanent").length,Object.entries(this.ui.filters).forEach((([s,i])=>{const a="all"===s?e.length:t[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===e.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else e.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.append(t)}))}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),l=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const d=t.querySelector(".actions");d&&this.updateActionButtons(e,d)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.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=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let e=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{e--,this.ui.countdown.textContent=e,e<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}showPopup(e,t="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=e),this.ui.popup.className=`popup ${t} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(e,t=!0){return Array.isArray(e)||"string"!=typeof e||(e=[e]),t?Array.from(this.store.data.values()).filter((t=>e.includes(t.status))):Array.from(this.store.data.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}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 e}))})();
\ No newline at end of file
diff --git a/assets/js/min/referral.min.js b/assets/js/min/referral.min.js
index d4800d3..08fc78b 100644
--- a/assets/js/min/referral.min.js
+++ b/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",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}))})();
\ No newline at end of file
+(()=>{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"),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 s=r.querySelector("p");s&&(s.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){window.debouncer.cancel("check-referral"),e.preventDefault();const t=e.target,r=new FormData(t);let n={};this.setFormLoading(!0,t);try{let e={success:!1,message:""};"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}))})();
\ No newline at end of file
diff --git a/assets/js/min/selector.min.js b/assets/js/min/selector.min.js
index e7339bb..b94488e 100644
--- a/assets/js/min/selector.min.js
+++ b/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(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}))})();
\ No newline at end of file
+(()=>{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.triggers=new Set([".taxonomy-toggle"]),this.subscribers=new Set,this.store=window.jvbStore.register("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:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0}),this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.hasAutocomplete&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":const e=this.store.filters.taxonomy;if(e?.includes(",")&&this.handleBatchDataLoaded(e,t),e){(e.includes(",")?e.split(",").map((e=>e.trim())):[e]).forEach((e=>{this.updateFieldsForTaxonomy(e)}))}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=this.store.getFiltered(),i=this.store.lastResponse?.page||{},s=e.filters?.search&&e.filters.search.length>0,o=i.page>1;this.notify("terms-loaded",{terms:t,filters:e.filters}),0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),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.")}updateFieldButtonState(e){const t=this.fields.get(e);if(!t)return;const i=Array.from(this.store.data.values()).some((e=>e.taxonomy===t.taxonomy));t.toggle&&(t.toggle.disabled=!i&&!t.canCreate,t.toggle.title=i?`Select ${this.getPlural(t.taxonomy)}`:`No ${this.getSingular(t.taxonomy)} available`)}updateFieldsForTaxonomy(e){this.getFieldsForTaxonomy(e).forEach((e=>{this.updateFieldButtonState(e.id)}))}getFieldsForTaxonomy(e){return Array.from(this.fields.values()).filter((t=>t.taxonomy===e))}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!1;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=Object.hasOwn(t,"button")?t.button:e.querySelector("button.taxonomy-toggle");Object.hasOwn(t,"buttonSelector")&&this.triggers.add(t.buttonSelector);let r={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:Object.hasOwn(t,"selected")?t.selected:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&r.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=>r.selectedTerms.add(e)))}return Object.hasOwn(t,"selectedItems")&&t.selectedItems.forEach((e=>{r.selectedTerms.add(e)})),this.fields.set(s,r),this.isInitializing&&this.taxonomiesToFetch.add(r.taxonomy),r.selectedTerms.size>0&&this.initFieldDisplay(s),s}registerFilterButton(e,t={}){const i=this.createFieldId(e);e.dataset.fieldId=i,t.buttonSelector&&this.triggers.add(t.buttonSelector);const s={id:i,input:null,container:t.container||e.closest(".filters")||e.parentElement,taxonomy:e.dataset.taxonomy,name:`filter_${e.dataset.taxonomy}`,maxSelection:parseInt(e.dataset.max)||0,canSearch:"search"in e.dataset,hasAutocomplete:!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(t.selectedItems||[]),toggle:e,selectedContainer:t.selected||null,isFilterMode:!0,...t};return this.fields.set(i,s),this.isInitializing?this.taxonomiesToFetch.add(s.taxonomy):this.store.setFilter("taxonomy",s.taxonomy),i}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;Array.from(t.selectedTerms).forEach((t=>{const i=this.store.get(t);i&&this.addTermToDisplay(e,i.id,i.name,i.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":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.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(){this.autocompleteHandler=window.debounce((e=>this.handleAutocomplete(e)),300),document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this)),document.addEventListener("focus",(e=>{if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);i&&this.preloadTaxonomy(i.taxonomy)}),!0)}handleClick(e){const t=window.targetCheck(e,Array.from(this.triggers));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,!0)}catch(e){console.error("Error handling toggle click:",e),this.error?.log&&this.error.log(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e,t=!1){this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=this.getSingular(this.currentConfig.taxonomy),this.currentPlural=this.getPlural(this.currentConfig.taxonomy),t&&this.modalInstance.handleOpen(),this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms.forEach((e=>{const t=this.store.get(e);t&&this.selectedTerms.set(e,{id:e,name:t.name,path:t.path})}))}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,!0),this.modalInstance.handleOpen()}openModal(){this.currentConfig?(!this.creator&&this.currentConfig.canCreate&&"jvbTaxCreator"in window&&(this.creator=new window.jvbTaxCreator(this)),this.updateModalForTaxonomy(),this.updateModalSelections(),this.updateSelectionCount(),window.removeChildren(this.ui.termsList),this.showLoading()):console.error("No active field set")}updateSelectionCount(){if(!this.currentConfig)return;const e=this.selectedTerms.size,t=this.currentConfig.maxSelection,i=this.modal?.querySelector(".selection-count");i&&(i.textContent=t>0?`${e} of ${t} selected`:`${e} selected`)}getSingular(e){return jvbSettings.labels[e]?.single||e}getPlural(e){return jvbSettings.labels[e]?.plural||e}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.notify("selected-terms",{terms:this.selectedTerms,taxonomy:this.currentConfig.taxonomy}),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),!this.hasAutocomplete&&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 r=Array.from(i.selectedTerms);i.input.value=r.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 r=window.getTemplate("selectedTerm").cloneNode(!0);r.dataset.id=t,r.dataset.path=s,r.dataset.name=i,r.dataset.taxonomy=o.taxonomy,r.querySelector("span").textContent=s,r.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(r)}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(){const e=this.store.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e=null,t=!1,i=!1){if(e||(e=this.store.getFiltered()),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;const o=document.createDocumentFragment();e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&o.appendChild(t)})),this.ui.termsList.appendChild(o)}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"),r=t.querySelector("span, .term-name");if(s&&o&&r&&(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,r.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}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}handleBatchDataLoaded(e,t){const i=e.split(",").map((e=>e.trim())),s=this.store.getStore();i.forEach((e=>{const t={taxonomy:e,page:1,search:"",parent:0},i=this.generateCacheKeyForFilters(t),o={key:i,items:Array.from(this.store.data.values()).filter((t=>t.taxonomy===e)).map((e=>e.id)),timestamp:Date.now(),endpoint:s.config.endpoint,filters:t};if(s.cache.set(i,o),s.db?.objectStoreNames.contains("cache")){s.db.transaction(["cache"],"readwrite").objectStore("cache").put(o)}this.updateFieldsForTaxonomy(e)})),this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}generateCacheKeyForFilters(e){const t=Object.keys(e).sort().reduce(((t,i)=>(t[i]=e[i],t)),{});return JSON.stringify(t)}async preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/swiper.min.js b/assets/js/min/swiper.min.js
new file mode 100644
index 0000000..10b3a47
--- /dev/null
+++ b/assets/js/min/swiper.min.js
@@ -0,0 +1 @@
+window.jvbSwiper=new class{constructor(){this.isInitialized=!1,this.initSubscribers(),this.initHandlers(),this.swipe={startX:null,endX:null,startY:null,endY:null,minSwipe:50},this.pinch={active:!1,startDistance:0,lastDistance:0,scale:1}}initHandlers(){this.touchStartHandler=this.handleTouchStart.bind(this),this.touchMoveHandler=this.handleTouchMove.bind(this),this.touchEndHandler=this.handleTouchEnd.bind(this)}initListeners(){this.isInitialized||(this.isInitialized=!0,document.addEventListener("touchstart",this.touchStartHandler),document.addEventListener("touchmove",this.touchMoveHandler),document.addEventListener("touchend",this.touchEndHandler))}cleanupListeners(){this.subscribers.size>0||(this.isInitialized=!1,document.removeEventListener("touchstart",this.touchStartHandler),document.removeEventListener("touchmove",this.touchMoveHandler),document.removeEventListener("touchend",this.touchEndHandler))}handleTouchStart(t){if(2===t.touches.length){const i=t.touches[0].clientX-t.touches[1].clientX,s=t.touches[0].clientY-t.touches[1].clientY,e=Math.sqrt(i*i+s*s);return this.pinch.active=!0,this.pinch.startDistance=this.pinch.lastDistance=e,void this.notify("pinch-start",{distance:e})}this.swipe.startX=t.touches[0].clientX,this.swipe.startY=t.touches[0].clientY}handleTouchMove(t){if(this.pinch.active&&2===t.touches.length){const i=t.touches[0].clientX-t.touches[1].clientX,s=t.touches[0].clientY-t.touches[1].clientY,e=Math.sqrt(i*i+s*s),n=e/this.pinch.startDistance;return this.pinch.lastDistance=e,this.pinch.scale=n,this.notify("pinch-move",{e:t,distance:e,scale:n}),void(e>this.pinch.startDistance?this.notify("pinch-out",{scale:n}):this.notify("pinch-in",{scale:n}))}this.swipe.endX=t.touches[0].clientX,this.swipe.endY=t.touches[0].clientY}handleTouchEnd(t){if(this.pinch.active)return this.notify("pinch-end",{finalScale:this.pinch.scale}),void(this.pinch.active=!1);if(!(this.swipe.startX&&this.swipe.endX&&this.swipe.startY&&this.swipe.endY))return;const i=this.swipe.startX-this.swipe.endX,s=this.swipe.startY-this.swipe.endY;if(Math.abs(i)>this.swipe.minSwipe){let t=i>0?"swipe-right":"swipe-left";this.notify(t)}if(Math.abs(s)>this.swipe.minSwipe){let t=s>0?"swipe-up":"swipe-down";this.notify(t)}this.swipe.startX=this.swipe.startY=this.swipe.endX=this.swipe.endY=null}initSubscribers(){this.subscribers=new Set}subscribe(t){return this.isInitialized||this.initListeners(),this.subscribers.add(t),()=>this.subscribers.delete(t)}unsubscribe(t){this.subscribers.delete(t),0===this.subscribers.size&&this.cleanupListeners()}notify(t,i={}){this.subscribers.forEach((s=>{try{s(t,i)}catch(t){console.error("Subscriber error:",t)}}))}destroy(){this.subscribers.clear(),this.cleanupListeners()}};
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 8d83df7..51414d3 100644
--- a/assets/js/min/uploader.min.js
+++ b/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.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}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStore=window.jvbStore.register("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"}],TTL:6048e5,delayFetch:!0}),this.uploadStore=window.jvbStore.register("uploads",{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"}],delayFetch:!0}),window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.uploadElements=new Map,this.fieldElements=new Map,this.groupElements=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.sortableInstances=new Map,this.initWorker(),this.subscribers=new Set,this.dragController=null,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',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.sortableConfig={animation:150,draggable:".item",handle:".select-item, img",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleReorder(e)},this.init()}async init(){this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":s&&this.handleOperationCancelled(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":this.handleOperationComplete(t,s);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,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=this.buildFieldUI(e),r={id:t,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(r),this.fieldElements.set(t,{element:e,ui:o,config:s}),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"post_group"!==s.destination||this.dragController||this.initGroupFeatures(),"single"!==s.type&&this.initSortable(t),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}initSortable(e){if(!window.Sortable)return;const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((t=>{if(t.sortableInstance)return;const s=t.classList.contains("group")?`${e}-group-${t.closest(".upload-group")?.dataset.groupId}`:`${e}-preview`,o=new Sortable(t,{...this.sortableConfig,group:{name:e,pull:!0,put:!0},onAdd:e=>{this.updateSortableState(e.to)},onRemove:e=>{this.updateSortableState(e.from)}});t.sortableInstance=o,this.sortableInstances.set(s,o)}))}updateSortableState(e){const t=e?.sortableInstance;if(!t)return;const s=e.querySelectorAll(".item").length>0;t.option("disabled",!s)}refreshSortable(e){const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((e=>this.updateSortableState(e)))}handleReorder(e){const t=e.to.closest(".field, .upload");if(!t)return;let s=t.querySelector('input[type="hidden"]'),o=Array.from(t.querySelectorAll(".item")).map((e=>e.dataset.id));console.log(o),s.value=o.join(","),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}))}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"})},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}})}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)}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`))}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;o=s.grid}e.forEach((e=>{s?this.removeFromGroup(e):this.addToGroup(e,o)}));const r=this.getFieldIdFromElement(t);this.schedulePersistance(r);const a=e.length>1?`Moved ${e.length} items`:"Moved item";this.a11y.announce(a)}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 s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}if(t){const s=this.getFieldData(t);"post_group"===s?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return;o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const r=t.length;let a=0;this.updateUploadProgress(e,0,r,"Processing files...");const i=Array.from(t).map((async t=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,n={id:i,attachmentId:null,fieldId:e,status:"local_processing",groupId:null,meta:{originalName:t.name,size:t.size,type:t.type}},l=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,s.config.subtype):t;await this.saveBlobData(i,d||t);const c=this.getSubtypeFromMime(t.type),u=this.createUploadElement({id:i,preview:l,meta:n.meta,subtype:c},"post_group"===s.config.destination);return this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),o.ui.preview&&(o.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:l,location:o.ui.preview})),n.status="processed",await this.uploadStore.save(n),s.uploads.add(i),await this.saveFieldData(s),a++,this.updateUploadProgress(e,a,r,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),a++,this.updateUploadProgress(e,a,r,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((o,r)=>{let a,i=!1;a=setTimeout((()=>{i||(i=!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:a}),this.handleProcess(e,t).then((e=>{i||(i=!0,clearTimeout(a),this.worker.tasks.delete(t),o(e))})).catch((e=>{i||(i=!0,clearTimeout(a),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 a=new Image,i=document.createElement("canvas"),n=i.getContext("2d");let l=null;const d=()=>{a.onload=null,a.onerror=null,l&&(URL.revokeObjectURL(l),l=null),i.width=1,i.height=1,n.clearRect(0,0,1,1)};a.onload=()=>{try{const{width:l,height:c}=this.calculateOptimalDimensions(a,t);i.width=l,i.height=c,n.imageSmoothingEnabled=!0,n.imageSmoothingQuality="high",n.drawImage(a,0,0,l,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);i.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}`))}},a.onerror=()=>{d(),r(new Error(`Failed to load image: ${e.name}`))};try{l=this.createPreviewUrl(e),a.src=l}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,a)=>{if(!this.worker.worker)return void a(new Error("Worker not available"));const i=`${t}_${Date.now()}`,n=t=>{if(t.data.messageId===i)if(this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),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 a(new Error(t.data.error||"Worker processing failed"))},l=e=>{this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),a(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",n),this.worker.worker.addEventListener("error",l),this.worker.worker.postMessage({messageId:i,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\t\t\t\tself.onmessage = async function(e) {\n\t\t\t\t\tconst { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst bitmap = await createImageBitmap(file);\n\t\t\t\t\t\tconst scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n\t\t\t\t\t\tconst width = Math.round(bitmap.width * scale);\n\t\t\t\t\t\tconst height = Math.round(bitmap.height * scale);\n\t\t\t\t\t\tconst canvas = new OffscreenCanvas(width, height);\n\t\t\t\t\t\tconst ctx = canvas.getContext('2d');\n\t\t\t\t\t\tctx.imageSmoothingEnabled = true;\n\t\t\t\t\t\tctx.imageSmoothingQuality = 'high';\n\t\t\t\t\t\tctx.drawImage(bitmap, 0, 0, width, height);\n\t\t\t\t\t\tbitmap.close();\n\t\t\t\t\t\tconst blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });\n\t\t\t\t\t\tself.postMessage({ messageId, success: true, blob: blob, format: outputFormat });\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tself.postMessage({ messageId, success: false, error: error.message });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t"],{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))}async submitUploads(e){const t=this.getFieldData(e);this.fieldElements.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=[],a=new FormData;let i=[];for(const e of o){const t={images:[],fields:{}};for(let[s,o]of Object.entries(e.changes))t.fields[s]=o;const o=s.filter((t=>{const s=this.uploadStore.get(t);return s?.groupId===e.id}));for(const e of o){const s=await this.getBlobData(e);if(s){a.append("files[]",s);const o={upload_id:e,index:i.length},r=this.uploadElements.get(e),n=r?.element?.querySelector('[name="featured"]');n?.checked&&(t.fields.featured=e),t.images.push(o),i.push(e)}}r.push(t)}const n=s.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of n){const t={images:[],fields:{}},s=await this.getBlobData(e);if(s){a.append("files[]",s);const o={upload_id:e,index:i.length};t.images.push(o),i.push(e)}r.push(t)}a.append("content",t.config.content),a.append("user",t.config.itemID),a.append("posts",JSON.stringify(r)),a.append("upload_ids",JSON.stringify(i));const l={endpoint:"uploads/groups",method:"POST",data:a,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=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),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}}async queueUpload(e){const t=this.getFieldData(e);if(!t?.uploads||0===t.uploads.size)return;const s=Array.from(t.uploads),o=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");const r={endpoint:"uploads",method:"POST",data:o,title:`Uploading ${s.length} file${s.length>1?"s":""} to server...`,popup:`Uploading ${s.length} file${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(r);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async 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=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const r=await this.getBlobData(e);r&&(s.append("files[]",r),o.push(t.id))}));return await Promise.all(r),s.append("upload_ids",JSON.stringify(o)),s}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),s=this.uploadStore.get(t);if(!s)return;if(!this.getFieldData(s.fieldId))return;let o={};o[e.target.name]=e.target.value,s.meta={...s.meta,...o},await this.uploadStore.save(s);let r={};r[s.attachmentId??s.id]=s.meta;const a={endpoint:"uploads/meta",method:"POST",data:r,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(a)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:s.id})}}async handleOperationComplete(e,t){if((e.result?.data||e.serverData?.data||[]).forEach((e=>{const t=this.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploadStore.save(t),this.updateUploadStatus(e.upload_id,"completed"))})),!t)return;const s=this.getFieldData(t);if(!s)return;const o=Array.from(s.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of o)await this.clearUpload(e,!1),s.uploads.delete(e);0===s.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(s),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const s=this.uploadStore.get(t);s&&(s.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(s),this.updateUploadStatus(t,s.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s)await this.clearUpload(e,!1);await this.clearFieldFromStores(e),this.updateFieldState(e),this.a11y.announce("Upload cancelled")}getFieldGroups(e){const t=this.getFieldData(e);return t?.groups?t.groups.map((e=>({id:e.id,uploads:e.uploads||[],changes:e.changes||{}}))):[]}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}async restoreSelectedUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,s]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=s,await this.restoreField(t))}}async restoreField(e){const{config:t,context:s,uploads:o,groups:r,id:a}=e;s?.modalType&&await this.openModalForRestore(s);let i=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!i){const e=`${t.content}_${t.itemID}_${t.name}`;i=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!i)return void console.warn(`Field ${t.name} not found for restoration`,t);let n=i.dataset.uploader;n&&this.fieldElements.has(n)||(n=this.registerUploader(i));const l=this.fieldElements.get(n),d=this.getFieldData(n);if(!l||!d)return void console.error("Failed to register field for restoration");d.state=e.state||"ready",l.ui||(l.ui=this.buildFieldUI(i)),l.ui.groups?.display&&(l.ui.groups.display.hidden=!1),l.ui.dropZone&&(l.ui.dropZone.hidden=!0),r&&r.length>0&&await this.restoreGroups(n,r);const c=o instanceof Set?Array.from(o):Array.isArray(o)?o:[];for(const e of c){const t=this.uploadStore.get(e);t&&await this.restoreUpload(n,t)}await this.saveFieldData(d),this.updateFieldState(n),this.maybeLockUploads(n),this.refreshSortable(n),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(n)}async restoreUpload(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(!s||!o)return void console.error("Field not found for upload restoration:",e);const r=await this.getBlobData(t.id);if(!r)return void console.warn("Blob data not found for upload:",t.id);const a=this.createPreviewUrl(r),i=this.getSubtypeFromMime(r.type),n=this.createUploadElement({id:t.id,preview:a,meta:t.meta||{originalName:r.name,size:r.size,type:r.type},subtype:i},"post_group"===o.config.destination);let l;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){l=e.grid;const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads||(s.uploads=[]),s.uploads.includes(t.id)||s.uploads.push(t.id))}else l=s.ui.preview,t.groupId=null}else l=s.ui.preview;l?l.appendChild(n):s.ui.preview&&(s.ui.preview.appendChild(n),l=s.ui.preview),this.uploadElements.set(t.id,{element:n,preview:a,location:l}),o.uploads||(o.uploads=new Set),o.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),l&&this.updateSortableState(l)}async restoreGroups(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(s&&o){for(const s of t){const t=this.createGroup(e,s.id);if(!t){console.warn("Failed to create group:",s.id);continue}const r=o.groups?.find((e=>e.id===s.id));if(r&&(s.changes&&(r.changes={...s.changes}),s.uploads&&(r.uploads=[...s.uploads]),s.changes)){const e=t.element.querySelector('[name*="post_title"]'),o=t.element.querySelector('[name*="post_excerpt"]');e&&s.changes.post_title&&(e.value=s.changes.post_title),o&&s.changes.post_excerpt&&(o.value=s.changes.post_excerpt)}}await this.saveFieldData(o)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:s}=e;let o=null;switch(t){case"create":o=document.querySelector('[data-action="create"]');break;case"edit":s&&(o=document.querySelector(`[data-action="edit"][data-id="${s}"]`));break;case"bulkEdit":o=document.querySelector('[data-action="bulk-edit"]')}o?(o.click(),await new Promise((e=>setTimeout(e,300)))):console.warn("Modal trigger not found for restoration:",e)}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]}async clearUpload(e,t=!0){const s=this.uploadElements.get(e);if(s&&(this.revokePreviewUrl(s.preview),s.element)){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}if(this.uploadElements.delete(e),await this.uploadStore.delete(e),t){const t=this.uploadStore.get(e);t?.fieldId&&await this.schedulePersistance(t.fieldId)}}async clearFieldFromStores(e){const t=this.getFieldData(e);if(t?.uploads){const e=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const t of e)await this.uploadStore.delete(t)}await this.fieldStore.delete(e)}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}updateFieldState(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t||!s)return;const o=t.element,r=s.uploads?.size||0,a=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;o.dataset.hasUploads=r>0?"true":"false",o.dataset.uploadCount=r.toString(),o.dataset.hasGroups=a?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${r} item${1!==r?"s":""}`)}updateUploadProgress(e,t,s,o){const r=this.fieldElements.get(e);if(!r?.ui?.progress?.progress)return;const a=r.ui.progress,i=s>0?t/s*100:0;a.fill&&(a.fill.style.width=`${i}%`),a.text&&(a.text.textContent=o),a.count&&(a.count.textContent=`${t}/${s}`),a.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.getFieldData(e);s&&(s.state=t,this.saveFieldData(s))}updateUploadStatus(e,t){const s=this.uploadStore.get(e);s&&(s.status=t,this.uploadStore.save(s),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),s=this.uploadStore.get(e);if(!s||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${s.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(s.status),s.status)}showUploadProgress(e,t=!0){const s=this.uploadElements.get(e);if(!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.uploadElements.get(e);if(!o?.element)return;const r=o.element.querySelector(".progress");if(!r)return;const a=r.querySelector(".fill"),i=r.querySelector(".details"),n=r.querySelector(".icon");a&&(a.style.width=`${t}%`),s&&i&&(i.textContent=this.getStatusText(s)),s&&n&&(n.innerHTML=this.getStatusIcon(s).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t?.ui?.dropZone||!s)return;if("post_group"===s.config.destination)return;const o=s.uploads?.size||0,r=s.config?.maxFiles||999;t.ui.dropZone.hidden=o>=r,t.element.classList.toggle("at-max-uploads",o>=r)}createGroup(e,t=null){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const r=this.createGroupElement(t,e);if(!r)return null;o.ui.groups||(o.ui.groups={groups:new Map,container:null,empty:null,display:null}),o.ui.groups.groups.set(t,r),o.ui.groups.container&&o.ui.groups.empty?o.ui.groups.container.insertBefore(r,o.ui.groups.empty):o.ui.groups.container&&o.ui.groups.container.appendChild(r);const a=r.querySelector(".item-grid.group");if(this.groupElements.set(t,{element:r,grid:a,fieldId:e}),s.groups||(s.groups=[]),s.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(s),this.addGroupSelectionHandler(e,t),a){const s=new Sortable(a,{...this.sortableConfig,group:{name:e,pull:!0,put:!0},disabled:!0,onAdd:e=>this.updateSortableState(e.to),onRemove:e=>this.updateSortableState(e.from)});a.sortableInstance=s,this.sortableInstances.set(`${e}-group-${t}`,s)}return{id:t,element:r,grid:a}}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 a=r.querySelector('[name="post_title"]'),i=r.querySelector('[name="post_excerpt"]');a&&(a.id=`${e}_title`,a.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const n=this.getFieldData(t);if(n&&""!==n.config.content){let e=s.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid.group");return a&&(a.dataset.groupId=e),s}deleteGroup(e,t=!0){const s=this.groupElements.get(e);if(!s)return;const o=this.getFieldData(s.fieldId);if(!o)return;const r=o.groups?.find((t=>t.id===e));let a=!0;t&&r?.uploads?.length>0&&(a=!window.confirm("Delete uploads in group?")),t&&a&&r?.uploads&&r.uploads.forEach((e=>{this.removeFromGroup(e)})),o.groups&&(o.groups=o.groups.filter((t=>t.id!==e)),this.saveFieldData(o)),s.element&&(s.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${s.fieldId}-group-${e}`,n=this.sortableInstances.get(i);n?.destroy&&n.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){const o=this.uploadStore.get(e),r=this.uploadElements.get(e);if(!o||!r)return;const a=this.getFieldData(o.fieldId),i=this.fieldElements.get(o.fieldId);if(!a||!i)return;if(!t&&r.location===i.ui.preview||t===r.location)return;if(o.groupId){const t=a.groups?.find((e=>e.id===o.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(o.groupId))}const n=r.element.querySelector('[name*="select-item"]');n&&(n.checked=!1);let l=r.element.querySelector('[name="featured"]');if(l&&(l.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,o.groupId=null;else{const s=t.dataset.groupId;l&&(l.name=s+"_"+l.name);const r=a.groups?.find((e=>e.id===s));r&&(r.uploads||(r.uploads=[]),r.uploads.push(e),o.groupId=s)}r.location=t,t.append(r.element),this.uploadStore.save(o),s&&this.saveFieldData(a),this.updateSortableState(t),r.location&&r.location!==t&&this.updateSortableState(r.location)}removeFromGroup(e){const t=this.uploadStore.get(e),s=this.uploadElements.get(e);if(!t||!s)return;const o=this.getFieldData(t.fieldId),r=this.fieldElements.get(t.fieldId);if(!o||!r)return;if(t.groupId){const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}r.ui?.preview&&(r.ui.preview.appendChild(s.element),s.location=r.ui.preview);const a=s.element.querySelector('[name="featured"]');a&&(a.hidden=!0,a.checked=!1),this.uploadStore.save(t),this.updateSortableState(r.ui.preview)}removeUpload(e,t){const s=this.getFieldData(e),o=this.uploadStore.get(t),r=this.uploadElements.get(t);if(!s||!o)return;if(s.uploads?.delete(t),o.groupId){const e=s.groups?.find((e=>e.id===o.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(o.groupId))}r?.element?.remove(),this.clearUpload(t),this.saveFieldData(s),this.updateFieldState(e),this.maybeLockUploads(e);const a=this.selectionHandlers.get(e);a&&a.deselect(t),this.a11y.announce("Upload removed")}handleGroupMetaChange(e){const t=this.getGroupFromElement(e);if(!t)return;const s=this.getFieldData(t.fieldId),o=s?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!o)return;o.changes||(o.changes={});let r=e.name;r.includes("group")&&(r=r.replace(`${o.id}_`,"").replace(`${o.id}[`,"").replace("]","")),o.changes[r]=e.value,this.saveFieldData(s),this.schedulePersistance(t.fieldId)}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":const t=this.fieldElements.get(s);t&&(t.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.fieldElements.get(e);if(!t?.element)return;const s=new window.jvbHandleSelection({container:t.element,ui:{selectAll:t.element.querySelector('[name="select-all-uploads"]'),bulkControls:t.element.querySelector(".selection-actions"),count:t.element.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return s.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,s),s}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const o=this.groupElements.get(t);if(!o?.element)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){}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}getFieldData(e){const t=this.fieldStore.get(e);return t?(Array.isArray(t.uploads)?t.uploads=new Set(t.uploads):t.uploads||(t.uploads=new Set),Array.isArray(t.groups)||(t.groups=[]),t):null}async saveFieldData(e){await this.fieldStore.save({...e,timestamp:Date.now()})}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",getRuntimeData:e=>this.fieldElements.get(e),getStoreData:e=>this.getFieldData(e)},upload:{selector:this.selectors.items.item,key:"uploadId",getRuntimeData:e=>this.uploadElements.get(e),getStoreData:e=>this.uploadStore.get(e)},group:{selector:this.selectors.groups.container,key:"groupId",getRuntimeData:e=>this.groupElements.get(e),getStoreData:e=>{const t=this.groupElements.get(e);if(!t)return null;const s=this.getFieldData(t.fieldId);return s?.groups?.find((t=>t.id===e))}}},o=s[t];if(!o)return null;const r=e.closest(o.selector);if(!r)return null;const a=r.dataset[o.key];return{...o.getRuntimeData(a),...o.getStoreData(a)}}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){const t=this.getFromElement(e,"field");return t?.id??null}getUploadIdFromElement(e){const t=this.getFromElement(e,"upload");return t?.id??null}getGroupIdFromElement(e){const t=this.getFromElement(e,"group");return t?.id??null}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){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]||0}getModalType(e){if(!e?.element)return null;if(void 0!==e._cachedModalType)return e._cachedModalType;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}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return;s.dataset.uploadId=e.id,s.dataset.subtype=e.subtype||"image";let[o,r,a,i,n]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch(o&&(o.value=e.id),e.subtype){case"image":r&&(r.src=e.preview,r.alt=e.meta?.originalName||""),a?.remove(),i?.remove();break;case"video":a&&(a.src=e.preview),r?.remove(),i?.remove();break;case"document":const t=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"},n=window.getIcon(o[s]||"file");i&&(i.innerText=t,i.prepend(n)),r?.remove(),a?.remove()}if(n){let e=window.getTemplate("uploadMeta");e&&n.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}normalizeFieldData(e){return e?(Array.isArray(e.uploads)?e.uploads=new Set(e.uploads):e.uploads||(e.uploads=new Set),Array.isArray(e.groups)||(e.groups=[]),e.groups=e.groups.map((e=>({...e,uploads:Array.isArray(e.uploads)?e.uploads:[]}))),e):null}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),1e3)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const s=await t.arrayBuffer(),o=this.uploadStore.get(e)||{id:e};o.blobData={buffer:s,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(o)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const s=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([s],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.checkForStoredUploads()}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":break;case"item-saved":this.showSaveIndicator(t.key)}}async checkForStoredUploads(){const e=this.fieldStore.getAll();console.log("Checking for stored uploads...",{fieldStates:e.length,uploadStoreSize:this.uploadStore.data.size});const t=e.filter((e=>{if(!e.uploads)return!1;return(e.uploads instanceof Set?Array.from(e.uploads):Array.isArray(e.uploads)?e.uploads:[]).some((e=>{const t=this.uploadStore.get(e);return t&&!t.operationId&&["completed","processed","local_processing","processed-original"].includes(t.status)}))}));console.log("Found pending fields:",t.length),0!==t.length&&this.showRecoveryNotification(t)}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 a=r.querySelector(".restore-details");a&&(a.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.getBlobData(e.id);if(r)try{const o=this.createPreviewUrl(r);let[a,i,n,l,d]=[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 c=this.getSubtypeFromMime(r.type);switch(s.dataset.subtype=c,c){case"image":[i.src,i.alt]=[o,r.name??e.meta?.originalName??""],n.remove(),l.remove();break;case"video":n.src=o,i.remove(),l.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")}l.innerText=e.originalFile.name,l.prepend(t),i.remove(),n.remove()}s.dataset.previewUrl=o}catch(t){console.warn("Failed to create preview for upload:",e.id,t)}const a=s.querySelector("summary span");a&&(a.textContent=e.meta?.originalName||"Unknown file");const i=s.querySelector("details");i&&e.meta&&(i.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 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())}showSaveIndicator(e){}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){await this.fieldStore.clear(),await this.uploadStore.clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}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.uploadElements.clear(),this.fieldElements.clear(),this.groupElements.clear(),this.selected.clear(),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/utility.min.js b/assets/js/min/utility.min.js
index c79b1bb..bcac47e 100644
--- a/assets/js/min/utility.min.js
+++ b/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.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()}}})();
\ No newline at end of file
+(()=>{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&&window.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=null,window.getIcon=function(t,e=""){if(void 0===t)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return e=""!==e&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${e.slice(0,2)}`:"",n.classList.add(`icon-${t}${e}`),n},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 Array.isArray(e)&&(e=e.join(",")),"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()}}})();
\ No newline at end of file
diff --git a/assets/js/min/view.min.js b/assets/js/min/view.min.js
index 4b63dae..7f934f3 100644
--- a/assets/js/min/view.min.js
+++ b/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(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`}}};
\ No newline at end of file
+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.isTimeline=!!document.querySelector("[data-timeline]"),this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.subscribers=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"form.table",form:"table",body:"table body",header:"table thead",footer:"table tfoot",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 r=Math.min(s,l),a=Math.max(s,l);let d=0;for(let e=r;e<=a;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()}))}));const e=document.querySelector("[data-view]:checked");e&&(this.currentView=e.dataset.view)}handleDataUpdate(e){console.log(e);const t=e.data?.items||e.items||[];this.render(t)}handleItemsUpdate(){this.render()}render(){if(!this.store)return void console.error("No store connected to renderer");const e=this.store.getFiltered();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()}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.renderGridItem(e);t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){if(this.items.grid.has(e.id))return this.items.grid.get(e.id);const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,r,a]=[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,r.dataset.id,a.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??""],this.items.grid.set(e.id,t),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.form=this.ui.table.table.querySelector("table"),this.ui.table.header=this.ui.table.form.querySelector("thead"),this.ui.table.footer=this.ui.table.form.querySelector("tfoot"),this.ui.table.body=this.ui.table.form.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,e?this.notify("table-view",this.ui.table.table):this.notify("not-table-view",this.ui.table.table),this.ui.table.body&&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.isTimeline?this.renderTimelineTableItem(e):this.renderTableItem(e);this.ui.table.body?this.ui.table.body.append(t):(this.ui.table.footer||(this.ui.table.footer=this.ui.table.table.querySelector("tfoot")),this.ui.table.form.insertBefore(t,this.ui.table.footer))})),window.jvbSelector.scanExistingFields()}renderTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);const t=window.getTemplate("tableView");return t.dataset.id=e.id,[t.querySelector(".select-item").id,t.querySelector(".select-item").value,t.querySelector(".select-item").checked,t.querySelector(".select-item + label").htmlFor,t.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],new window.jvbPopulate(t,e.fields,e.images),this.cleanupTableRow(t),this.items.table.set(e.id,t),t}renderTimelineTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);const t=window.getTemplate("tableView");t.dataset.id=e.id,[t.querySelector(".select-item").id,t.querySelector(".select-item").value,t.querySelector(".select-item").checked,t.querySelector(".select-item + label").htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id];let i=t.querySelector(".timeline-point"),s=t,l=t.querySelector("tr.shared");return new window.jvbPopulate(l,e.fields,e.images),this.prefixTimelineFieldNames(l,e.id),this.cleanupTableRow(l),e.fields.timeline&&"object"==typeof e.fields.timeline&&Object.entries(e.fields.timeline).forEach((([t,l],r)=>{let a=i.cloneNode(!0);a.dataset.index=r,a.dataset.imageId=t,new window.jvbPopulate(a,l,e.images),this.cleanupTableRow(a);let d=e.images[l.post_thumbnail];d&&(a.querySelector(".field.upload").title=d["image-title"]),this.prefixTimelineFieldNames(a,l.id),s.insertBefore(a,i)})),i.remove(),this.items.table.set(e.id,t),t}prefixTimelineFieldNames(e,t){e.querySelectorAll("input, textarea, select").forEach((e=>{const i=e.name;if(!i||i.startsWith("[")||"form-id"===i||i.startsWith("_"))return;let s=e.nextElementSibling;e.name=`[${t}]${i}`,s&&"LABEL"===s.tagName&&(e.id=`[${t}]${e.id}`,s.htmlFor=e.id)}))}cleanupTableRow(e){e.querySelectorAll("td[data-field]").forEach((e=>{e.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach((e=>{e.closest(".radio-options")||e.remove()})),"true_false"===e.dataset.fieldType&&e.querySelector(".toggle-label")?.remove(),["checkbox","radio","select"].includes(e.dataset.fieldType)&&e.querySelector(".label")?.remove()}))}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.renderListItem(e);this.ui.grid.appendChild(t)}))}renderListItem(e){if(this.items.list.has(e.id))return this.items.list.get(e.id);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??""]),this.items.list.set(e.id,t),t}setupTimelineDragHandler(){this.isTimeline&&"table"===this.currentView&&(this.timelineDragHandler&&this.timelineDragHandler.destroy(),this.timelineDragHandler=new window.jvbDragHandler({draggableSelector:".timeline-point",dropTargetSelector:".timeline-point",handleSelector:".drag-handle",getItemId:e=>e.dataset.imageId,getSelectedItems:()=>[],validateDrop:(e,t)=>{const i=document.querySelector(`.timeline-point[data-image-id="${e[0]}"]`);return!!i&&i.closest("tbody")===t.closest("tbody")},onDragStart:(e,t)=>{t.classList.add("is-dragging")},onDrop:(e,t)=>{const i=document.querySelector(`.timeline-point[data-image-id="${e[0]}"]`);if(!i)return;document.querySelectorAll(".drop-above, .drop-below").forEach((e=>{e.classList.remove("drop-above","drop-below")}));const s=i.closest("tbody");"above"===t.dataset.dropPosition?s.insertBefore(i,t):s.insertBefore(i,t.nextSibling),i.classList.remove("is-dragging"),this.updateTimelineOrder(s)},onDragEnd:(e,t)=>{document.querySelectorAll(".is-dragging, .drop-above, .drop-below").forEach((e=>{e.classList.remove("is-dragging","drop-above","drop-below")}))},previewElement:".drag-handle",previewOptions:{offset:{x:-20,y:-20},showCount:!1}}),this.addTimelineDragHoverLogic())}addTimelineDragHoverLogic(){let e=null;document.addEventListener("pointermove",(t=>{if(!document.querySelector(".timeline-point.is-dragging"))return;const i=t.target.closest(".timeline-point:not(.is-dragging)");if(!i)return void(e&&(e.classList.remove("drop-above","drop-below"),delete e.dataset.dropPosition,e=null));const s=i.getBoundingClientRect(),l=s.top+s.height/2,r=t.clientY<l;e&&e!==i&&(e.classList.remove("drop-above","drop-below"),delete e.dataset.dropPosition),i.classList.remove("drop-above","drop-below"),i.classList.add(r?"drop-above":"drop-below"),i.dataset.dropPosition=r?"above":"below",e=i}))}updateTimelineOrder(e){const t=parseInt(e.dataset.id),i=Array.from(e.querySelectorAll(".timeline-point")),s=this.store.get(t);if(!s)return;let l={};i.forEach(((e,t)=>{const i=e.dataset.imageId;l[i]=s.fields.timeline[i]})),s.fields.timeline=l,this.store.save(s),this.notify("order-changed",t),this.a11y?.announce(`Timeline order updated. ${i.length} steps reordered.`)}extractRowFields(e){const t={};return e.querySelectorAll("[data-field]").forEach((e=>{const i=e.dataset.field,s=e.querySelector("input, textarea, select");s&&("checkbox"===s.type?t[i]=s.checked:t[i]=s.value)})),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`}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((i=>i(e,t)))}};
\ No newline at end of file
diff --git a/base/_setup.php b/base/_setup.php
index 9855a72..e8d7871 100644
--- a/base/_setup.php
+++ b/base/_setup.php
@@ -12,3 +12,9 @@
 $base = apply_filters('jvb_base', 'jvb_');
 $base = (str_ends_with($base, '_')) ? $base : $base.'_';
 define('BASE', $base);
+
+$childPath = apply_filters('jvbChildDir', JVB_DIR);
+define('JVB_CHILD_DIR', $childPath);
+
+$childURL = apply_filters('jvbChildUrl', JVB_URL);
+define('JVB_CHILD_URL', $childURL);
diff --git a/build/feed/block.json b/build/feed/block.json
index 17327ca..d8b7aff 100644
--- a/build/feed/block.json
+++ b/build/feed/block.json
@@ -67,13 +67,8 @@
       "message": "This is a notice!"
     }
   },
-  "render": "file:./render.php",
   "editorScript": "file:./index.js",
   "editorStyle": "file:./index.css",
   "style": "file:./style-index.css",
-  "viewScript": "file:./view.js",
-  "dependencies": [
-    "jvb-taxonomy-selector",
-    "jvb-client-queue"
-  ]
+  "viewScript": "file:./view.js"
 }
\ No newline at end of file
diff --git a/build/feed/index-rtl.css b/build/feed/index-rtl.css
deleted file mode 100644
index 3adeaff..0000000
--- a/build/feed/index-rtl.css
+++ /dev/null
@@ -1 +0,0 @@
-.feed-content-types .components-checkbox-control{margin-bottom:8px}.feed-content-types .components-checkbox-control:last-child{margin-bottom:0}.feed-content-types .components-checkbox-control .components-checkbox-control__input-container{margin-left:8px}.feed-content-types{margin-bottom:16px}.feed-content-types .components-base-control__label{font-weight:500;margin-bottom:8px}.feed-content-types .checkbox-list{background:#fff;border:1px solid #ddd;border-radius:4px;max-height:200px;overflow-y:auto;padding:8px}.feed-content-types .checkbox-list .components-checkbox-control{margin:4px 0}.feed-content-types .checkbox-list .components-checkbox-control:first-child{margin-top:0}.feed-content-types .checkbox-list .components-checkbox-control:last-child{margin-bottom:0}.feed-content-types .select-all-wrapper{border-top:1px solid #ddd;margin-top:8px;padding-top:8px}.feed-content-types .components-checkbox-control__input-container{margin-left:8px}.feed-block{background:#fff;border:1px solid #ddd;padding:20px}.feed-block .feed-block-preview .filter-preview{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}.feed-block .feed-block-preview .filter-preview .content-type-badge{background:#f0f0f0;border-radius:4px;font-size:12px;padding:4px 8px}.feed-block .feed-block-preview .feed-grid-placeholder{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));margin-top:20px}.feed-block .feed-block-preview .feed-grid-placeholder .grid-item-placeholder{aspect-ratio:1;background:#f0f0f0;border-radius:4px}
diff --git a/build/feed/index.asset.php b/build/feed/index.asset.php
index e640b6a..4220db5 100644
--- a/build/feed/index.asset.php
+++ b/build/feed/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'eb5002a905c1285d41c3');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'c4a784b7836c150eb94e');
diff --git a/build/feed/index.css b/build/feed/index.css
deleted file mode 100644
index bb67595..0000000
--- a/build/feed/index.css
+++ /dev/null
@@ -1 +0,0 @@
-.feed-content-types .components-checkbox-control{margin-bottom:8px}.feed-content-types .components-checkbox-control:last-child{margin-bottom:0}.feed-content-types .components-checkbox-control .components-checkbox-control__input-container{margin-right:8px}.feed-content-types{margin-bottom:16px}.feed-content-types .components-base-control__label{font-weight:500;margin-bottom:8px}.feed-content-types .checkbox-list{background:#fff;border:1px solid #ddd;border-radius:4px;max-height:200px;overflow-y:auto;padding:8px}.feed-content-types .checkbox-list .components-checkbox-control{margin:4px 0}.feed-content-types .checkbox-list .components-checkbox-control:first-child{margin-top:0}.feed-content-types .checkbox-list .components-checkbox-control:last-child{margin-bottom:0}.feed-content-types .select-all-wrapper{border-top:1px solid #ddd;margin-top:8px;padding-top:8px}.feed-content-types .components-checkbox-control__input-container{margin-right:8px}.feed-block{background:#fff;border:1px solid #ddd;padding:20px}.feed-block .feed-block-preview .filter-preview{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}.feed-block .feed-block-preview .filter-preview .content-type-badge{background:#f0f0f0;border-radius:4px;font-size:12px;padding:4px 8px}.feed-block .feed-block-preview .feed-grid-placeholder{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));margin-top:20px}.feed-block .feed-block-preview .feed-grid-placeholder .grid-item-placeholder{aspect-ratio:1;background:#f0f0f0;border-radius:4px}
diff --git a/build/feed/index.js b/build/feed/index.js
index 816bd42..c1546b7 100644
--- a/build/feed/index.js
+++ b/build/feed/index.js
@@ -1 +1 @@
-(()=>{"use strict";var e,t={111:(e,t,n)=>{const l=window.wp.blocks,r=window.wp.i18n,s=window.wp.blockEditor,a=window.wp.components,o=window.wp.element,i=window.wp.apiFetch;var c=n.n(i);const d=window.ReactJSXRuntime;(0,l.registerBlockType)("jvb/feed",{edit:function({attributes:e,setAttributes:t}){const n=(0,s.useBlockProps)(),[l,i]=(0,o.useState)({});return(0,o.useEffect)((()=>{c()({path:"/jvb/v1/types"}).then((e=>{i(JSON.parse(e))}))}),[]),(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(s.InspectorControls,{children:(0,d.jsxs)(a.PanelBody,{title:(0,r.__)("Feed Settings","jvb"),children:[(0,d.jsx)(a.TextControl,{label:(0,r.__)("Title","jvb"),value:e.title,onChange:e=>t({title:e})}),(0,d.jsx)(a.ToggleControl,{label:(0,r.__)("Inherit Current Query","jvb"),help:(0,r.__)("Inherit filters from the current archive or taxonomy query","jvb"),checked:e.inheritQuery,onChange:e=>t({inheritQuery:e})}),!e.inheritQuery&&(0,d.jsxs)("div",{className:"feed-content-types",children:[(0,d.jsx)("p",{className:"components-base-control__label",children:(0,r.__)("Content Types","jvb")}),(0,d.jsx)("div",{className:"checkbox-list",children:Object.entries(l).map((([n,l])=>(0,d.jsx)(a.CheckboxControl,{label:l,checked:e.contentTypes.includes(n),onChange:l=>{const r=l?[...e.contentTypes,n]:e.contentTypes.filter((e=>e!==n));t({contentTypes:r})}},n)))}),(0,d.jsx)("div",{className:"select-all-wrapper",children:(0,d.jsx)(a.CheckboxControl,{label:(0,r.__)("Select All","jvb"),checked:e.contentTypes.length===Object.keys(l).length,onChange:e=>{t({contentTypes:e?Object.keys(l):[]})}})})]}),(0,d.jsx)(a.SelectControl,{label:(0,r.__)("Items Per Page","jvb"),value:e.itemsPerPage,options:[{label:"12",value:12},{label:"24",value:24},{label:"36",value:36}],onChange:e=>t({itemsPerPage:parseInt(e)})}),(0,d.jsx)(a.SelectControl,{label:(0,r.__)("Default Order","jvb"),value:e.defaultOrder,options:[{label:(0,r.__)("Newest First","jvb"),value:"date_desc"},{label:(0,r.__)("Oldest First","jvb"),value:"date_asc"},{label:(0,r.__)("Random","jvb"),value:"random"}],onChange:e=>t({defaultOrder:e})})]})}),(0,d.jsx)("div",{...n,children:(0,d.jsxs)("div",{className:"feed-block-preview",children:[(0,d.jsx)("h2",{children:e.title}),(0,d.jsx)("div",{className:"feed-filters",children:(0,d.jsx)("div",{className:"filter-preview",children:e.contentTypes.map((e=>(0,d.jsx)("span",{className:"content-type-badge",children:l[e]},e)))})}),(0,d.jsx)("div",{className:"feed-grid-placeholder",children:[...Array(6)].map(((e,t)=>(0,d.jsx)("div",{className:"grid-item-placeholder"},t)))})]})})]})},save:function(){return null}})}},n={};function l(e){var r=n[e];if(void 0!==r)return r.exports;var s=n[e]={exports:{}};return t[e](s,s.exports,l),s.exports}l.m=t,e=[],l.O=(t,n,r,s)=>{if(!n){var a=1/0;for(d=0;d<e.length;d++){for(var[n,r,s]=e[d],o=!0,i=0;i<n.length;i++)(!1&s||a>=s)&&Object.keys(l.O).every((e=>l.O[e](n[i])))?n.splice(i--,1):(o=!1,s<a&&(a=s));if(o){e.splice(d--,1);var c=r();void 0!==c&&(t=c)}}return t}s=s||0;for(var d=e.length;d>0&&e[d-1][2]>s;d--)e[d]=e[d-1];e[d]=[n,r,s]},l.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return l.d(t,{a:t}),t},l.d=(e,t)=>{for(var n in t)l.o(t,n)&&!l.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},l.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={814:0,138:0};l.O.j=t=>0===e[t];var t=(t,n)=>{var r,s,[a,o,i]=n,c=0;if(a.some((t=>0!==e[t]))){for(r in o)l.o(o,r)&&(l.m[r]=o[r]);if(i)var d=i(l)}for(t&&t(n);c<a.length;c++)s=a[c],l.o(e,s)&&e[s]&&e[s][0](),e[s]=0;return l.O(d)},n=globalThis.webpackChunkjvb=globalThis.webpackChunkjvb||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))})();var r=l.O(void 0,[138],(()=>l(111)));r=l.O(r)})();
\ No newline at end of file
+(()=>{"use strict";var e,t={111:(e,t,n)=>{const o=window.wp.blocks,l=window.wp.element,i=window.wp.blockEditor,r=window.wp.components,s=window.wp.apiFetch;var c=n.n(s);const a=window.wp.i18n,d=window.ReactJSXRuntime;(0,o.registerBlockType)("jvb/feed",{edit:function({attributes:e,setAttributes:t}){const[n,o]=(0,l.useState)(null),[s,h]=(0,l.useState)(!0),[p,j]=(0,l.useState)(null),x=(0,i.useBlockProps)({className:"feed-block-editor"});(0,l.useEffect)((()=>{c()({path:"/jvb/v1/feed/types",headers:{"If-Modified-Since":localStorage.getItem("feed_types_modified")}}).then((n=>{if(o(n),h(!1),!e.contentTypes&&!e.inheritQuery){const e=Object.keys(n)[0];e&&t({contentTypes:[e]})}})).catch((e=>{console.error("Error loading feed types:",e),j(e.message),h(!1)}))}),[e.inheritQuery]);const y=(n,o)=>{const l=e.contentTypes||[],i=o?[...l,n]:l.filter((e=>e!==n));t({contentTypes:i})},b=(e,t)=>`${t.plural} (${t.type})`,g=n?{content:Object.entries(n).filter((([e,t])=>"content"===t.type)),taxonomy:Object.entries(n).filter((([e,t])=>"taxonomy"===t.type))}:{content:[],taxonomy:[]};return(0,d.jsxs)("div",{...x,children:[(0,d.jsxs)(i.InspectorControls,{children:[(0,d.jsxs)(r.PanelBody,{title:(0,a.__)("Feed Settings","jvb"),initialOpen:!0,children:[(0,d.jsx)(r.ToggleControl,{label:(0,a.__)("Inherit from Page Context","jvb"),help:e.inheritQuery?(0,a.__)("Feed will adapt to the current page (profile, taxonomy, etc.)","jvb"):(0,a.__)("Manually select content types to display","jvb"),checked:e.inheritQuery,onChange:e=>t({inheritQuery:e})}),!e.inheritQuery&&(0,d.jsxs)(d.Fragment,{children:[s&&(0,d.jsxs)("div",{style:{textAlign:"center",padding:"20px"},children:[(0,d.jsx)(r.Spinner,{}),(0,d.jsx)("p",{children:(0,a.__)("Loading feed types...","jvb")})]}),p&&(0,d.jsxs)(r.Notice,{status:"error",isDismissible:!1,children:[(0,a.__)("Error loading feed types: ","jvb")," ",p]}),!s&&!p&&n&&(0,d.jsxs)(d.Fragment,{children:[g.content.length>0&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)("h4",{children:(0,a.__)("Content Types","jvb")}),g.content.map((([t,n])=>(0,d.jsx)(r.CheckboxControl,{label:b(0,n),checked:e.contentTypes?.includes(t)||!1,onChange:e=>y(t,e),help:n.taxonomies?.length>0?`Filters: ${n.taxonomies.join(", ")}`:null},t)))]}),g.taxonomy.length>0&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)("h4",{style:{marginTop:"20px"},children:(0,a.__)("Content Taxonomies","jvb")}),(0,d.jsx)("p",{style:{fontSize:"12px",color:"#757575"},children:(0,a.__)("These are collections that group other content","jvb")}),g.taxonomy.map((([t,n])=>(0,d.jsx)(r.CheckboxControl,{label:b(0,n),checked:e.contentTypes?.includes(t)||!1,onChange:e=>y(t,e),help:n.for_content?.length>0?`Contains: ${n.for_content.join(", ")}`:null},t)))]}),!e.contentTypes?.length&&(0,d.jsx)(r.Notice,{status:"warning",isDismissible:!1,children:(0,a.__)("Please select at least one content type","jvb")})]})]})]}),(0,d.jsx)(r.PanelBody,{title:(0,a.__)("Display Settings","jvb"),initialOpen:!1,children:(0,d.jsx)(r.ToggleControl,{label:(0,a.__)("Show Gallery View","jvb"),help:(0,a.__)("Enable lightbox for images","jvb"),checked:e.enableGallery||!1,onChange:e=>t({enableGallery:e})})})]}),(0,d.jsxs)("div",{className:"feed-block-placeholder",children:[(0,d.jsx)("div",{className:"feed-block-icon",children:(0,d.jsxs)("svg",{width:"48",height:"48",viewBox:"0 0 24 24",fill:"none",children:[(0,d.jsx)("rect",{x:"3",y:"3",width:"7",height:"7",fill:"currentColor",opacity:"0.3"}),(0,d.jsx)("rect",{x:"13",y:"3",width:"7",height:"7",fill:"currentColor",opacity:"0.3"}),(0,d.jsx)("rect",{x:"3",y:"13",width:"7",height:"7",fill:"currentColor",opacity:"0.3"}),(0,d.jsx)("rect",{x:"13",y:"13",width:"7",height:"7",fill:"currentColor",opacity:"0.3"})]})}),(0,d.jsx)("h3",{children:(0,a.__)("Feed Block","jvb")}),e.inheritQuery?(0,d.jsx)("p",{className:"feed-block-description",children:(0,a.__)("📍 Inheriting from page context","jvb")}):(0,d.jsx)("div",{className:"feed-block-description",children:e.contentTypes?.length>0?(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)("p",{children:(0,d.jsx)("strong",{children:(0,a.__)("Showing:","jvb")})}),(0,d.jsx)("ul",{style:{listStyle:"none",padding:"0",margin:"8px 0"},children:e.contentTypes.map((e=>{const t=n?.[e];return(0,d.jsxs)("li",{style:{padding:"4px 0",color:"#2271b1"},children:["✓ ",t?.plural||e]},e)}))})]}):(0,d.jsx)("p",{style:{color:"#d63638"},children:(0,a.__)("⚠️  No content types selected","jvb")})}),(0,d.jsx)("p",{className:"feed-block-note",children:(0,a.__)("Feed will be displayed on the frontend","jvb")})]})]})},save:function(){return null}})}},n={};function o(e){var l=n[e];if(void 0!==l)return l.exports;var i=n[e]={exports:{}};return t[e](i,i.exports,o),i.exports}o.m=t,e=[],o.O=(t,n,l,i)=>{if(!n){var r=1/0;for(d=0;d<e.length;d++){for(var[n,l,i]=e[d],s=!0,c=0;c<n.length;c++)(!1&i||r>=i)&&Object.keys(o.O).every((e=>o.O[e](n[c])))?n.splice(c--,1):(s=!1,i<r&&(r=i));if(s){e.splice(d--,1);var a=l();void 0!==a&&(t=a)}}return t}i=i||0;for(var d=e.length;d>0&&e[d-1][2]>i;d--)e[d]=e[d-1];e[d]=[n,l,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={814:0,138:0};o.O.j=t=>0===e[t];var t=(t,n)=>{var l,i,[r,s,c]=n,a=0;if(r.some((t=>0!==e[t]))){for(l in s)o.o(s,l)&&(o.m[l]=s[l]);if(c)var d=c(o)}for(t&&t(n);a<r.length;a++)i=r[a],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(d)},n=globalThis.webpackChunkjvb=globalThis.webpackChunkjvb||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))})();var l=o.O(void 0,[138],(()=>o(111)));l=o.O(l)})();
\ No newline at end of file
diff --git a/build/feed/render.php b/build/feed/render.php
deleted file mode 100644
index 7af8d29..0000000
--- a/build/feed/render.php
+++ /dev/null
@@ -1,252 +0,0 @@
-<?php
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
-}
-
-/**
- * Feed block render callback
- *
- * @param array    $attributes Block attributes.
- * @param string   $content    Block content.
- * @param WP_Block $block      Block instance.
- * @return string  Rendered block type output.
- */
-function jvbRenderFeedBlock($attributes, $content = '', $block = null)
-{
-
-    ob_start();
-    // Get block settings
-    global $jvb_taxonomy_for;
-    global $jvb_everything;
-    $context = '';
-
-    $taxonomies = [];
-
-    foreach ($jvb_taxonomy_for as $tax => $for) {
-        if (array_intersect($for, $attributes['contentTypes'])) {
-            $taxonomies[] = $tax;
-        }
-    }
-
-    $settings = [
-        'content'         => $attributes['contentTypes'][0] ?? 'tattoo',
-        'contentTypes'    => $attributes['contentTypes'],
-        'taxonomies'      => $taxonomies,
-        'defaultOrder'    => $attributes['defaultOrder'],
-        'itemsPerPage'    => $attributes['itemsPerPage'],
-    ];
-
-    $isGallery = false;
-    if ($attributes['inheritQuery']) {
-        $obj = get_queried_object();
-        $taxonomies = $contentTypes = [];
-        if (is_singular()) {
-            switch ($obj->post_type) {
-                case BASE.'artist':
-                case BASE.'partner':
-                    $contentTypes = jvbGetUserContentTypes(get_the_ID());
-                    break;
-            }
-
-            if ($obj->post_type == BASE.'artist') {
-                $isGallery = true;
-                $context = 'artist';
-                $name = get_post_meta($obj->id, 'jvb_first_name', true);
-                $feed_data['title'] = ($name== '') ? $obj->post_title : $name;
-            } elseif ($obj->post_type == BASE.'partner') {
-                $context = 'partner';
-                $feed_data['title'] = 'Offers:';
-            }
-            $type = 'post';
-        } elseif (is_tax('jvb_shop')) {
-            $context = 'shop';
-            $feed_data['title'] = 'At the Shop:';
-            $contentTypes = [
-                'artist',
-                'tattoo',
-                'piercing',
-//                'event'
-            ];
-            $type = 'term';
-        } elseif (is_tax()) {
-            $context = str_replace(BASE, '', $obj->taxonomy);
-
-            foreach ($jvb_taxonomy_for as $tax => $for) {
-                if ($context === $tax) {
-                    $contentTypes = $for['types'];
-                }
-            }
-            $type = 'term';
-            $context = 'taxonomy:'.$context;
-        }
-        jvbDump($settings);
-        jvbDump($contentTypes);
-        if (empty($contentTypes)) {
-            return;
-        }
-        foreach ($contentTypes as $index => $type) {
-            $contentTypes[$index] = str_replace(BASE, '', $type);
-        }
-        foreach ($jvb_taxonomy_for as $tax => $for) {
-            if (array_intersect($for, $contentTypes)) {
-                $taxonomies[] = $tax;
-            }
-        }
-
-        $feed_data = [
-            'content'        => $contentTypes[0],
-            'contentTypes'     => $contentTypes,
-            'taxonomies'    => $taxonomies,
-            'context'        => $context,
-        ];
-        if ($isGallery) {
-            $feed_data['isGallery'] = true;
-        }
-
-        $settings = array_merge($settings, $feed_data);
-        $title = !array_key_exists('title', $settings) ? '' : '<h2 id="work">'.$feed_data['title'].'</h2>';
-
-    } else {
-        $title = $attributes['title'] ? "<h2>{$attributes['title']}</h2>" : '';
-    }
-
-    $many = count($settings['contentTypes']) > 1;
-    $work = is_singular(BASE.'artist') ? ' id="work"' : '';
-
-    $source = get_queried_object_id();
-    global $jvb_feed;
-
-    if (empty($settings['contentTypes'])) {
-        return;
-    }
-    ?>
-
-    <section <?= $work ?> class="feed-block"
-        data-source="<?= get_queried_object_id(); ?>"<?= ($context !== '') ? '
-        data-context="'.$context.'"' : ''    ?><?= ($isGallery) ? ' data-gallery="true"' : ''?>>
-        <?= $title ?>
-
-        <form class="feed-filters">
-            <?= (!$many) ? '' : '<details>' ?>
-            <?php if ($many) : ?>
-            <summary class="row btw">
-                <div class="type-filter label">
-                    <span>SHOW:</span>
-                </div>
-                <?php
-                foreach ($settings['contentTypes'] as $i => $type) :
-                    $checked = $i === 0 ? ' checked' : '';
-
-                    $label = $jvb_everything[$type]['plural'];
-                    ?>
-                    <div class="type-filter">
-                        <input type="radio"
-                               id="filter-<?= esc_attr($type) ?>"
-                               class="btn"
-                               name="content"
-                               value="<?= esc_attr($type) ?>"
-                            <?= $checked ?>>
-                        <label for="filter-<?= esc_attr($type) ?>" title="Show <?= $label ?>">
-                            <?= jvbIcon($type, ['title'=> $label]) ?>
-                            <span class="label"><?= $label ?></span>
-                        </label>
-                    </div>
-                <?php endforeach; ?>
-            <?php endif; ?>
-
-            <?php if (is_user_logged_in()) : ?>
-                <div class="type-filter favourites-toggle">
-                    <input type="checkbox" id="favourites_only" class="btn" name="favourites_only">
-                    <label for="favourites_only" title="Show Favourites">
-                        <?= jvbIcon('heart', ['title'    =>'Favourites']) ?>
-                        <span class="label">Favourites</span>
-                    </label>
-                </div>
-            <?php endif; ?>
-
-            <?php if ($many) {
-                echo '</summary>';
-            } ?>
-
-            <div class="filters">
-                <div class="filter-group">
-                    <div class="type-filter label">
-                        <span>FILTER:</span>
-                    </div>
-
-                    <?php foreach ($jvb_taxonomy_for as $tax => $items) :
-                        $label = $jvb_everything[$tax]['plural'];
-                        $hidden = !in_array($tax, $settings['taxonomies']) ? ' hidden' : '';
-
-                        $tax = new JVBase\Forms\TaxonomySelector(
-                            $tax,
-                            BASE.$tax,
-                            [
-                                'label' => $label,
-                                'types' => $items,
-                                'hidden'=> $hidden
-                            ]
-                        );
-                        echo  $tax->renderFeed();
-                    endforeach; ?>
-                </div>
-                    <div class="selected-items-section">
-                        <div class="selected-items"></div>
-                        <div class="filter-actions">
-                            <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', true) ?>
-                            <button type="button" class="clear-filters row" hidden>
-                                <?= jvbIcon('x', ['title'    => 'Clear']) ?>
-                                Clear All Filters
-                            </button>
-                        </div>
-                    </div>
-                </div>
-
-                <div class="order-options">
-                    <div class="order-by">
-                        <span class="type-filter label">ORDER:</span>
-                        <div class="radio-group-label">
-                            <input type="radio" id="order-title" class="btn" name="orderby" value="title" data-for="artist,shop" hidden>
-                            <label for="order-title" title="Order by Name">
-                                <?= jvbIcon('alphabetical') ?>
-                                <span class="label">Name</span>
-                            </label>
-
-                            <input type="radio" id="order-date" class="btn" name="orderby" value="date" checked>
-                            <label for="order-date" title="Order by Date">
-                                <?= jvbIcon('calendar', ['title'=>'Date']) ?>
-                                <span class="label">Date</span>
-                            </label>
-
-                            <input type="radio" id="order-random" class="btn" name="orderby" value="random">
-                            <label for="order-random" title="Random Order">
-                                <?= jvbIcon('shuffle') ?>
-                                <span class="label">Random</span>
-                            </label>
-                        </div>
-                    </div>
-
-                    <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('sort-descending') ?>
-                        </label>
-
-                        <input type="radio" id="order-asc" class="btn" name="order" value="asc">
-                        <label for="order-asc" title="Oldest First">
-                            <?= jvbIcon('sort-ascending') ?>
-                        </label>
-                    </div>
-                </div>
-                <?= (!$many) ? '' : '</details>' ?>
-        </form>
-
-        <div class="feed-grid"></div>
-
-
-    </section>
-    <?= jvbGetFeedItemTemplate(); ?>
-    <?= jvbGetFeedArtistTemplate(); ?>
-    <?php
-    return ob_get_clean();
-}
diff --git a/build/feed/style-index-rtl.css b/build/feed/style-index-rtl.css
index 6e3c414..425fcc7 100644
--- a/build/feed/style-index-rtl.css
+++ b/build/feed/style-index-rtl.css
@@ -1 +1 @@
-.feed-block{margin:0 auto;max-width:var(--full)}.feed-block:target{scroll-snap-margin-top:5rem;border-radius:v0;outline:0;padding:0;scroll-margin-top:5rem}.feed-block:target .feed-item{outline:double var(--pink-0)}.loading .feed-block{opacity:.7}.label{align-items:center;display:flex;font-size:.9rem;gap:.25rem}.label a:hover{color:var(--pink-0)}.feed-filters{background:var(--overlay-heavy);margin:2rem 0;padding:.25rem 3rem;position:sticky;top:3rem;z-index:15}.feed-filters details[open] summary{background-color:var(--overlay)}.feed-filters summary{justify-content:flex-start}.feed-filters summary>*{order:3}.feed-filters summary .label{order:1}.feed-filters summary .filter-label{order:2}.feed-filters summary:after{order:4}.feed-filters summary #favourites+label{margin-right:auto}.feed-filters summary #favourites+label:hover,.feed-filters summary #favourites:checked+label{background-color:var(--pink-0);border-color:var(--pink-0);color:var(--white)}.feed-filters summary #favourites:checked+label{animation:pop .4s cubic-bezier(.25,.46,.45,.94)}.feed-filters details[open],.feed-filters summary:hover{background-color:var(--overlay-heavy)}.feed-filters:has(#favourites) summary:after{margin-right:1rem}summary>*{order:3}summary .label{order:1}.filter-label{display:inline-block;height:1.3em;margin:0;order:2;padding:0;vertical-align:middle}.filter-label li{height:0;list-style:none;overflow:hidden}.filter-label li.active{height:100%}.filter-group{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.filter-group label{font-weight:100}.filter-group:has(.order-by){justify-content:space-between}.filter-group .order-by,.filter-group .order-direction{display:flex;flex:1;flex-wrap:wrap;gap:.5rem;justify-content:flex-start}.filter-group .order-by .label,.filter-group .order-direction .label{width:100%}.filter-group .order-by label:has(.label),.filter-group .order-direction label:has(.label){gap:.5rem;padding:0 .35rem}.filter-group .order-by label:has(.label) .label,.filter-group .order-direction label:has(.label) .label{font-size:1rem}.filter-group>.label{width:100%}.feed-grid{display:grid;gap:.5rem;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));margin-bottom:2rem;padding:0 4rem;--delay:0s;--increase:.1s}.feed-empty-state{background:var(--base-100);border-radius:var(--innerRadius);grid-column:1/-1;margin:0 auto;max-width:600px;padding:2rem;text-align:center}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder .icon{--w:50%;color:var(--base-200)}.placeholder .icon svg{animation:dance 2.5s ease-in-out infinite}.feed-item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;opacity:0;overflow:hidden;padding:0;position:relative;transition:opacity var(--transition-base) var(--delay)}.feed-item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7}.feed-item[data-loaded=true] img{filter:none;opacity:1}.feed-item a:after,.feed-item a:before{display:none}.feed-item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed-item[data-loaded]{opacity:1}.feed-item[data-loaded]+[data-loaded]{--delay:var(--delay) + var(--increase)}.feed-item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed-item:hover .handle,.feed-item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed-item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed-item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-light);border-radius:var(--innerRadius);bottom:0;right:0;padding:.25rem 1.1rem .25rem .25rem;position:absolute;left:0;z-index:1}.feed-item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;left:.7rem;width:1.5rem;z-index:11}.feed-item label{font-weight:400;text-transform:none}.feed-item label .icon{--w:1.5em}.load-more{align-items:center;background:var(--base);border:2px solid transparent;border-radius:4px;color:var(--contrast);display:flex;font-size:var(--medium);gap:.5rem;margin:1rem auto;opacity:1;padding:.75rem 1.5rem;width:66.666%}.load-more,.load-more[hidden]{transition:all var(--transition-base)}.load-more[hidden]{opacity:0}.load-more:hover{background:var(--pink-50);border-color:var(--contrast);color:var(--white)}button.favourite{align-items:center;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background:var(--overlay-medium);border:none;border-radius:50%;box-shadow:var(--subtle);display:flex;height:2rem;justify-content:center;position:absolute;left:.5rem;top:.5rem;transition:all var(--transition-base);width:2rem;z-index:10}button.favourite:hover{background:var(--base);box-shadow:0 4px 8px rgba(0,0,0,.15);color:var(--pink-0);transform:scale(1.1)}button.favourite.favourited{animation:pop .4s cubic-bezier(.25,.46,.45,.94)}.feed-image{aspect-ratio:1;display:block;overflow:hidden}.feed-image,.feed-images{height:100%;width:100%}.feed-images.multi{display:grid;gap:4px;grid-auto-rows:1fr;grid-template-columns:repeat(3,1fr)}.feed-images.multi>a{aspect-ratio:1;height:100%;width:100%}.feed-images.multi .feed-image{grid-column:span 2;grid-row:span 2}.feed-images img{height:100%;-o-object-fit:cover;object-fit:cover;transition:transform var(--timing) var(--function);width:100%}.feed-images a:hover img{transform:scale(1.05)}.feed-item:nth-of-type(4n+2) .multi .feed-image{grid-column:2/span 2;grid-row:1/span 2}.feed-item:nth-of-type(4n+3) .multi .feed-image{grid-column:1/span 2;grid-row:2/span 2}.feed-item:nth-of-type(4n+4) .multi .feed-image{grid-column:2/span 2;grid-row:2/span 2}.item-info{border-right:1px solid var(--base-200);padding:.25rem}.item-info>div+div{margin-top:.5em;position:relative}.item-info>div+div:before{border-bottom:1px solid var(--base-200);content:"";display:block;right:-.25rem;position:absolute;top:-.3em;width:66.6%}.item-info h3{font-family:var(--body);font-size:1.1rem;font-weight:var(--bWeight);margin:0 0 .5em!important}.item-info span{align-items:center;display:flex;text-transform:uppercase}.item-info .icon{--w:1.1em;display:inline-block;margin-left:.5em;vertical-align:middle}.item-list ul{display:flex;flex-wrap:wrap;gap:.5rem;margin:0;padding:.5em 0}.item-list ul li{list-style:none}.item-list a{background-color:var(--pink-0);border:1px solid transparent;border-radius:4px;color:var(--light-0);line-height:.8;padding:.25em}.item-list a:visited{background-color:var(--pink-100);color:var(--white)}.item-list a:focus,.item-list a:hover,.item-list a:visited:focus,.item-list a:visited:hover{background-color:transparent;border-color:var(--contrast);color:var(--contrast)}.loading{opacity:.7}.loading-overlay{align-items:center;background-color:var(--overlay-medium);bottom:0;display:flex;justify-content:center;right:0;opacity:0;position:fixed;left:0;top:0;transition:opacity .3s ease,visibility .3s ease;visibility:hidden;z-index:9999}.loading .loading-overlay{opacity:1;visibility:visible}.loading .loading-overlay:after{animation:shimmer 3s ease-in-out infinite;background:linear-gradient(-90deg,var(--shimmer));content:"";inset:0;position:absolute;z-index:-1}.loading .loading-overlay .wrapper{align-items:center;background-color:var(--overlay-heavy);border-radius:var(--innerRadius);display:flex;height:300px;justify-content:center;max-width:90%;padding:2rem;position:relative;text-align:center;width:400px;z-index:5}.loading .loading-overlay .wrapper .spinner{--h:150px;--w:calc(var(--h)*2);animation:spin 1s var(--timing) infinite;border-radius:50%;border-top:5px solid var(--pink-0);height:var(--w);right:calc(50% - var(--h));opacity:.5;position:absolute;top:calc(50% - var(--h));width:var(--w);z-index:0}.loading .loading-overlay .wrapper div.icon{height:50px;width:50px}.loading .loading-overlay .wrapper div.icon .icon{--w:100%}.loading .loading-overlay .wrapper div.icon .icon svg{animation:dance 2s ease-in-out infinite;transition:color .3s ease}.loading .loading-overlay .wrapper .status{align-items:center;display:flex;flex-direction:column;height:200px;width:100%;z-index:5}.loading .loading-overlay .wrapper .status h3{color:var(--contrast-200);margin:1.5rem 0 .25rem!important}.loading .loading-overlay .wrapper .status .message{animation:flicker 2s infinite;color:var(--contrast-100);font-size:var(--small);margin:0;max-width:275px}@keyframes highlight{0%,to{box-shadow:none}50%{box-shadow:0 0 0 4px var(--pink-0)}}@keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.3)}75%{transform:scale(.9)}to{transform:scale(1)}}@keyframes bubble{50%{box-shadow:-19px 0 0 3px,-38px 0 0 7px,-57px 0 0 3px}to{box-shadow:-19px 0 0 0,-38px 0 0 3px,-57px 0 0 7px}}@keyframes highlight-pulse{0%,to{box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}50%{box-shadow:0 0 0 8px #ff0080,0 12px 24px rgba(0,0,0,.15)}}@keyframes spin{to{transform:rotate(-1turn)}}@keyframes shimmer{0%{transform:translateX(100%)}50%,to{transform:translateX(-100%)}}@keyframes dance{0%,to{transform:rotate(5deg) scale(1)}50%{transform:rotate(-5deg) scale(1.1)}}@keyframes flicker{0%{opacity:.6}50%{opacity:1}to{opacity:.6}}.feed-item[tabindex="0"]{position:relative}.feed-item[tabindex="0"]:after{border:2px solid transparent;bottom:0;content:"";right:0;pointer-events:none;position:absolute;left:0;top:0;transition:border-color .2s ease}.feed-item[tabindex="0"]:focus:after{border-color:#ff0080}
+.feed-block .feed-filters{padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{right:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--bWeight);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7;transition:opacity var(--transition-base),filter var(--transition-base)}.feed.item img[data-loaded=true]{filter:none;opacity:1}.feed.item[data-timeline]{aspect-ratio:unset}.feed.item[data-timeline] summary{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] summary span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] summary span:first-of-type{bottom:0;left:50%;text-align:left}.feed.item[data-timeline] summary span:last-of-type{right:50%;top:0}.feed.item[data-timeline] summary>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-left:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed.item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed.item:hover .handle,.feed.item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed.item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed.item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-light);border-radius:var(--innerRadius);bottom:0;right:0;padding:.25rem 1.1rem .25rem .25rem;position:absolute;left:0;z-index:1}.feed.item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;left:.7rem;width:1.5rem;z-index:11}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
diff --git a/build/feed/style-index.css b/build/feed/style-index.css
index 882d833..4ef7862 100644
--- a/build/feed/style-index.css
+++ b/build/feed/style-index.css
@@ -1 +1 @@
-.feed-block{margin:0 auto;max-width:var(--full)}.feed-block:target{scroll-snap-margin-top:5rem;border-radius:v0;outline:0;padding:0;scroll-margin-top:5rem}.feed-block:target .feed-item{outline:double var(--pink-0)}.loading .feed-block{opacity:.7}.label{align-items:center;display:flex;font-size:.9rem;gap:.25rem}.label a:hover{color:var(--pink-0)}.feed-filters{background:var(--overlay-heavy);margin:2rem 0;padding:.25rem 3rem;position:sticky;top:3rem;z-index:15}.feed-filters details[open] summary{background-color:var(--overlay)}.feed-filters summary{justify-content:flex-start}.feed-filters summary>*{order:3}.feed-filters summary .label{order:1}.feed-filters summary .filter-label{order:2}.feed-filters summary:after{order:4}.feed-filters summary #favourites+label{margin-left:auto}.feed-filters summary #favourites+label:hover,.feed-filters summary #favourites:checked+label{background-color:var(--pink-0);border-color:var(--pink-0);color:var(--white)}.feed-filters summary #favourites:checked+label{animation:pop .4s cubic-bezier(.25,.46,.45,.94)}.feed-filters details[open],.feed-filters summary:hover{background-color:var(--overlay-heavy)}.feed-filters:has(#favourites) summary:after{margin-left:1rem}summary>*{order:3}summary .label{order:1}.filter-label{display:inline-block;height:1.3em;margin:0;order:2;padding:0;vertical-align:middle}.filter-label li{height:0;list-style:none;overflow:hidden}.filter-label li.active{height:100%}.filter-group{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.filter-group label{font-weight:100}.filter-group:has(.order-by){justify-content:space-between}.filter-group .order-by,.filter-group .order-direction{display:flex;flex:1;flex-wrap:wrap;gap:.5rem;justify-content:flex-start}.filter-group .order-by .label,.filter-group .order-direction .label{width:100%}.filter-group .order-by label:has(.label),.filter-group .order-direction label:has(.label){gap:.5rem;padding:0 .35rem}.filter-group .order-by label:has(.label) .label,.filter-group .order-direction label:has(.label) .label{font-size:1rem}.filter-group>.label{width:100%}.feed-grid{display:grid;gap:.5rem;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));margin-bottom:2rem;padding:0 4rem;--delay:0s;--increase:.1s}.feed-empty-state{background:var(--base-100);border-radius:var(--innerRadius);grid-column:1/-1;margin:0 auto;max-width:600px;padding:2rem;text-align:center}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder .icon{--w:50%;color:var(--base-200)}.placeholder .icon svg{animation:dance 2.5s ease-in-out infinite}.feed-item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;opacity:0;overflow:hidden;padding:0;position:relative;transition:opacity var(--transition-base) var(--delay)}.feed-item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7}.feed-item[data-loaded=true] img{filter:none;opacity:1}.feed-item a:after,.feed-item a:before{display:none}.feed-item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed-item[data-loaded]{opacity:1}.feed-item[data-loaded]+[data-loaded]{--delay:var(--delay) + var(--increase)}.feed-item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed-item:hover .handle,.feed-item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed-item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed-item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-light);border-radius:var(--innerRadius);bottom:0;left:0;padding:.25rem .25rem .25rem 1.1rem;position:absolute;right:0;z-index:1}.feed-item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;right:.7rem;width:1.5rem;z-index:11}.feed-item label{font-weight:400;text-transform:none}.feed-item label .icon{--w:1.5em}.load-more{align-items:center;background:var(--base);border:2px solid transparent;border-radius:4px;color:var(--contrast);display:flex;font-size:var(--medium);gap:.5rem;margin:1rem auto;opacity:1;padding:.75rem 1.5rem;width:66.666%}.load-more,.load-more[hidden]{transition:all var(--transition-base)}.load-more[hidden]{opacity:0}.load-more:hover{background:var(--pink-50);border-color:var(--contrast);color:var(--white)}button.favourite{align-items:center;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background:var(--overlay-medium);border:none;border-radius:50%;box-shadow:var(--subtle);display:flex;height:2rem;justify-content:center;position:absolute;right:.5rem;top:.5rem;transition:all var(--transition-base);width:2rem;z-index:10}button.favourite:hover{background:var(--base);box-shadow:0 4px 8px rgba(0,0,0,.15);color:var(--pink-0);transform:scale(1.1)}button.favourite.favourited{animation:pop .4s cubic-bezier(.25,.46,.45,.94)}.feed-image{aspect-ratio:1;display:block;overflow:hidden}.feed-image,.feed-images{height:100%;width:100%}.feed-images.multi{display:grid;gap:4px;grid-auto-rows:1fr;grid-template-columns:repeat(3,1fr)}.feed-images.multi>a{aspect-ratio:1;height:100%;width:100%}.feed-images.multi .feed-image{grid-column:span 2;grid-row:span 2}.feed-images img{height:100%;-o-object-fit:cover;object-fit:cover;transition:transform var(--timing) var(--function);width:100%}.feed-images a:hover img{transform:scale(1.05)}.feed-item:nth-of-type(4n+2) .multi .feed-image{grid-column:2/span 2;grid-row:1/span 2}.feed-item:nth-of-type(4n+3) .multi .feed-image{grid-column:1/span 2;grid-row:2/span 2}.feed-item:nth-of-type(4n+4) .multi .feed-image{grid-column:2/span 2;grid-row:2/span 2}.item-info{border-left:1px solid var(--base-200);padding:.25rem}.item-info>div+div{margin-top:.5em;position:relative}.item-info>div+div:before{border-bottom:1px solid var(--base-200);content:"";display:block;left:-.25rem;position:absolute;top:-.3em;width:66.6%}.item-info h3{font-family:var(--body);font-size:1.1rem;font-weight:var(--bWeight);margin:0 0 .5em!important}.item-info span{align-items:center;display:flex;text-transform:uppercase}.item-info .icon{--w:1.1em;display:inline-block;margin-right:.5em;vertical-align:middle}.item-list ul{display:flex;flex-wrap:wrap;gap:.5rem;margin:0;padding:.5em 0}.item-list ul li{list-style:none}.item-list a{background-color:var(--pink-0);border:1px solid transparent;border-radius:4px;color:var(--light-0);line-height:.8;padding:.25em}.item-list a:visited{background-color:var(--pink-100);color:var(--white)}.item-list a:focus,.item-list a:hover,.item-list a:visited:focus,.item-list a:visited:hover{background-color:transparent;border-color:var(--contrast);color:var(--contrast)}.loading{opacity:.7}.loading-overlay{align-items:center;background-color:var(--overlay-medium);bottom:0;display:flex;justify-content:center;left:0;opacity:0;position:fixed;right:0;top:0;transition:opacity .3s ease,visibility .3s ease;visibility:hidden;z-index:9999}.loading .loading-overlay{opacity:1;visibility:visible}.loading .loading-overlay:after{animation:shimmer 3s ease-in-out infinite;background:linear-gradient(90deg,var(--shimmer));content:"";inset:0;position:absolute;z-index:-1}.loading .loading-overlay .wrapper{align-items:center;background-color:var(--overlay-heavy);border-radius:var(--innerRadius);display:flex;height:300px;justify-content:center;max-width:90%;padding:2rem;position:relative;text-align:center;width:400px;z-index:5}.loading .loading-overlay .wrapper .spinner{--h:150px;--w:calc(var(--h)*2);animation:spin 1s var(--timing) infinite;border-radius:50%;border-top:5px solid var(--pink-0);height:var(--w);left:calc(50% - var(--h));opacity:.5;position:absolute;top:calc(50% - var(--h));width:var(--w);z-index:0}.loading .loading-overlay .wrapper div.icon{height:50px;width:50px}.loading .loading-overlay .wrapper div.icon .icon{--w:100%}.loading .loading-overlay .wrapper div.icon .icon svg{animation:dance 2s ease-in-out infinite;transition:color .3s ease}.loading .loading-overlay .wrapper .status{align-items:center;display:flex;flex-direction:column;height:200px;width:100%;z-index:5}.loading .loading-overlay .wrapper .status h3{color:var(--contrast-200);margin:1.5rem 0 .25rem!important}.loading .loading-overlay .wrapper .status .message{animation:flicker 2s infinite;color:var(--contrast-100);font-size:var(--small);margin:0;max-width:275px}@keyframes highlight{0%,to{box-shadow:none}50%{box-shadow:0 0 0 4px var(--pink-0)}}@keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.3)}75%{transform:scale(.9)}to{transform:scale(1)}}@keyframes bubble{50%{box-shadow:19px 0 0 3px,38px 0 0 7px,57px 0 0 3px}to{box-shadow:19px 0 0 0,38px 0 0 3px,57px 0 0 7px}}@keyframes highlight-pulse{0%,to{box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}50%{box-shadow:0 0 0 8px #ff0080,0 12px 24px rgba(0,0,0,.15)}}@keyframes spin{to{transform:rotate(1turn)}}@keyframes shimmer{0%{transform:translateX(-100%)}50%,to{transform:translateX(100%)}}@keyframes dance{0%,to{transform:rotate(-5deg) scale(1)}50%{transform:rotate(5deg) scale(1.1)}}@keyframes flicker{0%{opacity:.6}50%{opacity:1}to{opacity:.6}}.feed-item[tabindex="0"]{position:relative}.feed-item[tabindex="0"]:after{border:2px solid transparent;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;transition:border-color .2s ease}.feed-item[tabindex="0"]:focus:after{border-color:#ff0080}
+.feed-block .feed-filters{padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{left:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--bWeight);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7;transition:opacity var(--transition-base),filter var(--transition-base)}.feed.item img[data-loaded=true]{filter:none;opacity:1}.feed.item[data-timeline]{aspect-ratio:unset}.feed.item[data-timeline] summary{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] summary span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] summary span:first-of-type{bottom:0;right:50%;text-align:right}.feed.item[data-timeline] summary span:last-of-type{left:50%;top:0}.feed.item[data-timeline] summary>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-right:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed.item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed.item:hover .handle,.feed.item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed.item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed.item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-light);border-radius:var(--innerRadius);bottom:0;left:0;padding:.25rem .25rem .25rem 1.1rem;position:absolute;right:0;z-index:1}.feed.item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;right:.7rem;width:1.5rem;z-index:11}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 7427c60..2aa4bed 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'db2e1dcc97363f747c51');
+<?php return array('dependencies' => array(), 'version' => '40e69d90292156d1e263');
diff --git a/build/feed/view.js b/build/feed/view.js
index 79d9a63..de6b42c 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.loading=window.jvbLoading,this.error=window.jvbError,this.container=document.querySelector("section.feed-block"),this.container&&(this.openGallery=!1,this.initElements(),this.addPlaceholders(),this.config={api:feedSettings.apiUrl,nonce:feedSettings.nonce,user:jvbSettings.currentUser||null,source:"",context:"",highlight:null,gallery:!1,showAuthor:!0,showDate:!1,view:localStorage.getItem("feedViewMode")||"grid",...this.container.dataset},this.taxonomies={},this.rendered={},this.feed={imageLoadThreshold:5,lazyLoadOffset:"100px",gallery:[],loaded:0,intsersectionObserver:null,templates:new Map},this.isLoading=!1,this.hasMore=!0,this.retries={count:0,max:3,delay:1e3},this.page=1,this.order="DESC",this.orderby="date",this.gallery=!!this.config.gallery&&new window.jvbGallery(document.querySelector("dialog.gallery"),{imageWrapper:".item",loadMore:()=>this.fetchFeed.bind(this)}),this.initListeners(),1===this.page?this.processURLFilters():this.updateFilters())}initElements(){this.filterSelector="form.feed-filters",this.filterForm=this.container.querySelector(this.filterSelector),this.grid=this.container.querySelector(".item-grid"),this.loadMore=this.container.querySelector(".load-more"),this.filterControls=this.container.querySelector(".filter-actions"),this.contentTypes=Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map((e=>e.value)),this.selectedTerms=this.container.querySelector(".selected-items-section .selected-items")}initListeners(){window.addEventListener("popstate",this.handlePopState.bind(this)),document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&(this.loadImage(e.target),this.imageObserver.unobserve(e.target))}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?(this.resizeObserver=new ResizeObserver(window.debounce((()=>{this.updateImageSizes()}),250)),this.resizeObserver.observe(this.container)):window.addEventListener("resize",window.debounce((()=>{this.updateImageSizes()}),250)),this.taxonomies={},this.container.querySelectorAll(".jvb-selector:not([hidden])").forEach((e=>{let t=e.dataset.taxonomy;Object.hasOwn(this.taxonomies,t)||(this.taxonomies[t]=new window.jvbTaxonomySelector(e,{multiple:!0,feed:!0,selected:{},onClose:()=>this.setSelectedTerms(t)}))}))}handlePopState(e){e.state&&e.state.filters&&this.processURLFilters()&&(this.resetPage(),this.fetchFeed(),this.a11y.announce("Feed filters updated from browser history."))}processURLFilters(){const e=new URLSearchParams(window.location.search);if(e.toString()){for(var[t,i]of(["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get("f_"+t);e.delete("f_"+t),i&&this.filterForm.querySelector(`input[name="${t}"][value="${i}"]`)&&(this.filterForm.querySelector(`input[name="${t}"][value="${i}"]`).checked=!0)})),Object.entries(Object.fromEntries(e))))t=t.replace("f_",""),this.contentTypes.includes(t)?this.openGallery=i:(this.taxonomies[t].addTermsFromURL(i),this.setSelectedTerms(t));this.updateFilters()}else this.updateFilters()}handleClick(e){if(e.target.classList.contains("load-more")||e.target.closest(".load-more"))this.fetchFeed(!1),e.target.disabled=!0;else if(e.target.classList.contains("clear-filters")||e.target.closest(".clear-filters"))this.resetFilters();else if(this.config.gallery&&e.target.closest(".feed-image"))this.gallery.handleGalleryOpen(e);else if(e.target.classList.contains(".remove-item")||e.target.closest(".remove-item")){let t=e.target.closest(".selected-item"),i=t.dataset.taxonomy;this.taxonomies[i].removeSelectedTerm(t.dataset.id),this.setSelectedTerms(i),this.updateFilters()}}handleChange(e){e.target.closest(this.filterSelector)&&(this.resetPage(),window.removeChildren(this.grid),this.addPlaceholders(),this.updateFilters())}updateFilters(){this.page=1;const e=new URLSearchParams(window.location.search);let t=Object.fromEntries(new FormData(this.filterForm)),i=[];for(let[r,s]of Object.entries(t)){let a=!1;switch(r){case"content":s!==this.contentTypes[0]?a=!0:e.delete("f_"+r);break;case"orderby":"date"!==s&&(a=!0);break;case"order":"desc"!==s&&(a=!0);break;default:a=!0}a||e.delete("f_"+r),a&&!1!==s&&""!==s&&e.set("f_"+r,s),""!==s&&i.push(s);const o=`${window.location.pathname}?${e.toString()}`;history.pushState(t,"",o)}this.filters=t,this.updateContentFor(t.content),this.updateFilterControls(),this.loading.setContent(i),this.fetchFeed(!0)}updateFilterControls(){this.filterControls.hidden=this.selectedTerms.children.length<2}updateContentFor(e){this.filterForm.querySelectorAll(".jvb-selector").forEach((t=>{let i=t.dataset.for.includes(e);if(t.hidden=!i,!i){let e=t.dataset.taxonomy;this.clearSelectedTerms(e)}})),this.filterForm.querySelectorAll("input[data-for]").forEach((t=>{t.hidden=!t.dataset.for.includes(e)})),this.filterForm.querySelectorAll('input[name="order"]').forEach((e=>{e.hidden="random"===this.filters.order}))}clearSelectedTerms(e){this.filterForm.querySelector(`input[name="${e}"]`).value="",Object.hasOwn(this.taxonomies,e)&&(this.taxonomies[e].selectedItems={})}setSelectedTerms(e){let t=this.filterForm.querySelector(`input[name="${e}"]`);t.value="";let i=this.taxonomies[e].selectedTerms;if(!window.isEmptyObject(i)){let e=Object.keys(i);t.value=e.join(",")}this.updateFilters()}nextPage(){this.hasMore&&this.page++}resetPage(){this.page=1,this.hasMore=!0}resetState(){this.resetPage(!0),this.isLoading=!1,this.retries={count:0,max:3,delay:1e3}}resetFilters(){this.filterForm.reset(),this.filterForm.querySelector('input[name="content"]').checked=!0,this.filterForm.querySelector('input[name="orderby"][value="date"]').checked=!0,this.page=1,this.updateFilters()}buildFilterRequest(){let e={};for(let[t,i]of Object.entries(this.filters))!1!==i&&""!==i&&(e[t]=i);return e.page=parseInt(this.page),this.container.dataset.context&&(e.context=this.container.dataset.context),this.container.dataset.source&&(e.source=this.container.dataset.source),new URLSearchParams(e).toString()}async fetchFeed(e=!1,t=!1){if(this.isLoading)return!1;this.loading.showLoading(this.filters);try{1===this.page&&(window.removeChildren(this.grid),this.addPlaceholders());const e=await this.cache.fetchWithCache(`${this.config.api}feed?${this.buildFilterRequest()}`,{method:"GET"},{context:"feed",forceRefresh:!0});return e&&e.items&&0!==e.items.length?(this.hasMore=e.has_more,this.renderItems(e.items,this.page>1),this.hasMore&&this.nextPage(),!0):(1===this.page&&this.showEmptyState(),this.hasMore=!1,!1)}catch(e){this.handleError(e)}finally{this.loading.hideLoading(),!1!==this.openGallery&&(this.gallery.openWhenReady=this.openGallery,this.openGallery=!1),this.loadMore.disabled=!1,this.loadMore.hidden=!this.hasMore}}removePlaceholders(){this.grid.querySelector(".placeholder")&&window.removeChildren(this.grid)}showEmptyState(){window.removeChildren(this.grid);let e=window.getTemplate("emptyState"),t=Object.hasOwn(this.filters,"favourites")&&!0===this.filters.favourites;t&&([e.querySelector("h3").textContent,e.querySelector("p:first-of-type").textContent,e.querySelector("p:last-of-type").textContent]=["♡ BLANK CANVAS ♡","You haven't fallen in love with any pieces... yet!","Hit that heart icon when something stops your scroll — your dream collection is waiting to start."]),this.grid.append(e),this.a11y.announceEmpty(t)}handleError(e){return this.error.handleApiError(e,{component:"Feed Block",action:"loaditems"},(()=>this.fetchFeed()))}addPlaceholders(){let e=this.contentTypes.length-1;for(let t=0;t<9;t++){let t=window.getTemplate("placeholderTemplate"),i=Math.floor(Math.random()*e+1),r=window.getIcon(this.contentTypes[i]).cloneNode(!0);t.append(r),this.grid.append(t)}}renderItems(e,t=!1){if(t||(window.removeChildren(this.grid),this.addPlaceholders()),0===e.length)return void this.a11y.announceUpdate(0,t);const i=document.createDocumentFragment(),r=s=>{const a=Math.min(s+10,e.length);for(let t=s;t<a;t++){const r=e[t],s=this.createItemElement(r);i.appendChild(s),this.imageObserver.observe(s)}a<e.length?requestAnimationFrame((()=>{r(a)})):(this.removePlaceholders(),this.grid.appendChild(i),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.hasMore))};e.length>0?r(0):this.a11y.announceUpdate(0,t)}createItemElement(e){var t;if(this.rendered[e.icon]||(this.rendered[e.icon]=new Map),this.rendered[e.icon].has(e.id))return this.rendered[e.icon].get(e.id);const i=null!==(t=window.isFavourited(e.icon,e.id))&&void 0!==t&&t,r=window.getTemplate("feed-item");r.id=`${e.icon}-${e.id}`,r.dataset.id=e.id,r.classList.add(e.icon),e.umami_view&&this.buildUmamiData(r,e.umami_view);let s=r.querySelector("button.favourite");[s.dataset.id,s.dataset.type,s.dataset.artist,s.title]=[e.id,e.icon,e.user_id,i?"Remove from Favourites":"Add to Favourites"];let a=e.order,o=r.querySelector(".item"),n=r.querySelector(".item-list"),l=(r.querySelector(".feed-images"),r.querySelector("summary")),d=r.querySelector(".item-info");for(let[t,i]of Object.entries(a)){let t,s=e[i];if("title"===i)t=r.querySelector("h3 a"),""!==e.title?([t.textContent,t.href,t.url]=[e.title,e.url,`Learn more about this ${e.icon}`],""!==e.icon&&t.closest("h3").prepend(window.getIcon(e.icon)),e.umami_click&&this.buildUmamiData(t,e.umami_click)):t.remove();else if(Object.hasOwn(s,"terms")){if(0===s.terms.length)continue;let e=n.cloneNode(!0),t=e.querySelector(".label"),i=e.querySelector("ul"),r=e.querySelector("li");s.label&&(t.textContent=s.label),s.icon&&t.prepend(window.getIcon(s.icon)),s.label||s.icon||t.remove(),s.terms.forEach((e=>{let t=r.cloneNode(!0),s=t.querySelector("a");[s.href,s.title,s.textContent]=[e.url,`Learn more about ${e.title}`,e.title],e.umami_click.length>0&&this.buildUmamiData(s,e.umami_click),i.append(t)})),r.remove(),d.appendChild(e)}else if(Object.hasOwn(s,"value")&&""!==s.value){let e=o.cloneNode(!0),t=e.querySelector(".label"),i=e.querySelector("a"),r=e.querySelector("p");Object.hasOwn(s,"label")&&(t.textContent=s.label),Object.hasOwn(s,"icon")&&t.prepend(window.getIcon(s.icon)),Object.hasOwn(s,"icon")||Object.hasOwn(s,"label")||t.remove(),Object.hasOwn(s,"url")?(r.remove(),[i.textContent,i.href,i.title]=[s.value,s.url,`Learn more about ${s.value}`]):(i.remove(),r.textContent=s.value),d.appendChild(e)}else if("image"===i){let t=l.querySelector(".feed-images"),i=t.querySelector("a"),r=i.cloneNode(!0);this.config.gallery||(r.href=e.url),r.classList.add("feed-image"),this.buildImageData(r.querySelector("img"),e.image),t.append(r),e.content?.length>0&&(t.classList.add("multi"),e.content.forEach((e=>{let r=i.cloneNode(!0);this.config.gallery||(r.href=e.url);let s=r.querySelector("img");s.src=e.image.small,s.alt=e.image.alt,t.append(r)}))),i.remove()}}return o.remove(),n.remove(),this.rendered[e.icon].set(e.id,r),r}buildImageData(e,t){"string"==typeof t.tiny&&([e.src,e.dataset.small,e.dataset.medium,e.dataset.large,e.alt]=[t.tiny,t.small,t.medium,t.large,t.alt])}buildUmamiData(e,t){for(let[i,r]of Object.entries(t))e.dataset[i]=r}loadImage(e){const t=e.querySelector("img");if(!t)return;const i=this.getImageSize();t.src=t.dataset[i]||t.dataset.src,e.setAttribute("data-loaded","true")}updateImageSizes(){const e=this.getImageSize();this.grid.querySelectorAll(".item").forEach((t=>{const i=t.querySelector("img");i&&i.dataset[e]&&i.src!==i.dataset[e]&&(i.src=i.dataset[e])}))}getImageSize(){const e=window.innerWidth;return e>1024||e>500?"medium":"small"}}document.addEventListener("DOMContentLoaded",(()=>{window.feedBlock=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.cache=new window.jvbCache("feed"),this.error=window.jvbError,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.initElements(),this.initFilters(),this.loadWhenAble())}loadWhenAble(){"requestIdleCallback"in window?requestIdleCallback((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),100)}initElements(){this.currentTaxonomies=new Set,this.taxonomyFilters={},this.elements={filterTrigger:"[data-filter]",filters:{content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},selectedTax:".selected-terms",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=this.ui.filterContainer.querySelectorAll('[name="content"]'),this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content.length>0?this.contentTypes=Array.from(this.ui.content).map((e=>e.value)):this.contentTypes=[this.container.dataset.content],this.ui.taxonomies.length>0?this.taxonomies=Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):this.taxonomies=[]}async initTaxonomies(){this.selector=window.jvbSelector;const e=document.querySelectorAll('[data-filter="taxonomy"]');this.selector.isInitializing=!0,e.forEach((e=>{const t=e.dataset.taxonomy;this.currentTaxonomies.add(t),this.selector.registerFilterButton(e,{button:e,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax})})),setTimeout((()=>{this.selector.batchFetchTaxonomies(),this.selector.isInitializing=!1}),0),this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;t.size>0?this.taxonomyFilters[i]=Array.from(t.keys()):delete this.taxonomyFilters[i];let s={page:1};Object.keys(this.taxonomyFilters).length>0&&(s.taxonomy=this.taxonomyFilters),this.updateFilter(s)}clearAllTaxonomies(){this.taxonomyFilters={},window.removeChildren(this.ui.selectedTax),this.updateFilter({taxonomy:null,page:1})}initFilters(){this.filters={content:this.contentTypes[0],orderby:"date",order:"desc",page:1},this.config.context&&(this.filters.context=this.config.context),this.config.source&&(this.filters.source=this.config.source),this.processCachedFilters(),this.processURLFilters(),this.syncUIToFilters()}syncUIToFilters(){Object.entries(this.filters).forEach((([e,t])=>{const i=this.ui.filterContainer.querySelector(`[data-filter="${e}"][value="${t}"]`);i&&(i.checked=!0)})),this.updateContentFor(this.filters.content)}nextPage(){this.store.setFilter("page",this.store.filters.page++)}initStore(){this.addPlaceholders(),this.store=window.jvbStore.register("feed",{endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content",delayFetch:!0}),this.store.subscribe(((e,t)=>{"data-loaded"===e&&(this.renderItems(),this.ui.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse.has_more&&(this.ui.loadMore.hidden=!this.store.lastResponse.has_more))}))}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse&&this.store.lastResponse.has_more&&this.nextPage()}))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get(`f_${t}`);if(i){this.filters[t]=i;let e=this.ui.filters[t];e&&(e.checked=!0)}}));let t=!1;if(e.forEach(((e,i)=>{if(i.startsWith("f_tax_")){t=!0;const s=i.replace("f_tax_","");this.taxonomyFilters[s]||(this.taxonomyFilters[s]=[]),this.taxonomyFilters[s]=e.split(",").map(Number)}})),t)for(let[e,t]in Object.entries(this.taxonomyFilters)){let i=this.ui.filterContainer.querySelector(`[data-taxonomy="${e}"]`);i&&(i.dataset.fieldId?(this.selector.get(i.dataset.fieldId).selectedTerms=new Set(t),this.selector.initFieldDisplay(i.dataset.fieldId)):this.selector.registerField(i,{button:i,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax,selectedItems:t}))}return!0}updateURL(){const e=new URLSearchParams;["content","order","orderby","match"].forEach((t=>{this.filters[t]&&e.set(`f_${t}`,this.filters[t])})),Object.entries(this.taxonomyFilters).forEach((([t,i])=>{i.length>0&&e.set(`f_tax_${t}`,i.join(","))}));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;window.history.pushState({filters:this.filters},"",t)}renderItems(){let e=this.store.getFiltered();if(1===this.store.filters.page&&(window.removeChildren(this.ui.grid),this.addPlaceholders()),0===e.length)return void this.a11y.announceItems(0,this.store.filters.page>0);const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.appendChild(r)}r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.observeImages(this.ui.grid),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,this.store.filters.page>1,this.store.hasMore))};e.length>0?i(0):this.a11y.announceItems(0,this.store.filters.page>1,!1),this.ui.filters.match.hidden=window.isEmptyObject(this.taxonomyFilters),this.ui.clearFilter.hidden=window.isEmptyObject(this.taxonomyFilters)}createItemElement(e){let t=window.getTemplate("feed-item");return Object.hasOwn(t.dataset,"timeline")?this.createTimelineElement(e,t):t}createTimelineElement(e,t){var i,s;let[r,a,o,n,l,h,d,c,m,u]=[t,t.querySelector("a"),t.querySelector("img.before"),t.querySelector("img.after"),t.querySelector("summary span:last-of-type"),t.querySelector("p.started time"),t.querySelector("p.updated time"),t.querySelector("p.total b"),t.querySelector(".term-list"),Object.values(e.fields.order)],f=u.length-1,y=e.images[u[0].post_thumbnail],g=e.images[u[f].post_thumbnail];return[r.dataset.id,a.href,o.src,o.dataset.small,o.dataset.medium,n.src,n.dataset.small,n.dataset.medium,l.textContent,h.textContent,d.textContent,c.textContent]=[e.id,e.url,y.tiny,y.small,y.medium,g.tiny,g.small,g.medium,`${l.textContent} ${f} Tx`,null!==(i=u[0].date)&&void 0!==i?i:e.date,null!==(s=u[f].date)&&void 0!==s?s:"",`${f} Treatments`],t}addPlaceholders(){let e=this.contentTypes.length;for(let t=0;t<12;t++){let t,i=window.getTemplate("placeholderTemplate"),s=Math.floor(Math.random()*e);t=this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[s])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),i.appendChild(t),this.ui.grid.appendChild(i)}}removePlaceholders(){this.ui.grid.querySelector(".placeholder")&&window.removeChildren(this.ui.grid)}updateFilter(e){let t=["taxonomy","favourites","match",...Object.keys(this.filters)];e=Object.keys(e).filter((e=>t.includes(e))).reduce(((t,i)=>(t[i]=e[i],t)),{}),window.getDifferences.map(this.filters,e)&&(this.filters={...this.filters,...e},this.updateURL(),this.store.setFilters(e))}updateContentFor(e){this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]').forEach((t=>{const i=t.dataset.for?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)})),this.ui.filterContainer.querySelectorAll("[data-for]").forEach((t=>{const i=t.dataset.for?.split(",")||[];i.length>0&&(t.hidden=!i.includes(e),t.hidden&&t.checked&&(t.checked=!1))}));const t=this.ui.filterContainer.querySelector('[name="orderby"]:checked');this.updateOrderDirectionVisibility(t?.value)}updateOrderDirectionVisibility(e){const t=this.ui.filterContainer.querySelector(".order-direction");if(t){const i=t.dataset.forOrder?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)}}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.imageObserver=null,this.resizeObserver=null,"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{this.loadImage(e.target),this.imageObserver.unobserve(e.target)}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?this.resizeObserver=new ResizeObserver(window.debounce((()=>{this.updateImageSizes()}),250)):window.addEventListener("resize",window.debounce((()=>{this.updateImageSizes()}),250)),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}loadImage(e){const t=this.getAppropriateImageSize(e);t&&t!==e.src&&(e.src=t,e.dataset.loaded="true")}getAppropriateImageSize(e){return window.innerWidth<768&&e.dataset.small?e.dataset.small:e.dataset.medium?e.dataset.medium:e.src}observeImages(e){e.querySelectorAll("img[data-small], img[data-medium]").forEach((e=>{e.dataset.loaded||this.imageObserver.observe(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(function(){window.feedBlock=new e}))})();
\ No newline at end of file
diff --git a/build/gmbreviews/render.php b/build/gmbreviews/render.php
index 47ad2b5..7aa2dcb 100644
--- a/build/gmbreviews/render.php
+++ b/build/gmbreviews/render.php
@@ -108,6 +108,7 @@
 			<?php
 			foreach ($reviews as $review) {
 				$reviewer = $review['reviewer']['displayName'] ?? 'Anonymous';
+				$reviewer = strtok($reviewer, ' ');
 				$profilePhoto = $review['reviewer']['profilePhotoUrl'] ?? '';
 				$rating = $review['starRating'] ?? 0;
 				$rating = match($rating) {
@@ -122,20 +123,27 @@
 				$date = $review['updateTime'] ?? '';
 				?>
 				<li>
-					<article class="review">
-						<header class="row btw">
+					<blockquote class="review">
+						<?php
+						// Review text
+						if (!empty($comment)) { ?>
+							<div class="content review">
+								<?= apply_filters('wpautop', $comment) ?>
+							</div>
+						<?php } ?>
+						<cite class="row start nowrap">
 							<?php if (!empty($profilePhoto)) { ?>
 								<img src="<?=esc_url($profilePhoto)?>"
-									alt="<?=esc_attr($reviewer)?>"
-									'loading="lazy">
+									 alt="<?=esc_attr($reviewer)?>"
+								'loading="lazy">
 							<?php } else { ?>
 								<div class="avatar">
-								<?= jvbIcon('user-circle')?>
+									<?= jvbIcon('user-circle')?>
 								</div>
 							<?php } ?>
 
-							<div class="col end">
-								<h4><?= esc_html($reviewer)?></h4>
+							<div class="row start wrap">
+								<p><?= esc_html($reviewer)?></p>
 								<?php
 								// Date
 								if ($showDate && !empty($date)) {
@@ -154,16 +162,8 @@
 									</div>
 								<?php } ?>
 							</div>
-
-						</header>
-						<?php
-						// Review text
-						if (!empty($comment)) { ?>
-							<div class="review">
-								<?= apply_filters('the_content', $comment) ?>
-							</div>
-						<?php } ?>
-					</article>
+						</cite>
+					</blockquote>
 				</li>
 			<?php
 			}
diff --git a/build/gmbreviews/style-index-rtl.css b/build/gmbreviews/style-index-rtl.css
index a413f26..66c34c0 100644
--- a/build/gmbreviews/style-index-rtl.css
+++ b/build/gmbreviews/style-index-rtl.css
@@ -1 +1 @@
-.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{display:inline-block;vertical-align:middle}.gmb-reviews ul{list-style:none;margin:0;padding:0}.gmb-reviews ul li{margin:2rem 0;position:relative}.gmb-reviews ul li:nth-of-type(odd){right:-2rem}.gmb-reviews ul li:nth-of-type(2n){left:-2rem}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:var(--alignWide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;padding:0}.gmb-reviews ul li{margin:2rem 0;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){right:-2rem}.gmb-reviews ul li:nth-of-type(2n){left:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
diff --git a/build/gmbreviews/style-index.css b/build/gmbreviews/style-index.css
index 963482d..d40b467 100644
--- a/build/gmbreviews/style-index.css
+++ b/build/gmbreviews/style-index.css
@@ -1 +1 @@
-.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{display:inline-block;vertical-align:middle}.gmb-reviews ul{list-style:none;margin:0;padding:0}.gmb-reviews ul li{margin:2rem 0;position:relative}.gmb-reviews ul li:nth-of-type(odd){left:-2rem}.gmb-reviews ul li:nth-of-type(2n){right:-2rem}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:var(--alignWide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;padding:0}.gmb-reviews ul li{margin:2rem 0;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){left:-2rem}.gmb-reviews ul li:nth-of-type(2n){right:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
diff --git a/build/timeline/block.json b/build/timeline/block.json
new file mode 100644
index 0000000..ae2a0f5
--- /dev/null
+++ b/build/timeline/block.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "https://schemas.wp.org/trunk/block.json",
+  "apiVersion": 3,
+  "name": "jvb/timeline",
+  "version": "0.1.0",
+  "title": "Timeline",
+  "category": "jvb",
+  "icon": "shortcode",
+  "description": "Outputs a single timeline post in a cool way",
+  "example": {},
+  "supports": {
+    "html": false,
+    "align": [
+      "wide",
+      "full"
+    ]
+  },
+  "textdomain": "jvb",
+  "selectors": {
+    "root": ".timeline"
+  },
+  "editorScript": "file:./index.js",
+  "editorStyle": "file:./index.css",
+  "style": "file:./style-index.css",
+  "viewScript": "file:./view.js"
+}
\ No newline at end of file
diff --git a/build/timeline/index-rtl.css b/build/timeline/index-rtl.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/build/timeline/index-rtl.css
@@ -0,0 +1 @@
+
diff --git a/build/timeline/index.asset.php b/build/timeline/index.asset.php
new file mode 100644
index 0000000..348680b
--- /dev/null
+++ b/build/timeline/index.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-i18n'), 'version' => 'b58129e5087f682034eb');
diff --git a/build/timeline/index.css b/build/timeline/index.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/build/timeline/index.css
@@ -0,0 +1 @@
+
diff --git a/build/timeline/index.js b/build/timeline/index.js
new file mode 100644
index 0000000..beab3c8
--- /dev/null
+++ b/build/timeline/index.js
@@ -0,0 +1 @@
+(()=>{"use strict";var r,e={798:()=>{const r=window.wp.blocks,e=window.wp.i18n,o=window.wp.blockEditor,i=window.ReactJSXRuntime,t=JSON.parse('{"UU":"jvb/timeline"}');(0,r.registerBlockType)(t.UU,{edit:function(){return(0,i.jsx)("p",{...(0,o.useBlockProps)(),children:(0,e.__)("Will output the timeline","jvb")})}})}},o={};function i(r){var t=o[r];if(void 0!==t)return t.exports;var n=o[r]={exports:{}};return e[r](n,n.exports,i),n.exports}i.m=e,r=[],i.O=(e,o,t,n)=>{if(!o){var l=1/0;for(v=0;v<r.length;v++){for(var[o,t,n]=r[v],s=!0,a=0;a<o.length;a++)(!1&n||l>=n)&&Object.keys(i.O).every((r=>i.O[r](o[a])))?o.splice(a--,1):(s=!1,n<l&&(l=n));if(s){r.splice(v--,1);var p=t();void 0!==p&&(e=p)}}return e}n=n||0;for(var v=r.length;v>0&&r[v-1][2]>n;v--)r[v]=r[v-1];r[v]=[o,t,n]},i.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{var r={643:0,627:0};i.O.j=e=>0===r[e];var e=(e,o)=>{var t,n,[l,s,a]=o,p=0;if(l.some((e=>0!==r[e]))){for(t in s)i.o(s,t)&&(i.m[t]=s[t]);if(a)var v=a(i)}for(e&&e(o);p<l.length;p++)n=l[p],i.o(r,n)&&r[n]&&r[n][0](),r[n]=0;return i.O(v)},o=globalThis.webpackChunkjvb=globalThis.webpackChunkjvb||[];o.forEach(e.bind(null,0)),o.push=e.bind(null,o.push.bind(o))})();var t=i.O(void 0,[627],(()=>i(798)));t=i.O(t)})();
\ No newline at end of file
diff --git a/build/timeline/style-index-rtl.css b/build/timeline/style-index-rtl.css
new file mode 100644
index 0000000..7f9d2fa
--- /dev/null
+++ b/build/timeline/style-index-rtl.css
@@ -0,0 +1 @@
+main{--gap:0}main section:last-of-type{margin-bottom:0}#at-a-glance{max-width:var(--alignWide);--gap:0}#at-a-glance img{border:2px solid var(--action-0);height:auto;width:100%}#at-a-glance h3{font-size:var(--small)}#at-a-glance .before img{border-right:0;border-left-width:1px;border-top:0}#at-a-glance .after img{border-bottom:0;border-right-width:1px;border-left:0}.timeline-point.timeline-point{--lineWidth:1px;--gap:2rem;background-color:var(--base);margin:0;max-width:100vw;overflow:hidden;padding:0;position:relative}.timeline-point.timeline-point .open-gallery{border-radius:4px;padding:.5rem;position:sticky;width:40%}.timeline-point.timeline-point .info{padding:1rem .5rem .5rem;position:relative;width:60%}.timeline-point.timeline-point .info h2{font-size:var(--medium);margin:0 0 .5rem;position:relative}.timeline-point.timeline-point .info h2 .icon{--w:2.5rem;background-color:var(--action-100);right:-2.5rem;position:absolute;top:.25rem;transform:rotate(90deg)}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{background-color:var(--action-0);content:"";display:block;height:100%;right:45%;position:absolute;width:var(--lineWidth)}.timeline-point.timeline-point:before{height:1rem}.timeline-point.timeline-point:after{top:4rem}.timeline-point.timeline-point#before-treatment:before,.timeline-point.timeline-point:last-of-type:after{display:none}@media(min-width:768px){#at-a-glance h3{font-size:var(--xlarge)}.timeline-point.timeline-point{--gap:4rem}.timeline-point.timeline-point .open-gallery{width:50%}.timeline-point.timeline-point .info{padding:25vh 1rem 1rem;width:50%}.timeline-point.timeline-point .info h2 .icon{--w:4rem;right:-4.15rem;top:0}.timeline-point.timeline-point .info a{align-items:center;display:flex;flex-wrap:wrap}.timeline-point.timeline-point .info time{font-size:var(--small);text-transform:uppercase}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{right:calc(50% + 2rem)}.timeline-point.timeline-point:before{height:calc(25vh - 2rem)}.timeline-point.timeline-point:after{top:calc(25vh + 6rem)}}
diff --git a/build/timeline/style-index.css b/build/timeline/style-index.css
new file mode 100644
index 0000000..eaaa6d9
--- /dev/null
+++ b/build/timeline/style-index.css
@@ -0,0 +1 @@
+main{--gap:0}main section:last-of-type{margin-bottom:0}#at-a-glance{max-width:var(--alignWide);--gap:0}#at-a-glance img{border:2px solid var(--action-0);height:auto;width:100%}#at-a-glance h3{font-size:var(--small)}#at-a-glance .before img{border-left:0;border-right-width:1px;border-top:0}#at-a-glance .after img{border-bottom:0;border-left-width:1px;border-right:0}.timeline-point.timeline-point{--lineWidth:1px;--gap:2rem;background-color:var(--base);margin:0;max-width:100vw;overflow:hidden;padding:0;position:relative}.timeline-point.timeline-point .open-gallery{border-radius:4px;padding:.5rem;position:sticky;width:40%}.timeline-point.timeline-point .info{padding:1rem .5rem .5rem;position:relative;width:60%}.timeline-point.timeline-point .info h2{font-size:var(--medium);margin:0 0 .5rem;position:relative}.timeline-point.timeline-point .info h2 .icon{--w:2.5rem;background-color:var(--action-100);left:-2.5rem;position:absolute;top:.25rem;transform:rotate(-90deg)}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{background-color:var(--action-0);content:"";display:block;height:100%;left:45%;position:absolute;width:var(--lineWidth)}.timeline-point.timeline-point:before{height:1rem}.timeline-point.timeline-point:after{top:4rem}.timeline-point.timeline-point#before-treatment:before,.timeline-point.timeline-point:last-of-type:after{display:none}@media(min-width:768px){#at-a-glance h3{font-size:var(--xlarge)}.timeline-point.timeline-point{--gap:4rem}.timeline-point.timeline-point .open-gallery{width:50%}.timeline-point.timeline-point .info{padding:25vh 1rem 1rem;width:50%}.timeline-point.timeline-point .info h2 .icon{--w:4rem;left:-4.15rem;top:0}.timeline-point.timeline-point .info a{align-items:center;display:flex;flex-wrap:wrap}.timeline-point.timeline-point .info time{font-size:var(--small);text-transform:uppercase}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{left:calc(50% + 2rem)}.timeline-point.timeline-point:before{height:calc(25vh - 2rem)}.timeline-point.timeline-point:after{top:calc(25vh + 6rem)}}
diff --git a/build/timeline/view.asset.php b/build/timeline/view.asset.php
new file mode 100644
index 0000000..f534533
--- /dev/null
+++ b/build/timeline/view.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array(), 'version' => '31d6cfe0d16ae931b73c');
diff --git a/build/timeline/view.js b/build/timeline/view.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build/timeline/view.js
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 966276f..43831f2 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -14,13 +14,9 @@
 class CustomBlocks
 {
     protected CacheManager $cache;
-    protected CacheManager $imgCache;
     public function __construct()
     {
         $this->cache = CacheManager::for('blocks', WEEK_IN_SECONDS);
-		$this->imgCache = CacheManager::for('images', WEEK_IN_SECONDS);
-
-
 		add_filter('render_block', [$this, 'render'], 999, 3);
 
 
@@ -241,7 +237,7 @@
 		if (array_key_exists('useFeaturedImage', $attrs)) {
 			global $post;
 			$ID = get_post_thumbnail_id($post->ID);
-		} else {
+		} else if (array_key_exists('id', $attrs)) {
 			$ID = (int)$attrs['id'];
 		}
 
@@ -781,7 +777,6 @@
     protected function render_core_template_part(array $block, string $content):string
     {
 
-		$check = ['header', 'footer'];
 		$isHeaderTemplate = (
 			(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'))
@@ -792,7 +787,8 @@
 		) ? 'footer' : false;
 
 
-        if ($isHeaderTemplate || $isFooterTemplate) {
+        if (($isHeaderTemplate || $isFooterTemplate)) {
+
 			$tag = $isHeaderTemplate ?: $isFooterTemplate ?: 'div';
 
             $breadcrumbs = $themeSwitch = $afterHeader = $beforeHeader = $footerText= '';
@@ -821,12 +817,21 @@
 					$beforeHeader = '<section class="pre-footer">'.$beforeHeader.'</section>';
 				}
 					$footerText = jvbRandomFooterText();
-            }
+			}
+//			jvbDump($beforeHeader,'beforeHeader');
+//			jvbDump('<'.$tag.$this->getClassesAndStyles($block['attrs']).'>','tag');
+//			jvbDump($themeSwitch,'themeSwitch');
+//			jvbDump($this->inside($block, $tag, $content),'inside');
+//			jvbDump($footerText,'footerText');
+//			jvbDump($afterHeader, 'afterheader');
+//			jvbDump($breadcrumbs, 'breadcrumbs');
+
             return $beforeHeader.'<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.
                    $themeSwitch .
-                   $this->inside($block, $tag, $content).
+				   $this->inside($block, $tag, $content) .
                    $footerText.'</'.$tag.'>'.$afterHeader.$breadcrumbs;
         }
+
         return $content;
     }
     //core_term_description
@@ -870,8 +875,6 @@
     protected function localize_feedblock():void
     {
         wp_localize_script('jvb-feed-view-script', 'feedSettings', [
-            'apiUrl' => rest_url('jvb/v1/'),
-            'nonce' => wp_create_nonce('wp_rest'),
             'currentUser' => is_user_logged_in() ? [
                 'id' => get_current_user_id()
             ] : null,
@@ -921,7 +924,9 @@
             $tag = (str_contains($tag, ' class')) ? strtok($tag, ' class') : $tag;
             $tag = trim($tag);
         }
-
+		if (!str_starts_with($o, '<'.$tag)) {
+			return $o;
+		}
 
         $len = strlen('</'.$tag.'>');
 
@@ -988,30 +993,16 @@
         int $ID = 0,
         string $start = 'tiny',
         string $replace = 'large',
-        string $postSlug = ''
+        ?string $postSlug = null
     ):string {
-        $image = $this->image($ID, $start, $replace);
-        if ($ID === 0) {
-            $ID = $this->imageID($ID);
-        }
-        if ($postSlug==='') {
-            global $post;
-            $postSlug = $post->post_name;
-        }
-        if (!$ID) {
-            return '';
-        }
-		$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;
+        $image = jvbFormatImage($ID, $start, $replace, true, $postSlug);
+
         if ($close) {
-            $img .= '</a>';
+            return $image;
         }
-        return $img;
+		$len = strlen('</a>');
+        return substr_replace($image, '', -$len, $len);
     }
     public function image(string $ID = '', string $start = 'tiny', string $replace = 'large'):string
     {
@@ -1023,41 +1014,7 @@
         if ($ID === 0 || $ID === false) {
             return '';
         }
-
-        $img = wp_get_attachment_image_src($ID, $start);
-		if (!$img) return '';
-		$img = $img[0];
-
-        $data = $this->gallerySizes($ID, $replace);
-
-        $alt = get_post_meta($ID, '_wp_attachment_image_alt', true);
-        $alt = ($alt=='')? '' : ' alt="'.$alt.'" ';
-
-        return '<img width="100%" height="auto" src="' .
-               $img . '"' .$alt . $data . ' loading="lazy" decoding="async">';
-    }
-    public function gallerySizes(int $ID, string $replace = 'medium'):string
-    {
-        if (!wp_get_attachment_image_src($ID)) {
-            return '';
-        }
-        if ($replace == 'large') {
-            return 'data-small="'.
-                   wp_get_attachment_image_src($ID, 'large')[0].'" data-medium="'.
-                   wp_get_attachment_image_src($ID, 'full')[0].'" data-full="'.
-                   wp_get_attachment_image_src($ID, 'full')[0].'"';
-        } elseif ($replace == 'medium') {
-            return 'data-small="'.
-                   wp_get_attachment_image_src($ID, 'large')[0].'" data-medium="'.
-                   wp_get_attachment_image_src($ID, 'large')[0].'" data-full="'.
-                   wp_get_attachment_image_src($ID, 'large')[0].'"';
-        } elseif ($replace == 'thumbnail') {
-            return 'data-small="'.
-                   wp_get_attachment_image_src($ID, 'medium')[0].'" data-medium="'.
-                   wp_get_attachment_image_src($ID, 'medium')[0].'" data-full="'.
-                   wp_get_attachment_image_src($ID, 'medium')[0].'"';
-        }
-        return '';
+		return jvbFormatImage($ID, $start, $replace, false);
     }
     public function sanitizeBlockName(array $block):string
     {
@@ -1753,61 +1710,15 @@
         return $styles;
     }
 
-    public function getGallerySizes(int $ID, string $replace):string
-    {
-        if (!wp_get_attachment_image_src($ID)) {
-            return '';
-        }
-        if (!has_image_size($replace)) {
-            $replace = 'large';
-        }
-
-        if ($replace == 'large') {
-            return 'data-small="' .
-                   wp_get_attachment_image_src($ID)[0] . '" data-medium="' .
-                   wp_get_attachment_image_src($ID, 'large')[0] . '" data-full="' .
-                   wp_get_attachment_image_src($ID, 'full')[0] . '"';
-        } elseif ($replace == 'medium') {
-            return 'data-small="'.
-                   wp_get_attachment_image_src($ID, 'large')[0].'" data-medium="'.
-                   wp_get_attachment_image_src($ID, 'large')[0].'" data-full="'.
-                   wp_get_attachment_image_src($ID, 'large')[0].'"';
-        } elseif ($replace == 'thumbnail') {
-            return 'data-small="'.
-                   wp_get_attachment_image_src($ID, 'medium')[0].'" data-medium="'.
-                   wp_get_attachment_image_src($ID, 'medium')[0].'" data-full="'.
-                   wp_get_attachment_image_src($ID, 'medium')[0].'"';
-        }
-        return '';
-    }
-
     public function formatImage(int $ID = 0, string $start = 'tiny', string $replace = 'large'):string
     {
-        if ($ID === 0) {
-            $ID = $this->imageID($ID);
-        }
-        if ($ID === 0) {
-            return '';
-        }
-
-		return $this->imgCache->remember(
-			['ID' => $ID, 'start' => $start, 'replace' => $replace],
-			function() use ($ID, $start, $replace) {
-				$img = wp_get_attachment_image_src($ID, $start);
-				if (!$img) {
-					return'';
-				}
-				$img = $img[0];
-
-				$data = $this->getGallerySizes($ID, $replace);
-
-
-
-				$alt = get_post_meta($ID, '_wp_attachment_image_alt', true);
-				$alt = ($alt=='')? '' : ' alt="'.$alt.'" ';
-				return '<img width="300px" height="300px" src="'.$img.'"'.$alt.$data.' loading="lazy" decoding="async">';
-			}
-		);
+		if ($ID === 0) {
+			$ID = $this->imageID($ID);
+		}
+		if ($ID === 0) {
+			return '';
+		}
+		return jvbFormatImage($ID, $start, $replace);
     }
 
 }
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 83f8b1d..1f9703a 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -2,306 +2,349 @@
 namespace JVBase\blocks;
 
 use JVBase\managers\CacheManager;
+use JVBase\utility\Features;
+use JVBase\utility\Checker;
 use JVBase\forms\TaxonomySelector;
 use WP_Block;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit;
 }
 
 class FeedBlock
 {
-    protected CacheManager $cache;
-    protected array $config;
-    protected string $path = JVB_DIR.'/build/feed';
+	protected CacheManager $cache;
+	protected array $config;
+	protected string $path = JVB_DIR.'/build/feed';
 
-    public function __construct()
-    {
-        $this->cache = CacheManager::for('feed',WEEK_IN_SECONDS);
-        add_action('init', [$this, 'registerBlock']);
-    }
+	public function __construct()
+	{
+		// Initialize cache with connections
+		$this->cache = CacheManager::for('feed_block', WEEK_IN_SECONDS);
 
-    public function registerBlock()
-    {
-        register_block_type($this->path, [
-            'render_callback' => [$this, 'render']
-        ]);
-    }
+		// Set up cache connections for all feed content types
+		$this->setupCacheConnections();
 
-    protected function buildParams(array $attributes):array
-    {
+		add_action('init', [$this, 'registerBlock']);
+	}
 
-        if (!jvbCheck('inheritQuery', $attributes)) {
-            return [
-                'title'     => $attributes['title'],
-                'content'   => $attributes['contentTypes'],
-                'taxonomies'=> $this->getTaxonomies($attributes['contentTypes'])
-            ];
-        }
+	/**
+	 * Set up cache connections for feed content
+	 */
+	protected function setupCacheConnections(): void
+	{
+		// Connect to all content types that show in feed
+		$contentTypes = Features::getTypesWithFeature('show_feed', 'content');
+		foreach ($contentTypes as $type) {
+			CacheManager::for('feed_content')->connectTo('post', $type);
+		}
 
-        if (isJVBUserType()) {
-            return $this->buildProfileConfig();
-        } elseif (isJVBContentTax()) {
-            return $this->buildShopConfig();
-        } elseif (is_tax()) {
-            return $this->buildTaxonomyConfig();
-        }
+		// Connect to all taxonomies that show in feed
+		$taxonomies = Features::getTypesWithFeature('show_feed', 'taxonomy');
+		foreach ($taxonomies as $tax) {
+			CacheManager::for('feed_taxonomy')->connectTo('taxonomy', $tax);
+		}
+	}
 
-        return [];
-    }
+	public function registerBlock()
+	{
+		register_block_type($this->path, [
+			'render_callback' => [$this, 'render']
+		]);
+	}
 
-    protected function buildProfileConfig():array
-    {
-        $obj = get_queried_object();
-        $content = jvbGetUserContentTypes($obj->ID);
-        return [
-            'is_gallery'    => true,
-            'content'       => $content,
-            'context'       => jvbNoBase($obj->post_type),
-            'taxonomies'    => $this->getTaxonomies($content)
-        ];
-    }
+	protected function buildParams(array $attributes): array
+	{
+		if (!jvbCheck('inheritQuery', $attributes)) {
+			return [
+				'title' => $attributes['title'],
+				'content' => $attributes['contentTypes'],
+				'taxonomies' => $this->getTaxonomies($attributes['contentTypes'])
+			];
+		}
+		$config = [
+			'is_gallery'    => false,
+			'content'       => '',
+			'taxonomies'    => []
+		];
+		$type = get_queried_object();
 
-    protected function buildShopConfig():array
-    {
-        $type = jvbNoBase(get_queried_object()->taxonomy);
-        $content = jvbContentTaxContent($type);
+		if (is_post_type_archive() || is_singular()) {
+			$content = is_singular() ? jvbNoBase($type->post_type) : jvbNoBase($type->name);
+			$mainConfig = JVB_CONTENT[$content]??false;
+			if ($mainConfig && array_key_exists('feed', $mainConfig) && array_key_exists('config', $mainConfig['feed'])){
+				$config = array_merge($config, $mainConfig['feed']['config']);
+			} else {
+				$config['content'] = $content;
+				$config['icon'] = JVB_CONTENT[$content]['icon']??['logo-triangle'];
+			}
+			if (is_singular()) {
+				$config['source'] = $type->ID;
+			}
 
-        $context = [
-            'content'   => $content,
-            'taxonomies' => $this->getTaxonomies($content),
-            'context'   => $type,
-        ];
-        unset($context['taxonomies'][array_search($type, $context['taxonomies'])]);
-        return $context;
-    }
+			$config['taxonomies'] = $this->getTaxonomies([$content]);
+		} elseif (is_tax()) {
+			$content = jvbNoBase($type->taxonomy);
+			$mainConfig = JVB_TAXONOMY[$content]??false;
+			if ($mainConfig) {
+				$config['content'] = $mainConfig['for_content'];
+				$config['context'] = $content;  // ← ADD THIS
+				$config['taxonomies'] = $this->getTaxonomies($mainConfig['for_content']);
+				if (array_key_exists('feed', $mainConfig) && array_key_exists('config', $mainConfig['feed'])){
+					$config = array_merge($config, $mainConfig['feed']['config']);
+				}
+			}
+			$config['source'] = $type->term_id;
+		}
 
-    protected function buildTaxonomyConfig():array
-    {
-        $type = jvbNoBase(get_queried_object()->taxonomy);
-        $content = jvbContentTaxContent($type);
-        return [
-            'content'   => $content,
-            'taxonomies' => $this->getTaxonomies($content),
-            'context'   => $type
-        ];
-    }
+		if (!is_array($config['content'])) {
+			$config['content'] = [$config['content']];
+		}
 
-    protected function getTaxonomies(array $content):array
-    {
-        global $jvb_taxonomy_for;
-        $taxonomies = [];
-        foreach ($jvb_taxonomy_for as $taxonomy => $for) {
-            if (array_intersect($for, $content)) {
-                $taxonomies[] = $taxonomy;
-            }
-        }
-        return $taxonomies;
-    }
+		return $config;
+	}
 
-    public function render(array $attributes, string $content, WP_Block $block)
-    {
-        $this->config = $this->buildParams($attributes);
+	/**
+	 * Get taxonomies for given content types
+	 * Uses Checker instead of globals
+	 */
+	protected function getTaxonomies(array $content): array
+	{
+		$checker = Checker::getInstance();
+		$taxonomies = [];
 
+		foreach ($content as $contentType) {
+			$contentTaxonomies = $checker->getTaxonomiesForContent($contentType);
+			$contentTaxonomies = array_filter($contentTaxonomies, function($taxonomy) {
+				return array_key_exists('show_feed', JVB_TAXONOMY[$taxonomy]) && JVB_TAXONOMY[$taxonomy]['show_feed'];
+			});
+			$taxonomies = array_merge($taxonomies, $contentTaxonomies);
+		}
+
+		return array_unique($taxonomies);
+	}
+
+	public function render(array $attributes, string $content, WP_Block $block)
+	{
+		$this->config = $this->buildParams($attributes);
 		return $this->cache->remember(
 			$this->config,
 			function() {
 				return $this->renderBlock();
 			}
 		);
-    }
+	}
 
-    protected function renderBlock():string
-    {
-        $work = isJVBUserType() ? ' id="work"' : '';
+	protected function renderBlock(): string
+	{
+		$ids = (array_key_exists('ids', $this->config) && !empty($this->config['ids'])) ? ' id="'.implode(' ',$this->config['ids']).'"' : '';
+		$classes = (array_key_exists('classes', $this->config) && !empty($this->config['classes'])) ? ' class="'.implode(' ',$this->config['classes']).'"' : '';
+		$source = (array_key_exists('source', $this->config)) ? ' data-source="'.$this->config['source'].'"' : '';
+		$context = (array_key_exists('context', $this->config)) ? ' data-context="'.$this->config['context'].'"' : '';
+		$icons = (array_key_exists('icon', $this->config)) ? ' data-icon="'.$this->config['icon'].'"' : ' data-icon="logo-triangle"';
+		$gallery = (array_key_exists('is_gallery', $this->config) && $this->config['is_gallery']) ? ' data-gallery' : '';
+		$content = (array_key_exists('content', $this->config)) ? ' data-content="'.implode(',',$this->config['content']).'"' : '';
+		ob_start();
+		?>
+		<section<?= $ids.$classes ?> class="feed-block"<?= $content.$source.$context.$gallery.$icons ?>>
+			<?php
+			$this->renderFilters();
+			$this->renderGrid();
+			$this->renderLoader();
+			$this->renderTemplates();
+			echo TaxonomySelector::outputSelectorModal();
+			?>
+		</section>
+		<?php
+		return ob_get_clean();
+	}
 
-        ob_start();
-        ?>
-        <section<?= $work ?> class="feed-block"
-                             data-source="<?= get_queried_object_id(); ?>"
-            <?= (array_key_exists('context', $this->config)) ? ' data-context="'.$this->config['context'].'"' : '' ?>
-            <?= (array_key_exists('is_gallery', $this->config)) ? ' data-gallery="true"' : ''?>>
-            <?php
-            $this->renderFilters();
-            $this->renderGrid();
-            $this->renderLoader();
-            $this->renderTemplates();
-            ?>
-        </section>
-        <?php
-        return ob_get_clean();
-    }
+	protected function renderFilters(): void
+	{
+		if (empty($this->config)) {
+			return;
+		}
 
-    protected function renderFilters():void
-    {
-        if (empty($this->config)) {
-            return;
-        }
-        $many = count($this->config['content']) > 1;
-        global $jvb_everything;
-        global $jvb_taxonomy_for;
-        ?>
-        <form class="feed-filters" data-save="feed">
-            <details>
-            <summary class="row btw">
-                <span class="label">SHOWING: </span>
-                <?php
-                $labels = [];
-                foreach ($this->config['content'] as $i => $type) :
-                    $checked = $i === 0 ? ' checked' : '';
+		$feedContent = $this->getFeedContent();
+		$hasMany = count($this->config['content']) > 1;
+		?>
+		<form class="feed-filters" data-save="feed-<?=$this->config['context']?>">
+			<?php if ($hasMany) {
+				//If we have multiple content, only show the content first
+				?>
+				<details class="col a-start">
+				<summary class="row btw">
+					<span class="label">SHOWING: </span>
+					<?php
+					$labels = [];
+					foreach ($this->config['content'] as $i => $type) :
 
-                    $label = $jvb_everything[$type]['plural'];
-                    ?>
-
-                    <input type="radio"
-                           id="filter-<?= esc_attr($type) ?>"
-                           class="btn"
-                           name="content"
-                           value="<?= esc_attr($type) ?>"
-                        <?= $checked ?>>
-                    <label for="filter-<?= esc_attr($type) ?>" title="Show <?= $label ?>" class="row">
-                        <?= jvbIcon($type, ['title'=> $label]) ?>
-                        <span class="screen-reader-text"><?= $label ?></span>
-                    </label>
-
-                <?php
-                $labels['filter-'.$type] = $label;
-                endforeach;
-                ?>
-                <ul class="filter-label">
-                <?php
-                $i = 0;
-                foreach ($labels as $id =>$label) {
-                    $active = $i === 0 ? ' class="active"' : '';
-                    ?>
-                    <li id="<?=$id?>"<?=$active?>>
-                        <?=$label?>
-                    </li>
-                    <?php
-                    $i++;
-                }
-                ?>
-                </ul>
+						$checked = $i === 0 ? ' checked' : '';
+						$label = $feedContent[$type]['plural'] ?? ucfirst($type);
+						?>
+						<input type="radio"
+							   id="filter-<?= esc_attr($type) ?>"
+							   class="btn"
+							   name="content"
+							   data-filter="content"
+							   value="<?= esc_attr($type) ?>"
+							<?= $checked ?>>
+						<label for="filter-<?= esc_attr($type) ?>" title="Show <?= $label ?>" class="row">
+							<?= jvbIcon($feedContent[$type]['icon']) ?>
+							<span class="screen-reader-text"><?= $label ?></span>
+						</label>
+						<?php
+						$labels['filter-'.$type] = $label;
+					endforeach;
+					?>
+					<ul class="filter-label">
+						<?php
+						$i = 0;
+						foreach ($labels as $id => $label) {
+							$active = $i === 0 ? ' class="active"' : '';
+							?>
+							<li id="<?= $id ?>"<?= $active ?>>
+								<?= $label ?>
+							</li>
+							<?php
+							$i++;
+						}
+						?>
+					</ul>
+			<?php } ?>
 
 
-                <?php if (is_user_logged_in()) : ?>
-                    <input type="checkbox" id="favourites" class="btn" name="favourites" value="on">
-                    <label for="favourites" title="Show Favourites" class="row">
-                        <?= jvbIcon('heart', ['title'    =>'Favourites']) ?>
-                        <span class="screen-reader-text">Show Favourites Only</span>
-                    </label>
-                <?php endif; ?>
+					<?php if (Features::forSite()->has('favourites') && is_user_logged_in()) : ?>
+						<input type="checkbox" id="favourites" class="btn" name="favourites" value="on"
+							   data-filter="favourites">
+						<label for="favourites" title="Show Favourites" class="row">
+							<?= jvbIcon('heart').jvbIcon('heart', ['style' => 'fill']) ?>
+							<span class="screen-reader-text">Show Favourites Only</span>
+						</label>
+					<?php endif; ?>
+				<?php if ($hasMany) { ?>
+				</summary>
+				<?php } ?>
 
-                <?php if ($many) {
-                    echo '</summary>';
-                } ?>
+					<div class="filters">
+						<div class="filter-group row start">
+							<span class="label">FILTER BY:</span>
 
-                <div class="filters">
-                    <div class="filter-group">
-                        <span class="label">FILTER BY:</span>
+							<?php
+							$checker = Checker::getInstance();
+							foreach ($this->config['taxonomies'] as $tax) :
+								$taxConfig = JVB_TAXONOMY[$tax] ?? null;
+								if (!$taxConfig) continue;
 
-                        <?php
-                        foreach ($jvb_taxonomy_for as $tax => $items) :
-                            $hidden = !in_array($tax, $this->config['taxonomies']) ? ' hidden' : '';
-                            if (in_array($tax, $this->config['taxonomies'])) {
-                                $tax = new TaxonomySelector(
-                                    'feed-'.$tax,
-                                    $tax,
-                                    [
-										'update' => '.selected-items-section .selected-items',
-                                        'types' => $items,
-										'hidden'=> $hidden
-                                    ]
-                                );
-                                echo  $tax->render();
-                            }
+								$contentForTax = $checker->getContentForTaxonomy($tax);
+								$hidden = empty($contentForTax) ? ' hidden' : '';
 
-                        endforeach; ?>
-                    </div>
-                    <div class="selected-items-section">
-                        <div class="selected-items row"></div>
-                        <div class="filter-actions row">
-                            <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY') ?>
-                            <button type="button" class="clear-filters row">
-                                <?= jvbIcon('x', ['title'    => 'Clear']) ?>
-                                Clear All Filters
-                            </button>
-                        </div>
-                    </div>
-                </div>
+								$taxSelector = new TaxonomySelector(
+									'feed-'.$tax,
+									$tax,
+									[
+										'icon'			=> $taxConfig['icon']??'logo-triangle',
+										'update' 		=> '.selected-items-section .selected-items',
+										'types' 		=> $contentForTax,
+										'autocomplete'	=> false,
+										'hidden' 		=> $hidden,
+										'output'		=> 'minimal'
+									]
+								);
+								echo $taxSelector->render();
+							endforeach;
+							?>
+						</div>
+						<div class="selected-items-section">
+							<div class="selected-items row"></div>
+							<div class="filter-actions row">
+								<?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', false, ['filter' => 'match']) ?>
+								<button type="button" class="clear-filters row">
+									<?= jvbIcon('x') ?>
+									Clear All Filters
+								</button>
+							</div>
+						</div>
+					</div>
 
-                <div class="filter-group">
-                    <div class="order-by">
-                        <span class="label">ORDER BY:</span>
-                        <input type="radio" id="order-title" class="btn" name="orderby" value="title" data-for="artist,shop" hidden>
-                        <label for="order-title" title="Order by Name" class="row">
-                            <?= jvbIcon('alphabetical') ?>
-                            <span class="label">Name</span>
-                        </label>
+					<div class="row btw nowrap">
+						<div class="order-by filter-group row start w-full">
+							<span class="label">ORDER BY:</span>
+							<?php
+							//TODO: Get content types that can be sorted alphabetically
+							?>
+							<input type="radio" id="order-title" class="btn" name="orderby" value="title" data-for="artist,shop" data-filter="orderby" hidden>
+							<label for="order-title" title="Order by Name" class="row">
+								<?= jvbIcon('alphabetical') ?>
+								<span class="label">Name</span>
+							</label>
 
-                        <input type="radio" id="order-date" class="btn" name="orderby" value="date" checked>
-                        <label for="order-date" title="Order by Date" class="row">
-                            <?= jvbIcon('calendar', ['title'=>'Date']) ?>
-                            <span class="label">Date</span>
-                        </label>
+							<input type="radio" id="order-date" class="btn" name="orderby" value="date" data-filter="orderby" checked>
+							<label for="order-date" title="Order by Date" class="row">
+								<?= jvbIcon('calendar', ['title' => 'Date']) ?>
+								<span class="label">Date</span>
+							</label>
 
-                        <input type="radio" id="order-random" class="btn" name="orderby" value="random">
-                        <label for="order-random" title="Random Order" class="row">
-                            <?= jvbIcon('shuffle') ?>
-                            <span class="label">Random</span>
-                        </label>
-                    </div>
+							<input type="radio" id="order-random" class="btn" name="orderby" value="random" data-filter="orderby">
+							<label for="order-random" title="Random Order" class="row">
+								<?= jvbIcon('shuffle') ?>
+								<span class="label">Random</span>
+							</label>
+						</div>
 
-                    <div class="order-direction radio-group-label" data-for-order="date,title">
-                        <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('sort-descending') ?>
-                        </label>
+						<div class="order-direction filter-group row start w-full" data-for-order="date,title">
+							<span class="label">ORDER:</span>
+							<input type="radio" id="order-desc" class="btn" name="order" value="desc" data-filter="order" checked>
+							<label for="order-desc" title="Sort Descending (A-Z, 1-10)" class="row">
+								<?= jvbIcon('sort-descending') ?>
+								<span class="label" >DESC (A-Z)</span>
+							</label>
 
-                        <input type="radio" id="order-asc" class="btn" name="order" value="asc">
-                        <label for="order-asc" title="Oldest First" class="row">
-                            <?= jvbIcon('sort-ascending') ?>
-                        </label>
-                    </div>
-                </div>
-            </details>
-        </form>
-        <?php
-    }
+							<input type="radio" id="order-asc" class="btn" name="order" value="asc" data-filter="order">
+							<label for="order-asc" title="Sort Ascending (Z-A, 10-1)" class="row">
+								<?= jvbIcon('sort-ascending') ?>
+								<span class="label" >ASC (Z-A)</span>
+							</label>
+						</div>
+					</div>
+			<?php if ($hasMany) { ?>
+				</details>
+			<?php } ?>
+		</form>
+		<?php
+	}
 
-    protected function renderGrid():void
-    {
-        ?>
-        <div class="item-grid"></div>
-        <?php
-    }
+	protected function renderGrid(): void
+	{
+		?>
+		<div class="item-grid"></div>
+		<?php
+	}
 
-    protected function renderLoader():void
-    {
-        ?>
-        <button type="button" class="load-more">
-            <?= jvbIcon('arrow-elbow-left-down', ['title'    =>'More']) ?>
-            Show Me More
-            <?= jvbIcon('arrow-elbow-right-down', ['title'=> 'More']) ?>
-        </button>
+	protected function renderLoader(): void
+	{
+		?>
+		<button type="button" class="load-more">
+			<?= jvbIcon('arrow-elbow-left-down') ?>
+			Show Me More
+			<?= jvbIcon('arrow-elbow-right-down') ?>
+		</button>
 
-        <?= jvbLoadingScreen() ?>
-        <?php
-        if (array_key_exists('is_gallery', $this->config)) {
-            jvbRenderGallery();
-        }
-    }
+		<?= jvbLoadingScreen() ?>
+		<?php
+		if (array_key_exists('is_gallery', $this->config)) {
+			jvbRenderGallery();
+		}
+	}
 
-    protected function renderTemplates():void
-    {
-        echo '<template class="feed-item">
-        <details class="item feed" data-umami-event="view_feed">
+	protected function renderTemplates(): void
+	{
+		echo '<template class="feed-item">'.apply_filters('jvbFeedItem', '<details class="item feed" data-umami-event="view_feed">
             <summary class="row btw">
                 <span class="handle">DETAILS</span>
                 <button class="favourite" title="Add to favourites" onclick="toggleFavourite(this)">
                     '.jvbIcon('heart')
-               .jvbIcon('heart', ['style'=>'fill']).'
+				.jvbIcon('heart', ['style'=>'fill']).'
                 </button>
                 <div class="feed-images">
                     <a>
@@ -327,17 +370,23 @@
                     </div>
                 </div>
             </div>
-        </details>
-    </template>';
+        </details>', $this->config).'</template>';
 
-        echo '<template class="emptyState">
-            <div class="feed-empty-state">
+		echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="feed-empty-state">
                 <h3>NOTHING HERE...</h3>
                 <p>Try tweaking those filters a bit.</p>
                 <p>Edmonton\'s got talent - let\'s find it.</p>
-            </div>
-        </template>';
+            </div>', $this->config). '</template>';
 
-        echo '<template class="placeholderTemplate"><div class="placeholder"></div></template>';
-    }
+		echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', '').'</div></template>';
+	}
+
+	/**
+	 * Get feed content using Features instead of get_option
+	 * Returns array of slug => config for types that show in feed
+	 */
+	public function getFeedContent(): array
+	{
+		return JVB()->routes('feed')->getFeedTypesConfig();
+	}
 }
diff --git a/inc/blocks/GlossaryBlock.php b/inc/blocks/GlossaryBlock.php
index c0442bb..5f8c394 100644
--- a/inc/blocks/GlossaryBlock.php
+++ b/inc/blocks/GlossaryBlock.php
@@ -24,7 +24,7 @@
 
 	public function __construct()
 	{
-		$this->cache = CacheManager::for('glossary_terms', WEEK_IN_SECONDS);
+		$this->cache = CacheManager::for('glossary_terms', WEEK_IN_SECONDS)->connectTo('post', 'terms');
 		add_action('init', [ $this, 'registerBlock' ]);
 	}
 
diff --git a/inc/blocks/MenuBlock.php b/inc/blocks/MenuBlock.php
index e6ba1e2..cabb11f 100644
--- a/inc/blocks/MenuBlock.php
+++ b/inc/blocks/MenuBlock.php
@@ -27,7 +27,7 @@
 
     public function __construct()
     {
-        $this->cache = CacheManager::for('menu', WEEK_IN_SECONDS);
+        $this->cache = CacheManager::for('menu', WEEK_IN_SECONDS)->connectTo('post', 'menu_item');
         add_action('init', [ $this, 'registerBlock' ]);
     }
 
diff --git a/inc/blocks/RegisterBlocks.php b/inc/blocks/RegisterBlocks.php
index b4cb788..5408503 100644
--- a/inc/blocks/RegisterBlocks.php
+++ b/inc/blocks/RegisterBlocks.php
@@ -6,7 +6,7 @@
 	exit;
 }
 //Blocks
-if (jvbSiteUsesFeedBlock()) {
+if (Features::forSite()->has('feed_block')) {
     require(JVB_DIR . '/build/feed/render.php');
 }
 require(JVB_DIR . '/build/list/render.php');
@@ -61,49 +61,6 @@
 }
 add_filter('block_categories_all', 'jvbRegisterBlockCategory');
 
-//Get our loaded types
-function jvbRegisterFeedTypes():void
-{
-    global $jvb_feed;
-    global $jvb_taxonomy_for;
-
-
-    wp_localize_script('jvb-feed-block-editor', 'jvbBlockSettings', [
-        'contentTypes'    => $jvb_feed,
-        'taxonomyFor'   =>  $jvb_taxonomy_for
-    ]);
-
-
-    // Filter for block.json
-    add_filter('block_type_metadata', function ($metadata) use ($jvb_feed) {
-        if ($metadata['name'] === 'jvb/feed') {
-            $metadata['attributes']['contentTypes']['items']['enum'] = array_keys($jvb_feed);
-        }
-        return $metadata;
-    });
-}
-add_action('wp_loaded', 'jvbRegisterFeedTypes');
-
-if (jvbSiteUsesFeedBlock()) {
-    add_action('rest_api_init', 'jvbGetFeedTypes');
-    function jvbGetFeedTypes():void
-    {
-        register_rest_route(
-            'jvb/v1',
-            'types',
-            [
-                'permission_callback'    => function () {
-                    return is_user_logged_in();
-                },
-                'callback'    => function () {
-                    global $jvb_feed;
-                    return new WP_REST_Response(json_encode((object) $jvb_feed));
-                }
-            ]
-        );
-    }
-}
-
 
 /**
  * Enqueue block editor assets
diff --git a/inc/blocks/SummaryBlock.php b/inc/blocks/SummaryBlock.php
index fd55561..7e92262 100644
--- a/inc/blocks/SummaryBlock.php
+++ b/inc/blocks/SummaryBlock.php
@@ -66,7 +66,7 @@
         );
         /**
          * The h1 title element, excluding the h1 tag.
-         * Can include additonal html (like <small> or <b>)
+         * Can include additional html (like <small> or <b>)
          */
         $this->header = apply_filters(
             'jvbSummaryHeader',
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
new file mode 100644
index 0000000..4d8e9d1
--- /dev/null
+++ b/inc/blocks/TimelineBlock.php
@@ -0,0 +1,181 @@
+<?php
+namespace JVBase\blocks;
+
+use JVBase\managers\CacheManager;
+use JVBase\forms\TaxonomySelector;
+use JVBase\meta\MetaManager;
+use JVBase\utility\Features;
+use WP_Block;
+use WP_Query;
+
+if (!defined('ABSPATH')) {
+    exit; // Exit if accessed directly
+}
+
+class TimelineBlock
+{
+    protected CacheManager $cache;
+    protected string $config;
+    protected string $type;
+    protected string $path = JVB_DIR . '/build/timeline';
+    protected string $image;
+    protected string $header;
+	protected int $parentID;
+	protected int $total;
+	protected array $children = [];
+	protected string $content;
+    protected string $headerExtra;
+    protected string $detailsTitle;
+    protected array $details;
+	protected array|false $sections = false;
+
+    public function __construct()
+    {
+        $this->cache = CacheManager::for('timelines', WEEK_IN_SECONDS)->connectTo('post', 'timeline');
+        add_action('init', [ $this, 'registerBlock' ]);
+		add_action('wp_footer', 'jvbRenderGallery');
+    }
+
+    public function registerBlock()
+    {
+        register_block_type($this->path, [
+            'render_callback' => [ $this, 'render' ]
+        ]);
+    }
+
+    public function render(array $attributes, string $content, WP_Block $block)
+    {
+		global $post;
+		if (!$post || !Features::forContent(jvbNoBase($post->post_type))->has('is_timeline') ) {
+			return '';
+		}
+		$this->parentID = $post->ID;
+        $cache = $this->cache->get($this->parentID);
+        $cache = false;
+        if ($cache) {
+            return $cache;
+        }
+		$this->content = jvbNoBase($post->post_type);
+
+		$this->children = get_children([
+			'post_parent' 	=> $this->parentID,
+			'post_status'	=> 'publish',
+			'orderby' 		=> 'date',
+			'order' 		=> 'ASC',
+			'fields' 		=> 'ids'
+		]);
+		$this->total = count($this->children);
+
+        ob_start();
+        $this->renderBlock();
+        $content = ob_get_clean();
+        $this->cache->set($this->parentID, $content);
+        return $content;
+    }
+
+    protected function renderBlock():void
+    {
+        $this->renderHeader();
+        $this->renderTimeline();
+    }
+
+    protected function renderHeader():void
+    {
+		$title = get_the_title();
+        ?>
+        <header id="top">
+            <h1><small>Before and After Laser Tattoo Removal:</small><?=$title?></h1>
+			<ul class="timeline terms">
+				<?php
+				$tax = ['goal', 'number', 'body-part', 'style', 'skin-type', 'age'];
+
+				foreach ($tax as $slug) {
+					$config = JVB_TAXONOMY[$slug];
+					$taxSlug = jvbCheckBase($slug);
+					$terms = get_the_terms($this->parentID, $taxSlug);
+					if ($terms && !is_wp_error($terms)) {
+						$many = count($terms) > 1;
+					?>
+					<li class="<?=$slug?>">
+						<?=jvbIcon($config['icon']??'triangle')?>
+
+						<?php
+						if ($many) { echo '<ul>'; }
+						$open = ($many) ? '<li>' : '';
+						$close = ($many) ? '</li>' : '';
+						foreach ($terms as $term) {
+							$link = get_term_link($term->term_id, $taxSlug);
+							?>
+
+							<?= $open ?>
+							<a href="<?=$link?>" rel="tag"><?=$term->name?></a>
+							<?= $close ?>
+						<?php }
+						if ($many) { echo '</ul>'; }
+						?>
+					</li>
+					<?php
+					}
+				}
+				?>
+			</ul>
+        </header>
+		<section id="at-a-glance" class="row nowrap">
+			<div class="before">
+				<h3>Before</h3>
+				<?= jvbFormatImage(get_post_thumbnail_id($this->parentID), 'tiny', 'large', false) ?>
+			</div>
+			<div class="after">
+				<h3>After <?=$this->total?> Treatment<?= $this->total > 1 ? 's' : '' ?></h3>
+				<?= jvbFormatImage(get_post_thumbnail_id($this->children[count($this->children)-1]), 'tiny', 'large', false) ?>
+			</div>
+		</section>
+        <?php
+    }
+	protected function renderTimeline():void
+	{
+		$all = $this->children;
+		array_unshift($all, $this->parentID);
+		$uniqueFields = JVB()->routes('content')->getTimelineUniqueFields($this->content);
+		foreach ($all as $i => $ID) {
+			$meta = new MetaManager($ID, 'post');
+			$fields = $meta->getAll($uniqueFields);
+
+			$plural = ($i>1) ? 's': '';
+			$title = ($i === 0) ? 'Before Laser Tattoo Removal' : 'After '.$i.' Treatment'.$plural;
+			$title = ($i > 0 && !str_contains('Treatment', $fields['post_title'])) ? $fields['post_title'] : $title;
+			?>
+			<section id="<?= $i === 0 ? 'before-treatment' : 'treatment-'.$i ?>" class="timeline-point row a-start nowrap">
+				<?php
+				if (array_key_exists('post_thumbnail', $fields) && is_int($fields['post_thumbnail'])) {
+					echo jvbFormatImage($fields['post_thumbnail']);
+				}
+				?>
+				<div class="info">
+					<header>
+						<h2><?=jvbIcon('logo-triangle')?><?= $title?></h2>
+						<?= array_key_exists('post_date', $fields) && $fields['date'] !== '' ? '<time>'.date('F Y', strtotime($fields['post_date'])).'</time>' : '' ?>
+						<?= array_key_exists('timeline', $fields) && $fields['timeline'] !== '' ? $this->outputTimelineTax($ID) : '' ?>
+						<?= array_key_exists('post_content', $fields) && $fields['post_content'] !== '' ? '<div class="content">'.wptexturize(wp_kses_post( wpautop($fields['post_content']))).'</div>' : '' ?>
+					</header>
+				</div>
+			</section>
+			<?php
+		}
+	}
+
+	protected function outputTimelineTax(int $ID):string
+	{
+		$timeline = get_the_terms($ID, BASE.'timeline');
+		if (!$timeline || is_wp_error($timeline)) {
+			return '';
+		}
+		$out = '<ul class="term-list">';
+		foreach ($timeline as $term) {
+			$link = get_term_link($term->term_id, BASE.'timeline');
+			$out .= '<li><a href="'.$link.'" rel="tag" title="See more progressions at this timeline">'.jvbIcon(JVB_TAXONOMY['timeline']['icon']??'hourglass').$term->name.' Later</a></li>';
+		}
+		$out .='</ul>';
+		return $out;
+	}
+}
diff --git a/inc/blocks/_setup.php b/inc/blocks/_setup.php
index 78a4122..07d0e9c 100644
--- a/inc/blocks/_setup.php
+++ b/inc/blocks/_setup.php
@@ -24,6 +24,10 @@
 	require(JVB_DIR . '/inc/blocks/GlossaryBlock.php');
 	new JVBase\blocks\GlossaryBlock();
 }
+if (Features::anyContentHas('is_timeline')) {
+	require(JVB_DIR . '/inc/blocks/TimelineBlock.php');
+	new JVBase\blocks\TimelineBlock();
+}
 require(JVB_DIR . '/inc/blocks/SummaryBlock.php');
 new JVBase\blocks\SummaryBlock();
 
diff --git a/inc/forms/TaxonomySelector.php b/inc/forms/TaxonomySelector.php
index 29f7df3..e79834d 100644
--- a/inc/forms/TaxonomySelector.php
+++ b/inc/forms/TaxonomySelector.php
@@ -211,6 +211,10 @@
 	 * @return string The rendered HTML
 	 */
 	public function render(array $selected = [], string $extra = ''): string {
+
+		if (array_key_exists('output', $this->config) && $this->config['output'] === 'minimal') {
+			return $this->renderTaxonomyToggle($selected, $extra);
+		}
 		// Build data attributes
 		$dataAttrs = $this->buildDataAttributes($selected);
 
@@ -237,12 +241,13 @@
 					<?= $hasAutocomplete ?>
 					title="Open <?= $this->singular ?> Selector"
 					aria-label="Select <?= esc_attr($this->plural) ?>">
-					<?= jvbIcon('plus-square', ['title' => 'Add ' . $this->title]) ?>
+					<?= jvbIcon('plus-square') ?>
 				</button>
-				<input type="text" id="<?= $this->base ?><?= esc_attr($this->config['name']) ?>-autocomplete" autocomplete="off" data-ignore data-autocomplete>
-				<ul class="autocomplete-dropdown" hidden>
-
-				</ul>
+				<?php if ($hasAutocomplete !== '') { ?>
+					<input type="text" id="<?= $this->base ?><?= esc_attr($this->config['name']) ?>-autocomplete" autocomplete="off" data-ignore data-autocomplete>
+					<ul class="autocomplete-dropdown" hidden>
+					</ul>
+				<?php } ?>
 			</div>
 
 			<div class="selected-items row" role="region" aria-label="Selected <?= esc_attr($this->plural) ?>">
@@ -259,6 +264,11 @@
 		return ob_get_clean();
 	}
 
+	protected function renderTaxonomyToggle(array $selected = [], string $extra = ''): string
+	{
+		return '<button type="button" data-filter="taxonomy" data-taxonomy="'.$this->name.'" title="Filter by '.$this->singular.'">'.jvbIcon($this->config['icon']).'<span class="label">'.$this->singular.'</span></button>';
+	}
+
 	/**
 	 * Build data attributes string for the toggle button
 	 */
diff --git a/inc/helpers/breadcrumbs.php b/inc/helpers/breadcrumbs.php
index 4a42118..81cb9b8 100644
--- a/inc/helpers/breadcrumbs.php
+++ b/inc/helpers/breadcrumbs.php
@@ -1,6 +1,7 @@
 <?php
 
 use JVBase\managers\CacheManager;
+use JVBase\utility\Features;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -12,7 +13,7 @@
  */
 function jvbGetCrumbs():array
 {
-    $cache = CacheManager::for('breadcrumbs', MONTH_IN_SECONDS);
+    $cache = CacheManager::for('breadcrumbs', MONTH_IN_SECONDS)->connectTo('all');
     $key = get_queried_object_id();
     $crumbs = $cache->get($key);
 	$crumbs = false;
@@ -31,19 +32,27 @@
 
     //taxonomies extra
     if (is_tax()) {
-        switch ($obj->taxonomy) {
-            case BASE.'type':
-                $type = 'artist';
-                break;
-            default:
-                $type = jvbNoBase($obj->taxonomy);
-                break;
-        }
-        $directory = jvbDirectories($type);
-        $crumbs[] = [
-            'name'  => $directory['title'],
-            'url'   => $directory['url']
-        ];
+		$tax = jvbNoBase($obj->taxonomy);
+		$config = Features::getConfig($tax, 'term');
+		if (count($config['for_content']) === 1) {
+			$contentConfig = JVB_CONTENT[$config['for_content'][0]];
+			$crumbs[] = [
+				'name'	=> $contentConfig['breadcrumb']??$contentConfig['plural'],
+				'url'	=> get_post_type_archive_link(jvbCheckBase($config['for_content'][0])),
+			];
+			$crumbs[] = [
+				'name'	=> 'By '.$config['singular'],
+				'url'	=> false,
+			];
+		}
+		if (Features::forTaxonomy($tax)->has('directory')){
+			$directory = jvbDirectories($tax);
+			$crumbs[] = [
+				'name'  => $directory['title'],
+				'url'   => $directory['url']
+			];
+		}
+
         $crumbs = array_merge($crumbs, jvbGetBreadcrumbTermHierarchy($obj));
 
     }
diff --git a/inc/helpers/formatting.php b/inc/helpers/formatting.php
index de3d379..ec0c40f 100644
--- a/inc/helpers/formatting.php
+++ b/inc/helpers/formatting.php
@@ -73,9 +73,9 @@
  */
 function jvbFormatRating(int $ID, JVBase\meta\MetaManager|null $meta = null):string
 {
-    $cache = CacheManager::for('bio-'.$ID, WEEK_IN_SECONDS);
-    $key = 'rating';
-    $cached = $cache->get($key);
+    $cache = CacheManager::for('rating', WEEK_IN_SECONDS)->connectTo('post')->connectTo('term');
+
+    $cached = $cache->get($ID);
     $cached = false;
     if ($cached) {
         return $cached;
@@ -100,7 +100,7 @@
         $out .= jvbFormatStarRating($avg, (int)$total);
     }
 
-    $cache->set($key, $out);
+    $cache->set($ID, $out);
     return $out;
 }
 
@@ -137,7 +137,7 @@
  */
 function jvbImageData(int $imgID):array
 {
-    $cache = CacheManager::for('imageData', WEEK_IN_SECONDS);
+    $cache = CacheManager::for('imageData', WEEK_IN_SECONDS)->connectTo('post');
     $cached = $cache->get($imgID);
     if ($cached) {
         return $cached;
diff --git a/inc/helpers/media.php b/inc/helpers/media.php
index 11ec253..6a6f6fe 100644
--- a/inc/helpers/media.php
+++ b/inc/helpers/media.php
@@ -8,18 +8,22 @@
 {
     ?>
     <dialog class="gallery" aria-modal="true" aria-label="Image Gallery">
+
 		<div class="wrap">
-			<button type="button" class="cancel" aria-label="Close Gallery"> <?= jvbIcon('x') ?></button>
-			<button class="nav prev row" aria-label="Previous image">
-				<?= jvbIcon('caret-left') ?>
-			</button>
+			<div class="controls row">
+				<button type="button" class="cancel" aria-label="Close Gallery"> <?= jvbIcon('x') ?></button>
+				<button class="nav prev row" aria-label="Previous image">
+					<?= jvbIcon('caret-left') ?>
+				</button>
 
-			<button class="nav next row" aria-label="Next image">
-				<?= jvbIcon('caret-right') ?>
-			</button>
-
+				<button class="nav next row" aria-label="Next image">
+					<?= jvbIcon('caret-right') ?>
+				</button>
+			</div>
 			<div class="content row">
+				<img src="" alt="" class="image-left">
 				<img src="" alt="" class="image">
+				<img src="" alt="" class="image-right">
 				<details>
 					<summary class="row btw">DETAILS</summary>
 					<div class="item-info"></div>
diff --git a/inc/helpers/members.php b/inc/helpers/members.php
index e4d8386..291ecb5 100644
--- a/inc/helpers/members.php
+++ b/inc/helpers/members.php
@@ -35,7 +35,7 @@
  */
 function jvbGetUserByFirstName(string $first_name):WP_User|false
 {
-    $cache = CacheManager::for('userFirstname');
+    $cache = CacheManager::for('userFirstname')->connectTo('user');
     $cached = $cache->get($first_name)??false;
     if ($cached) {
         return get_userdata($cached);
@@ -76,7 +76,7 @@
  */
 function jvbGetUserByDisplayName(string $display_name):WP_User|false
 {
-    $cache = CacheManager::for('user_displaynames');
+    $cache = CacheManager::for('user_displaynames')->connectTo('user');
     $cached = $cache->get($display_name)??false;
 
     if ($cached && is_int($cached)) {
@@ -115,7 +115,7 @@
 function jvbGetUsername(int $user_id):string
 {
     $key = 'user_display_names';
-    $cache = CacheManager::for('userNames', WEEK_IN_SECONDS);
+    $cache = CacheManager::for('userNames', WEEK_IN_SECONDS)->connectTo('user');
     $cached = $cache->get($user_id);
 
     if ($cached) {
@@ -156,7 +156,7 @@
         return false;
     }
 
-    $cache = CacheManager::for('artist', 3600);
+    $cache = CacheManager::for('artist', 3600)->connectTo('post');
 	$cached = $cache->get($userID);
 	if ($cached) {
 		return match ($return) {
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 8786204..4817908 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -25,12 +25,16 @@
     string $labelAfter,
     string $on,
     string $off,
-    bool $hidden = false
+    bool $hidden = false,
+	array $data = []
 ):string {
     $hidden = ($hidden) ? ' hidden' : '';
+	$dataset = (!empty($data)) ? implode('', array_map(function ($key, $value) {
+		return ' data-'.$key.'="'.$value.'"';
+	}, array_keys($data), array_values($data))) : '';
     return sprintf(
-        '<div class="toggle-text" %s>
-            <input value="all" name="%s" id="%s" type="checkbox">
+        '<div class="toggle-text"%s>
+            <input value="all" name="%s" id="%s" type="checkbox"%s>
             <label for="%s" class="row">
                        %s
                 <span class="text row">
@@ -43,6 +47,7 @@
         $hidden,
         $name,
         $name,
+		$dataset,
         $name,
         $labelBefore,
         $off,
@@ -59,9 +64,8 @@
  */
 function jvbRenderLinks(int $ID, MetaManager|null $meta = null):string
 {
-    $cache = CacheManager::for('bio-'.$ID, WEEK_IN_SECONDS);
-    $key = 'links';
-    $cached = $cache->get($key);
+    $cache = CacheManager::for('user_links', WEEK_IN_SECONDS)->connectTo('post')->connectTo('taxonomy');
+    $cached = $cache->get($ID);
     if ($cached) {
         return $cached;
     }
@@ -124,7 +128,7 @@
         }
         $out .= '</ul>';
     }
-    $cache->set($key, $out);
+    $cache->set($ID, $out);
     return $out;
 }
 
@@ -136,9 +140,9 @@
  */
 function jvbRenderContactInfo(int $ID, MetaManager|null $meta = null):string
 {
-    $cache = CacheManager::for('bio-'.$ID, WEEK_IN_SECONDS);
-    $key = 'contact';
-    $cached = $cache->get($key);
+    $cache = CacheManager::for('contact', WEEK_IN_SECONDS)->connectTo('post')->connectTo('taxonomy');
+
+    $cached = $cache->get($ID);
     if($cached){
         return $cached;
     }
@@ -178,7 +182,7 @@
         $out .= '</ul>';
     }
 
-    $cache->set($key, $out);
+    $cache->set($ID, $out);
 
     return $out;
 }
@@ -365,7 +369,7 @@
 						<div class="field">
 							<input type="checkbox" id="select-all-group" name="select-all-group">
 							<label for="select-all-group">
-								Select All
+								Select All In Group
 							</label>
 						</div>
 						<div class="info" hidden>
@@ -532,38 +536,39 @@
 	$dataID = ($ID) ? ['image-id' => $ID] : false;
 	$addID = ($ID) ? '-'.$ID : '';
 
-	$fields = array_merge([
-		'image_data'	=> [
-			'type'	=> 'group',
-			'wrap'	=> 'details',
-			'label'	=> 'Image Info',
-			'hint'	=> 'These will be automatically generated if left blank.',
-			'fields'	=> [
-				'image-title'.$addID => [
-					'type'	=> 'text',
-					'label'	=> 'Image Title',
-					'value'	=> $title,
-					'data'	=> $dataID
-				],
-				'image-alt-text'.$addID => [
-					'type'	=> 'text',
-					'label'	=> 'Alt Text',
-					'value'	=> $alt,
-					'hint'	=> 'Alt text helps the visually impaired, as well as some benefits for SEO.',
-					'data'	=> $dataID
-				],
-				'image-caption'.$addID => [
-					'type'	=> 'textarea',
-					'value'	=> $caption,
-					'label'	=> 'Image Caption',
-					'data'	=> $dataID
-				]
-			]
+
+	$defaultFields = [
+		'image-title'.$addID => [
+			'type'	=> 'text',
+			'label'	=> 'Image Title',
+			'value'	=> $title,
+			'data'	=> $dataID
+		],
+		'image-alt-text'.$addID => [
+			'type'	=> 'text',
+			'label'	=> 'Alt Text',
+			'value'	=> $alt,
+			'hint'	=> 'Alt text helps the visually impaired, as well as some benefits for SEO.',
+			'data'	=> $dataID
+		],
+		'image-caption'.$addID => [
+			'type'	=> 'textarea',
+			'value'	=> $caption,
+			'label'	=> 'Image Caption',
+			'data'	=> $dataID
 		]
-	], $fields);
+	];
+	$fields = array_merge($defaultFields, $fields);
 
+	$config = [
+		'type'	=> 'group',
+		'wrap'	=> 'details',
+		'label'	=> 'Image Info',
+		'hint'	=> 'These will be automatically generated if left blank.',
+		'fields'	=> $fields
+	];
 
-	return $form->render('image_data',null, $fields,false, true);
+	return $form->render('image_data',null, $config,false, true);
 }
 
 
@@ -573,7 +578,7 @@
 		return '';
 	}
 
-	$cache = CacheManager::for('locations');
+	$cache = CacheManager::for('locations')->connectTo('taxonomy');
 	$key = $cache->generateKey($location);
 
 	$cached = false;
diff --git a/inc/helpers/time.php b/inc/helpers/time.php
index 126f52a..6f94c49 100644
--- a/inc/helpers/time.php
+++ b/inc/helpers/time.php
@@ -141,7 +141,7 @@
  */
 function jvbRenderHours(int $ID, JVBase\Meta\MetaManager $meta):string
 {
-	$cache = CacheManager::for('hours-'.$ID, WEEK_IN_SECONDS);
+	$cache = CacheManager::for('hours-'.$ID, WEEK_IN_SECONDS)->connectTo('taxonomy');
 	$key = 'hours_display';
 	$cached = $cache->get($key);
 
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index 85f276d..0039a73 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -1,6 +1,7 @@
 <?php
 
 use JVBase\utility\Features;
+use JVBase\utility\Image;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -53,8 +54,7 @@
 				?>
 			</nav>
 		</div>
-		<div class="qitems col">
-			<!-- Operations will be listed here -->
+		<div class="qitems col a-start">
 		</div>
 		<div class="queue-actions row btw">
 			<button class="dismiss-all">Clear Completed</button>
@@ -245,19 +245,10 @@
  *
  * @return string
  */
-function jvbFormatImage(int|string $imgID, string $start = 'tiny', string $end = 'large'):string
+function jvbFormatImage(int $imgID, string $start = 'tiny', string $end = 'large', bool $addLink = true, ?string $postSlug = null):string
 {
-    $block = new \JVBase\blocks\CustomBlocks();
-    if ($imgID === '' || $imgID === 0) {
-        $imgID = $block->imageID($imgID);
-    }
-    if ($imgID === '' || $imgID === 0 || $imgID === false) {
-        return '';
-    }
-
-	$imgID = (int)$imgID;
-
-    return $block->formatImage($imgID, $start, $end);
+    $image = new Image();
+    return $image->formatImage($imgID, $start, $end, $addLink, $postSlug);
 }
 
 /**
@@ -265,8 +256,6 @@
  * @return void
  */
 add_action('wp_footer', 'jvbToastContainer');
-
-
 function jvbToastContainer():void
 {
     ?>
@@ -332,11 +321,12 @@
 add_action('wp_footer', 'jvbLoadingScreen');
 function jvbLoadingScreen():string
 {
+	$icon = apply_filters('jvbLoadingIcon', 'drop-simple');
 	return '<dialog class="loading">
 		<div class="col">
 			<div class="spinner"></div>
 			<div class="status col">
-				<div class="icon">'.apply_filters('jvbLoadingIcon', jvbIcon('drop-simple')).'</div>
+				<div class="icon">'.jvbIcon($icon).'</div>
 				<h3>Loading</h3>
 				<p class="typeText">Please wait...</p>
 			</div>
diff --git a/inc/integrations/GoogleMyBusiness.php b/inc/integrations/GoogleMyBusiness.php
index 3260b8c..e42e0b3 100644
--- a/inc/integrations/GoogleMyBusiness.php
+++ b/inc/integrations/GoogleMyBusiness.php
@@ -85,18 +85,18 @@
 				'label'	=> 'OAuth Client Secret',
 				'required'	=> true,
 			],
-			'access_token'	=> [
-				'type'	=> 'text',
-				'subtype'	=> 'password',
-				'label'	=> 'Access Token',
-				'hint'	=> 'Generated automagically after OAuth authorization.'
-			],
-			'refresh_token'	=> [
-				'type'	=> 'text',
-				'subtype'	=> 'password',
-				'label'	=> 'Refresh Token',
-				'hint'	=> 'Generated automagically after OAuth authorization.'
-			]
+//			'access_token'	=> [
+//				'type'	=> 'text',
+//				'subtype'	=> 'password',
+//				'label'	=> 'Access Token',
+//				'hint'	=> 'Generated automagically after OAuth authorization.'
+//			],
+//			'refresh_token'	=> [
+//				'type'	=> 'text',
+//				'subtype'	=> 'password',
+//				'label'	=> 'Refresh Token',
+//				'hint'	=> 'Generated automagically after OAuth authorization.'
+//			]
 		];
 
 		$this->advanced = [
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 9510af5..6e0d674 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -924,8 +924,11 @@
 		bool   $force = false
 	): ?array
 	{
-		$cacheKey = $this->buildCacheKey('GET', $endpoint, $params);
-		$ttl = $this->cacheStrategy[$cacheStrategy] ?? $this->ttl;
+		$cacheKey = $this->buildCacheKey('GET', $endpoint, $params, $baseKey);
+
+		$ttl = is_int($cacheStrategy)
+			? max(0, $cacheStrategy)
+			: ($this->cacheStrategy[$cacheStrategy] ?? $this->ttl);
 
 		if (!$force && $ttl > 0) {
 			$cached = $this->cache->get($cacheKey);
@@ -944,7 +947,6 @@
 
 		return $result;
 	}
-
 	/**
 	 * Check if response contains an error
 	 * Override in child classes for service-specific error detection
diff --git a/inc/integrations/PostMark.php b/inc/integrations/PostMark.php
index baac1bc..5bb1744 100644
--- a/inc/integrations/PostMark.php
+++ b/inc/integrations/PostMark.php
@@ -257,6 +257,9 @@
 	 */
 	protected function sendEmail(array $payload): bool|WP_Error
 	{
+		if (!$this->isSetUp()) {
+			return false;
+		}
 		try {
 			$response = $this->postRequest('email', $payload);
 
diff --git a/inc/integrations/_setup.php b/inc/integrations/_setup.php
index 2953c01..e3a5d39 100644
--- a/inc/integrations/_setup.php
+++ b/inc/integrations/_setup.php
@@ -40,3 +40,8 @@
 if (Features::hasIntegration('postmark')) {
 	require(JVB_DIR . '/inc/integrations/PostMark.php');
 }
+
+
+if (Features::hasIntegration('helcim')) {
+	require(JVB_DIR . '/inc/integrations/Helcim.php');
+}
diff --git a/inc/managers/AdminPages.php b/inc/managers/AdminPages.php
index d0a1f8c..5145e51 100644
--- a/inc/managers/AdminPages.php
+++ b/inc/managers/AdminPages.php
@@ -2,6 +2,7 @@
 namespace JVBase\managers;
 
 use JVBase\utility\Features;
+use WP_REST_Response;
 
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
@@ -43,8 +44,163 @@
         // Hook into WordPress admin
         add_action('admin_menu', [$this, 'registerAdminPages']);
         add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']);
+
+		add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
+
+		add_action('rest_api_init', [$this, 'registerRestRoutes']);
     }
 
+	/**
+	 * Register REST API routes for admin actions
+	 */
+	public function registerRestRoutes(): void
+	{
+		register_rest_route('jvb/v1', '/admin-cache', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleCacheAction'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		register_rest_route('jvb/v1', '/admin-icons', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleIconAction'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+	}
+
+	/**
+	 * Check if user has admin permissions
+	 */
+	public function checkAdminPermission(\WP_REST_Request $request): bool
+	{
+		if (!current_user_can('manage_options')) {
+			return false;
+		}
+
+		// Verify nonce
+		$nonce = $request->get_header('X-WP-Nonce');
+		if (!wp_verify_nonce($nonce, 'wp_rest')) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Handle cache-related actions
+	 */
+	public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$action = sanitize_text_field($request->get_param('action'));
+
+		switch ($action) {
+			case 'flush-all':
+				wp_cache_flush();
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => 'All caches flushed successfully'
+				]);
+
+			case 'flush-cache':
+				$group = sanitize_text_field($request->get_param('group'));
+				if (empty($group)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No cache group specified'
+					], 400);
+				}
+
+				\JVBase\managers\CacheManager::invalidateAll($group);
+
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => "Cache group '{$group}' flushed successfully"
+				]);
+
+			default:
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Invalid action'
+				], 400);
+		}
+	}
+
+	/**
+	 * Handle icon-related actions
+	 */
+	public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$action = sanitize_text_field($request->get_param('action'));
+		$icons = \JVBase\managers\IconsManager::getInstance();
+
+		switch ($action) {
+			case 'refresh-icons':
+				$icons->forceRefresh();
+				return new \WP_REST_Response([
+					'success' => true,
+					'message' => 'Icon CSS regenerated successfully'
+				]);
+
+			case 'restore-icon-version':
+				$timestamp = (int)$request->get_param('timestamp');
+				if (empty($timestamp)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No timestamp provided'
+					], 400);
+				}
+
+				if ($icons->restoreVersion($timestamp)) {
+					return new \WP_REST_Response([
+						'success' => true,
+						'message' => 'Icon version restored successfully'
+					]);
+				}
+
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Failed to restore icon version'
+				], 500);
+
+			case 'merge-icon-versions':
+				$timestamps = $request->get_param('timestamps');
+
+				if (empty($timestamps) || !is_array($timestamps)) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'No versions selected for merging'
+					], 400);
+				}
+
+				$timestamps = array_map('intval', $timestamps);
+
+				if (count($timestamps) < 2) {
+					return new \WP_REST_Response([
+						'success' => false,
+						'message' => 'Please select at least 2 versions to merge'
+					], 400);
+				}
+
+				if ($icons->mergeVersions($timestamps)) {
+					return new \WP_REST_Response([
+						'success' => true,
+						'message' => 'Icon versions merged successfully'
+					]);
+				}
+
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Failed to merge icon versions'
+				], 500);
+
+			default:
+				return new \WP_REST_Response([
+					'success' => false,
+					'message' => 'Invalid action'
+				], 400);
+		}
+	}
+
     /**
      * Register a subpage to appear under the main settings page
      *
@@ -179,6 +335,14 @@
             BASE.'cache',
             [$this, 'renderCachePage']
         );
+		add_submenu_page(
+			$this->main_page['menu_slug'],
+			'Icon Management',
+			'Icons',
+			'manage_options',
+			BASE.'icons',
+			[$this, 'renderIconsPage']
+		);
 
 //        $this->getSubpages();
         // Add registered subpages
@@ -489,42 +653,464 @@
         return 'data:image/svg+xml;base64,' . base64_encode($svg);
     }
 
-    public function renderCachePage()
-    {
-        $groups = get_option(BASE.'all_cache_groups', []);
+	public function renderCachePage():void
+	{
+		$connections = CacheManager::getAllConnections();
 
-        ?>
-        <h1>Manage Cache</h1>
-        <?php
-        foreach ($groups as $group => $caches) {
-            ?>
-            <details>
-                <summary class="row btw"><h2><?=$group?></h2></summary>
-                <table>
-                    <thead>
-                        <tr>
-                            <th scope="col"><input type="checkbox" name="select-all-<?=$group?>" id="select-all-<?=$group?>">
-                                <label for="select-all-<?=$group?>">All</label></th>
-                            <th scope="col">Cache Key</th>
-                            <th scope="col">Actions</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                    <?php
-                    foreach ($caches as $key) {
-                        ?>
-                        <tr>
-                            <td><input type="checkbox" name="select-<?=$group?>-<?=$key?>" id="select-<?=$group?>-<?=$key?>"><label for="select-<?=$group?>-<?=$key?>"></label></td>
-                            <td><?= $key ?></td>
-                            <td><button type="button" data-action="flush-<?=$group?>-<?=$key?>"><?= jvbIcon('trash')?></button></td>
-                        </tr>
-                        <?php
-                    }
-                    ?>
-                    </tbody>
-                </table>
-            </details>
-            <?php
-        }
-    }
+		// Separate generic vs. specific caches
+		$generic_groups = [];
+		$content_specific = [];
+		$nonce = wp_create_nonce('wp_rest');
+
+		foreach ($connections as $group => $configs) {
+			$is_generic = !$this->isBoundToContentOrTaxonomy($group);
+
+			if ($is_generic) {
+				$generic_groups[$group] = $configs;
+			} else {
+				$content_specific[$group] = $configs;
+			}
+		}
+
+		?>
+		<div class="wrap jvb-admin-wrap">
+			<h1>Cache Management</h1>
+
+			<div class="jvb-cache-actions">
+				<button type="button" class="button button-primary" data-action="flush-all">
+					<?= jvbIcon('arrows-clockwise'); ?>
+					Flush All Caches
+				</button>
+			</div>
+
+			<div class="jvb-cache-section">
+				<h2>Generic Caches &amp; Connections</h2>
+				<table class="wp-list-table widefat fixed striped">
+					<thead>
+					<tr>
+						<th class="manage-column">Cache Group</th>
+						<th class="manage-column">Connected To</th>
+						<th class="manage-column">Actions</th>
+					</tr>
+					</thead>
+					<tbody>
+					<?php if (empty($generic_groups)): ?>
+						<tr><td colspan="3">No generic caches registered</td></tr>
+					<?php else: ?>
+						<?php foreach ($generic_groups as $group => $configs): ?>
+							<tr>
+								<td><strong><?= esc_html($group); ?></strong></td>
+								<td><?= $this->formatConnections($configs); ?></td>
+								<td>
+									<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
+										<?= jvbIcon('trash'); ?> Flush
+									</button>
+								</td>
+							</tr>
+						<?php endforeach; ?>
+					<?php endif; ?>
+					</tbody>
+				</table>
+			</div>
+
+			<details class="jvb-cache-section">
+				<summary><h2>Content-Specific Caches</h2></summary>
+				<table class="wp-list-table widefat fixed striped">
+					<thead>
+					<tr>
+						<th>Cache Group</th>
+						<th>Connected To</th>
+						<th>Actions</th>
+					</tr>
+					</thead>
+					<tbody>
+					<?php foreach ($content_specific as $group => $configs): ?>
+						<tr>
+							<td><strong><?= esc_html($group); ?></strong></td>
+							<td><?= $this->formatConnections($configs); ?></td>
+							<td>
+								<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
+									<?= jvbIcon('trash'); ?> Flush
+								</button>
+							</td>
+						</tr>
+					<?php endforeach; ?>
+					</tbody>
+				</table>
+			</details>
+		</div>
+		<script>
+			(function() {
+				const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-cache')); ?>';
+				const nonce = '<?= esc_js($nonce); ?>';
+
+				function callCacheAction(action, data = {}) {
+					const body = { action, ...data };
+
+					return fetch(apiUrl, {
+						method: 'POST',
+						headers: {
+							'Content-Type': 'application/json',
+							'X-WP-Nonce': nonce
+						},
+						body: JSON.stringify(body)
+					})
+						.then(response => response.json())
+						.then(data => {
+							if (data.success) {
+								alert(data.message || 'Success!');
+								location.reload();
+							} else {
+								alert('Error: ' + (data.message || 'Unknown error'));
+							}
+						})
+						.catch(error => {
+							alert('Network error: ' + error.message);
+							console.error('Error:', error);
+						});
+				}
+
+				// Flush all caches
+				document.querySelector('[data-action="flush-all"]')?.addEventListener('click', function() {
+					if (confirm('Flush all caches? This may temporarily slow down your site.')) {
+						this.disabled = true;
+						callCacheAction('flush-all');
+					}
+				});
+
+				// Flush individual cache groups
+				document.querySelectorAll('[data-action="flush-cache"]').forEach(btn => {
+					btn.addEventListener('click', function() {
+						const group = this.getAttribute('data-group');
+						if (confirm(`Flush cache group "${group}"?`)) {
+							this.disabled = true;
+							callCacheAction('flush-cache', { group: group });
+						}
+					});
+				});
+			})();
+		</script>
+		<?php
+	}
+
+	protected function isBoundToContentOrTaxonomy(string $group): bool
+	{
+		$group = jvbNoBase($group);
+
+		if (defined('JVB_CONTENT')) {
+			foreach (JVB_CONTENT as $key => $config) {
+				if (jvbNoBase($key) === $group) {
+					return true;
+				}
+			}
+		}
+
+		if (defined('JVB_TAXONOMY')) {
+			foreach (JVB_TAXONOMY as $key => $config) {
+				if (jvbNoBase($key) === $group) {
+					return true;
+				}
+			}
+		}
+
+		return false;
+	}
+
+	protected function formatConnections(array $configs): string
+	{
+		$connections = [];
+		foreach ($configs as $config) {
+			$parent = $config['parent'] ?? 'unknown';
+			$scope = $config['scope'] ?? 'id';
+			$connections[] = "{$parent} ({$scope})";
+		}
+		return esc_html(implode(', ', $connections));
+	}
+
+	public function handleCacheActions($response, $request, $action):WP_REST_Response
+	{
+		if (!str_starts_with($action, 'flush-')) {
+			return $response;
+		}
+
+		if ($action === 'flush-all') {
+			wp_cache_flush();
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => 'All caches flushed successfully'
+			]);
+		}
+
+		if (str_starts_with($action, 'flush-cache')) {
+			$group = $request->get_param('group');
+			if (empty($group)) {
+				return new WP_REST_Response([
+					'success' => false,
+					'message' => 'No cache group specified'
+				], 400);
+			}
+
+			\JVBase\managers\CacheManager::invalidateAll($group);
+
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => "Cache group '{$group}' flushed successfully"
+			]);
+		}
+
+		if ($action === 'merge-icon-versions') {
+			$timestamps = $request->get_param('timestamps');
+
+			if (empty($timestamps) || !is_array($timestamps)) {
+				return new WP_REST_Response([
+					'success' => false,
+					'message' => 'No versions selected for merging'
+				], 400);
+			}
+
+			// Convert to integers
+			$timestamps = array_map('intval', $timestamps);
+
+			if (count($timestamps) < 2) {
+				return new WP_REST_Response([
+					'success' => false,
+					'message' => 'Please select at least 2 versions to merge'
+				], 400);
+			}
+
+			$icons = \JVBase\managers\IconsManager::getInstance();
+
+			if ($icons->mergeVersions($timestamps)) {
+				return new WP_REST_Response([
+					'success' => true,
+					'message' => 'Icon versions merged successfully'
+				]);
+			}
+
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Failed to merge icon versions'
+			], 500);
+		}
+
+		if ($action === 'refresh-icons') {
+			$icons = \JVBase\managers\IconsManager::getInstance();
+			$icons->forceRefresh();
+
+			return new WP_REST_Response([
+				'success' => true,
+				'message' => 'Icon CSS refresh triggered'
+			]);
+		}
+
+		if ($action === 'restore-icon-version') {
+			$timestamp = (int)$request->get_param('timestamp');
+			$icons = \JVBase\managers\IconsManager::getInstance();
+
+			if ($icons->restoreVersion($timestamp)) {
+				return new WP_REST_Response([
+					'success' => true,
+					'message' => 'Icon version restored successfully'
+				]);
+			}
+
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Failed to restore icon version'
+			], 500);
+		}
+
+		return $response;
+	}
+
+	public function renderIconsPage():void
+	{
+		$icons = \JVBase\managers\IconsManager::getInstance();
+		$versions = $icons->getVersionHistory();
+		$nonce = wp_create_nonce('wp_rest');
+
+		?>
+		<div class="wrap jvb-admin-wrap">
+			<h1>Icon Management</h1>
+
+			<div class="jvb-icon-actions">
+				<button type="button" class="button button-primary" data-action="refresh-icons">
+					<?= jvbIcon('arrows-clockwise'); ?>
+					Force Refresh CSS
+				</button>
+				<button type="button" class="button" data-action="merge-icon-versions" id="merge-versions-btn" disabled>
+					<?= jvbIcon('git-merge'); ?>
+					Merge Selected Versions
+				</button>
+			</div>
+
+			<h2>Version History</h2>
+			<table class="wp-list-table widefat fixed striped">
+				<thead>
+				<tr>
+					<th class="check-column">
+						<input type="checkbox" id="select-all-versions">
+						<label for="select-all-versions" class="screen-reader-text">Select All</label>
+					</th>
+					<th>Date/Time</th>
+					<th>Icon Count</th>
+					<th>File Size</th>
+					<th>Actions</th>
+				</tr>
+				</thead>
+				<tbody>
+				<?php if (empty($versions)): ?>
+					<tr><td colspan="5">No version history available</td></tr>
+				<?php else: ?>
+					<?php foreach (array_reverse($versions) as $index => $version): ?>
+						<tr>
+							<th class="check-column">
+								<input type="checkbox"
+									   name="version-select"
+									   class="version-checkbox"
+									   value="<?= esc_attr($version['timestamp']); ?>">
+							</th>
+							<td><?= esc_html(date('Y-m-d H:i:s', $version['timestamp'])); ?></td>
+							<td>
+								<?= esc_html($version['icon_count']); ?> icons
+								<button type="button"
+										class="button-link"
+										data-action="view-icon-list"
+										data-timestamp="<?= esc_attr($version['timestamp']); ?>">
+									(view)
+								</button>
+							</td>
+							<td><?= esc_html($version['size_formatted']); ?></td>
+							<td>
+								<button type="button" class="button"
+										data-action="restore-icon-version"
+										data-timestamp="<?= esc_attr($version['timestamp']); ?>">
+									<?= jvbIcon('arrow-counter-clockwise'); ?> Restore
+								</button>
+							</td>
+						</tr>
+						<tr id="icon-list-<?= esc_attr($version['timestamp']); ?>" class="icon-list-row" style="display: none;">
+							<td colspan="5">
+								<div class="icon-list-content">
+									<?php foreach ($version['iconList'] as $style => $icons): ?>
+										<strong><?= esc_html(ucfirst($style)); ?>:</strong>
+										<?= esc_html(implode(', ', $icons)); ?><br>
+									<?php endforeach; ?>
+								</div>
+							</td>
+						</tr>
+					<?php endforeach; ?>
+				<?php endif; ?>
+				</tbody>
+			</table>
+		</div>
+
+		<script>
+			(function() {
+				const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>';
+				const nonce = '<?= esc_js($nonce); ?>';
+
+				// Helper function for API calls
+				function callIconAction(action, data = {}) {
+					const body = { action, ...data };
+
+					return fetch(apiUrl, {
+						method: 'POST',
+						headers: {
+							'Content-Type': 'application/json',
+							'X-WP-Nonce': nonce
+						},
+						body: JSON.stringify(body)
+					})
+						.then(response => response.json())
+						.then(data => {
+							if (data.success) {
+								alert(data.message || 'Success!');
+								location.reload();
+							} else {
+								alert('Error: ' + (data.message || 'Unknown error'));
+							}
+							return data;
+						})
+						.catch(error => {
+							alert('Network error: ' + error.message);
+							console.error('Error:', error);
+						});
+				}
+
+				// Enable/disable merge button based on selection
+				document.querySelectorAll('.version-checkbox').forEach(checkbox => {
+					checkbox.addEventListener('change', function() {
+						const checkedCount = document.querySelectorAll('.version-checkbox:checked').length;
+						document.getElementById('merge-versions-btn').disabled = checkedCount < 2;
+					});
+				});
+
+				// Select all functionality
+				const selectAll = document.getElementById('select-all-versions');
+				if (selectAll) {
+					selectAll.addEventListener('change', function() {
+						document.querySelectorAll('.version-checkbox').forEach(checkbox => {
+							checkbox.checked = this.checked;
+							checkbox.dispatchEvent(new Event('change'));
+						});
+					});
+				}
+
+				// Toggle icon list view
+				document.querySelectorAll('.view-icon-list-btn').forEach(btn => {
+					btn.addEventListener('click', function() {
+						const timestamp = this.getAttribute('data-timestamp');
+						const row = document.getElementById('icon-list-' + timestamp);
+						if (row) {
+							row.style.display = row.style.display === 'none' ? '' : 'none';
+						}
+					});
+				});
+
+				// Force refresh button
+				const refreshBtn = document.getElementById('refresh-icons-btn');
+				if (refreshBtn) {
+					refreshBtn.addEventListener('click', function() {
+						if (confirm('Force regenerate icon CSS? This will reload the page.')) {
+							this.disabled = true;
+							callIconAction('refresh-icons');
+						}
+					});
+				}
+
+				// Merge versions button
+				const mergeBtn = document.getElementById('merge-versions-btn');
+				if (mergeBtn) {
+					mergeBtn.addEventListener('click', function() {
+						const checkboxes = document.querySelectorAll('.version-checkbox:checked');
+						const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
+
+						if (timestamps.length < 2) {
+							alert('Please select at least 2 versions to merge');
+							return;
+						}
+
+						if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
+							this.disabled = true;
+							callIconAction('merge-icon-versions', { timestamps: timestamps });
+						}
+					});
+				}
+
+				// Restore version buttons
+				document.querySelectorAll('.restore-version-btn').forEach(btn => {
+					btn.addEventListener('click', function() {
+						const timestamp = parseInt(this.getAttribute('data-timestamp'));
+
+						if (confirm('Restore this icon version? This will reload the page.')) {
+							this.disabled = true;
+							callIconAction('restore-icon-version', { timestamp: timestamp });
+						}
+					});
+				});
+			})();
+		</script>
+		<?php
+	}
 }
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index aa4293d..e05a0e5 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -58,6 +58,10 @@
 		];
 
 		$this->init();
+
+		if ($this->isTimeline) {
+			$this->stuck[] = 'post_thumbnail';
+		}
 		add_filter('jvbAdditionalActions', [$this, 'createItem']);
 	}
 
@@ -89,6 +93,7 @@
 		}));
 		array_unshift($this->timelineSharedFields, 'post_thumbnail');
 		array_unshift($this->timelineSharedFields, 'post_title');
+		array_unshift($this->timelineSharedFields, 'post_status');
 
 		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
 			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
@@ -192,7 +197,7 @@
 	{
 		ob_start();
 		?>
-		<div class="dashboard-page <?= esc_attr($this->content) ?>">
+		<div class="dashboard-page <?= esc_attr($this->content) ?>"<?=($this->isTimeline) ? ' data-timeline' : ''?>>
 			<?php
 			$this->renderHeader();
 			$this->renderContent();
@@ -331,7 +336,7 @@
 			<?php
 				$order = [
 					'orderby' => [
-						'calendar' => 'Order by date created',
+						'date' => 'Order by date created',
 						'alphabetical' => 'Order alphabetically'
 					],
 					'order' => [
@@ -347,10 +352,11 @@
 					<?php
 					$i = 0;
 					foreach ($option as $opt => $label) {
+						$icon = $opt === 'date' ? 'calendar' : $opt;
 						?>
 						<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
 
-						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($opt)?></label>
+						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($icon)?></label>
 						<?php
 						$i++;
 					}
@@ -736,36 +742,6 @@
 	}
 
 
-	protected function renderTimelineFields():string
-	{
-		ob_start();
-
-
-		?>
-		<div class="repeater-field timeline-repeater" data-timeline data-field="fields">
-			<div class="repeater-rows" data-repeater-container>
-				<!-- Parent row (non-draggable) -->
-				<div class="repeater-row parent-row" data-row-index="0" data-id="">
-					<div class="row-header">
-						<h4>Before (Starting Point)</h4>
-					</div>
-					<div class="row-fields">
-						<?php $this->renderRowFields(); ?>
-					</div>
-				</div>
-
-				<!-- Child rows will be added dynamically -->
-			</div>
-
-			<button type="button" class="add-repeater-row btn secondary">
-				<?= jvbIcon('plus-square') ?>
-				<span>Add Progress Step</span>
-			</button>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
 	protected function renderRowFields():void
 	{
 		$fields = $this->fields;
@@ -1010,6 +986,10 @@
 
 	protected function renderTableView():void
 	{
+		if ($this->isTimeline) {
+			$this->renderTimelineTableView();
+			return;
+		}
 		?>
 		<template class="contentTable">
 			<form class="table"
@@ -1038,13 +1018,17 @@
 	 * Render table row template
 	 */
 	protected function renderTableRow(): void {
+		if ($this->isTimeline) {
+			$this->renderTimelineTableGroup();
+			return;
+		}
 		?>
 		<template class="tableView">
 			<tr class="item">
 				<td class="select">
 					<?= $this->renderItemSelect() ?>
 				</td>
-				<td class="status">
+				<td class="status" data-field="post_status">
 					<?= $this->renderStatusRadios() ?>
 				</td>
 				<?php
@@ -1070,6 +1054,102 @@
 		</template>
 		<?php
 	}
+
+	protected function renderTimelineTableView():void
+	{
+		?>
+		<template class="contentTable">
+			<form class="table"
+				  data-save="content"
+				  data-content="<?= esc_attr($this->content) ?>"
+				  data-form-id="content-table-<?= esc_attr($this->content) ?>">
+				<?= jvbFormStatus() ?>
+				<?= $this->renderTableActions() ?>
+
+				<table>
+					<thead>
+					<?= $this->renderTimelineTableHeader() ?>
+					</thead>
+					<!-- Rows are inserted as tbody groups -->
+					<tfoot>
+					<?= $this->renderTimelineTableHeader() ?>
+					</tfoot>
+				</table>
+			</form>
+		</template>
+		<?php
+	}
+
+	protected function renderTimelineTableGroup():void
+	{
+		$makeDetails = [
+			'group',
+			'repeater',
+			'checkbox',
+			'radio'
+		];
+		?>
+		<template class="tableView">
+			<tbody class="item">
+				<tr class="shared">
+					<td class="select">
+						<?= $this->renderItemSelect() ?>
+					</td>
+					<td class="show-post_status field" data-field="post_status">
+						<?= $this->renderStatusRadios() ?>
+					</td>
+					<?php
+					foreach ($this->fields as $name => $config) {
+						if(array_key_exists('hidden', $config) || $name === 'post_status') {
+							continue;
+						}
+						if (!in_array($name, $this->timelineSharedFields)) {
+							echo '<td></td>';
+							continue;
+						}
+							$makeThisDetailed = (in_array($config['type'], $makeDetails));
+						?>
+						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+							<?php $this->meta->render('form', $name, $config); ?>
+							<?= $makeThisDetailed ? '</details>' : '' ?>
+						</td>
+						<?php
+					}
+
+					?>
+				</tr>
+				<tr class="timeline-point">
+					<td class="select">
+						<button class="drag-handle" title="Drag to reorder" aria-label="Drag to reorder this timeline point"><?= jvbIcon('dots-six') ?></button>
+					</td>
+					<td class="show-post_status field" data-field="post_status">
+						<?= $this->renderStatusRadios() ?>
+					</td>
+					<?php
+					foreach ($this->fields as $name => $config) {
+						if(array_key_exists('hidden', $config) || $name === 'post_status') {
+							continue;
+						}
+						if (!in_array($name, $this->timelineUniqueFields)) {
+							echo '<td></td>';
+							continue;
+						}
+						$makeThisDetailed = (in_array($config['type'], $makeDetails));
+						?>
+						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+							<?php $this->meta->render('form', $name, $config); ?>
+							<?= $makeThisDetailed ? '</details>' : '' ?>
+						</td>
+						<?php
+					}
+					?>
+				</tr>
+			</tbody>
+		</template>
+		<?php
+	}
 	/**
 	 * Render status radio buttons
 	 */
@@ -1131,6 +1211,32 @@
 		return ob_get_clean();
 	}
 
+	protected function renderTimelineTableHeader(): string {
+		ob_start();
+
+		?>
+		<tr>
+			<th scope="col" class="select-header">
+				<input type="checkbox" id="select-all" name="select-all">
+				<label for="select-all">All</label>
+			</th>
+			<th scope="col" class="show-post_status">
+				Status
+			</th>
+			<?php foreach ($this->fields as $name => $config):
+				if (array_key_exists('hidden', $config) || $name === 'post_status'){
+					continue;
+				}
+				?>
+				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+					<?= esc_html($config['label']) ?>
+				</th>
+			<?php endforeach; ?>
+		</tr>
+		<?php
+		return ob_get_clean();
+	}
+
 	/**
 	 * Render table action controls
 	 */
diff --git a/inc/managers/CacheManager.php b/inc/managers/CacheManager.php
index c8a38b9..8612385 100644
--- a/inc/managers/CacheManager.php
+++ b/inc/managers/CacheManager.php
@@ -13,14 +13,15 @@
  */
 class CacheManager
 {
+	private const CONNECTIONS_OPTION = BASE.'cache_connections';
+	private static ?array $connections_cache = null; // Cache in memory
 	private string $prefix = BASE;
 	private string $group;
 	private int $cache_ttl;
 	private static ?bool $use_object_cache = null;
 	private static array $instances = []; // Cache instances per type
 	private static array $http_timestamps = []; // Request-level memory cache
-	private static array $relationships = []; // Type => [related types]
-	private static bool $relationships_loaded = false;
+	private static ?CacheManager $singleton = null;
 
 	/**
 	 * Private constructor - use for() factory method instead
@@ -33,6 +34,76 @@
 		if (is_null(static::$use_object_cache)) {
 			static::$use_object_cache = wp_using_ext_object_cache();
 		}
+
+		add_action('init', [$this, 'registerHooks']);
+	}
+
+	/**
+	 * Get singleton instance (for general cache operations)
+	 * For type-specific operations, use for() or forUser() instead
+	 */
+	public static function getInstance(): self
+	{
+		if (self::$singleton === null) {
+			self::$singleton = new self('global', HOUR_IN_SECONDS);
+		}
+		return self::$singleton;
+	}
+
+	/**
+	 * Get all cache connections (public accessor)
+	 *
+	 * @return array Array of cache group connections
+	 */
+	public static function getAllConnections(): array
+	{
+		return self::getConnections();
+	}
+
+	/**
+	 * Get all registered cache groups
+	 *
+	 * @return array List of cache group names
+	 */
+	public static function getAllGroups(): array
+	{
+		$connections = self::getConnections();
+		return array_keys($connections);
+	}
+	/**
+	 * Register WordPress hooks for automatic cache invalidation
+	 * Call this once during plugin initialization
+	 */
+	public static function registerHooks(): void
+	{
+		// Post updates (all post types including core)
+		add_action('save_post', [self::class, 'onPostSave'], 10, 2);
+		add_action('delete_post', [self::class, 'onPostDelete']);
+		// Meta updates (will catch MetaManager updates)
+		add_action('updated_post_meta', [self::class, 'onPostMetaUpdate'], 10, 4);
+		add_action('added_post_meta', [self::class, 'onPostMetaUpdate'], 10, 4);
+		add_action('deleted_post_meta', [self::class, 'onPostMetaDelete'], 10, 4);
+		// transition_post_status?
+
+		// Term updates (all taxonomies)
+		add_action('edited_term', [self::class, 'onTermSave'], 10, 3);
+		add_action('create_term', [self::class, 'onTermSave'], 10, 3);
+		add_action('delete_term', [self::class, 'onTermDelete'], 10, 3);
+
+		// Term meta updates
+		add_action('updated_term_meta', [self::class, 'onTermMetaUpdate'], 10, 4);
+		add_action('added_term_meta', [self::class, 'onTermMetaUpdate'], 10, 4);
+		add_action('deleted_term_meta', [self::class, 'onTermMetaDelete'], 10, 4);
+
+		// User updates
+		add_action('profile_update', [self::class, 'onUserUpdate'], 10, 2);
+		add_action('user_register', [self::class, 'onUserUpdate'], 10, 1);
+		add_action('deleted_user', [self::class, 'onUserDelete']);
+
+		// User meta updates
+		add_action('updated_user_meta', [self::class, 'onUserMetaUpdate'], 10, 4);
+		add_action('added_user_meta', [self::class, 'onUserMetaUpdate'], 10, 4);
+		add_action('deleted_user_meta', [self::class, 'onUserMetaDelete'], 10, 4);
 	}
 
 	/**
@@ -127,21 +198,21 @@
 	}
 
 	/**
-	 * Invalidate cache for a content type with automatic cascade
+	 * Invalidate cache for a content type
 	 *
 	 * @param string $type Content type to invalidate
-	 * @param mixed $context Post/Term object or array with relationship data (for cascade)
 	 * @param string|array|null $specific_keys Optional specific key(s) to delete without flushing group
+	 * @param bool $flush_connections Whether to flush connected caches
 	 * @return void
 	 */
-	public static function invalidateAll(string $type, $context = null, $specific_keys = null): void
+	public static function invalidateAll(string $type, $specific_keys = null, bool $flush_connections = true): void
 	{
 		$type = jvbNoBase($type);
 
 		// Update HTTP timestamp
 		self::updateTimestamp($type);
 
-		// If specific keys provided, only delete those (don't flush whole group)
+		// If specific keys provided, only delete those
 		if ($specific_keys !== null) {
 			$instance = self::for($type);
 			if (is_array($specific_keys)) {
@@ -152,21 +223,20 @@
 				$instance->delete($specific_keys);
 			}
 		} else {
-			// No specific keys - flush the entire group
+			// Flush the entire group
 			if (function_exists('wp_cache_flush_group')) {
 				wp_cache_flush_group($type);
 			} else {
-				// Fallback for older WP
 				wp_cache_flush();
 			}
 		}
 
-		// Cascade to related types if context provided
-		if ($context !== null) {
-			self::cascadeInvalidation($type, $context);
+		// Flush connected caches
+		if ($flush_connections) {
+			self::for($type)->connections();
 		}
 
-		do_action('jvb_cache_invalidated', $type, $context);
+		do_action('jvb_cache_invalidated', $type);
 	}
 
 	/**
@@ -192,15 +262,14 @@
 
 	/**
 	 * Fluent instance method to invalidate this cache type
-	 * Allows chaining: CacheManager::for('tattoo')->invalidate()->clear()
 	 *
-	 * @param mixed $context Optional context for cascade
 	 * @param string|array|null $specific_keys Optional specific key(s)
+	 * @param bool $flush_connections Whether to flush connected caches
 	 * @return self For chaining
 	 */
-	public function invalidate($context = null, $specific_keys = null): self
+	public function invalidate($specific_keys = null, bool $flush_connections = true): self
 	{
-		self::invalidateAll($this->group, $context, $specific_keys);
+		self::invalidateAll($this->group, $specific_keys, $flush_connections);
 		return $this;
 	}
 
@@ -360,308 +429,383 @@
 		return $this->group;
 	}
 
-	// ===== RELATIONSHIP MANAGEMENT =====
 
+	/***************************************************************************
+	 * CONNECTIONS
+	 * Connect to other caches by instantiating and defining connection
+	 * Ex: CacheManager::for('usernames')->connectTo($type, $scope = 'all', $keyPattern)
+	 * Where: 	$type = content / taxonomy / user
+	 * 			$scope = either 'id' for specific item, or the entire group (registered post type, taxonomy, or user role)
+	 * 			$keyPattern = ??
+	***************************************************************************/
 	/**
-	 * Register cache relationship
-	 * When $type is invalidated, these related types are also invalidated
+	 * Define a connection between cache groups
+	 * Connected caches will have their ID-based keys deleted when this cache invalidates
 	 *
-	 * @param string $type Primary type
-	 * @param array $config Relationship configuration
-	 *   - 'author' => bool - Invalidate user content caches
-	 *   - 'taxonomies' => array - List of taxonomy types to invalidate
-	 *   - 'content_types' => array - List of content types to invalidate
-	 *   - 'related' => array - Generic related types to invalidate
-	 *   - 'cascade' => callable - Custom cascade function
+	 * @param string $type Grand overview ('post', 'taxonomy', 'user')
+	 * @param string $scope Type-specific constant, user role, or 'id'
+	 * @return self For chaining
 	 */
-	public static function registerRelationship(string $type, array $config): void
+	public function connectTo(string $type, string $scope = 'id'): self
 	{
-		$type = jvbNoBase($type);
+		//TODO: Handle connect to where $type === 'all'
+		$connections = self::getConnections();
 
-		// Merge with existing relationships
-		self::$relationships[$type] = array_merge(
-			self::$relationships[$type] ?? [],
-			$config
-		);
+		if (!isset($connections[$this->group])) {
+			$connections[$this->group] = [];
+		}
 
-		// Build reverse relationships for bidirectional linking
-		self::buildReverseRelationships($type, $config);
-	}
+		$new_connection = [
+			'parent' => $type,
+			'scope' => $scope
+		];
 
-	/**
-	 * Build reverse relationships (if A relates to B, B should know about A)
-	 *
-	 * @param string $type The type being registered
-	 * @param array $config Its relationship config
-	 */
-	private static function buildReverseRelationships(string $type, array $config): void
-	{
-		// If this type relates to taxonomies, those taxonomies should know about this type
-		if (!empty($config['taxonomies'])) {
-			foreach ($config['taxonomies'] as $taxonomy) {
-				$taxonomy = jvbNoBase($taxonomy);
-				self::$relationships[$taxonomy]['content_types'] =
-					array_unique(array_merge(
-						self::$relationships[$taxonomy]['content_types'] ?? [],
-						[$type]
-					));
+		// Check if already exists
+		foreach ($connections[$this->group] as $existing) {
+			if ($existing === $new_connection) {
+				return $this;
 			}
 		}
 
-		// If this type relates to content_types, those types should know about this taxonomy
-		if (!empty($config['content_types'])) {
-			foreach ($config['content_types'] as $content_type) {
-				$content_type = jvbNoBase($content_type);
-				self::$relationships[$content_type]['related'] =
-					array_unique(array_merge(
-						self::$relationships[$content_type]['related'] ?? [],
-						[$type]
-					));
+		$connections[$this->group][] = $new_connection;
+		update_option(self::CONNECTIONS_OPTION, $connections, false);
+		self::$connections_cache = $connections;
+
+		return $this;
+	}
+
+	/**
+	 * Get all registered connections (cached for performance)
+	 *
+	 * @param bool $refresh Force refresh from database
+	 * @return array
+	 */
+	private static function getConnections(bool $refresh = false): array
+	{
+		if (self::$connections_cache === null || $refresh) {
+			self::$connections_cache = get_option(self::CONNECTIONS_OPTION, []);
+		}
+
+		return self::$connections_cache;
+	}
+
+	/**
+	 * Flush all caches connected to this one
+	 *
+	 * @return self For chaining
+	 */
+	public function connections(): self
+	{
+		$all_connections = self::getConnections();
+
+		foreach ($all_connections as $cache_group => $connections) {
+			foreach ($connections as $conn) {
+				if ($this->matchesConnection($conn)) {
+					$this->flushConnection($cache_group, $conn);
+				}
 			}
 		}
+
+		return $this;
+	}
+
+	/**
+	 * Check if this cache group matches a connection definition
+	 */
+	private function matchesConnection(array $connection): bool
+	{
+		$parent = $connection['parent'] ?? '';
+		$scope = $connection['scope'] ?? 'id';
+
+		// Grand overview match
+		if ($this->group === $parent) {
+			return true;
+		}
+
+		// Type-specific match
+		if ($scope !== 'id') {
+			if ($this->group === jvbNoBase($scope)) {
+				return true;
+			}
+
+			// Check constants
+			if ($parent === 'post' && defined('JVB_CONTENT')) {
+				return isset(JVB_CONTENT[$scope]) && jvbNoBase($scope) === $this->group;
+			}
+
+			if ($parent === 'taxonomy' && defined('JVB_TAXONOMY')) {
+				return isset(JVB_TAXONOMY[$scope]) && jvbNoBase($scope) === $this->group;
+			}
+		}
+
+		// ID-specific match: 'user_123' matches 'user' + 'id'
+		if ($scope === 'id' && str_starts_with($this->group, $parent . '_')) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Flush a connected cache group
+	 * For ID-specific connections, deletes the specific ID key
+	 * For type/overview connections, flushes entire group
+	 */
+	private function flushConnection(string $cache_group, array $connection): void
+	{
+		$scope = $connection['scope'] ?? 'id';
+
+		// ID-specific: delete specific key
+		if ($scope === 'id') {
+			$id = $this->extractIdFromGroup();
+
+			if ($id !== null) {
+				self::invalidateKeys($cache_group, $id);
+				return;
+			}
+		}
+
+		// Type/overview: flush entire group
+		self::invalidateAll($cache_group, specific_keys: null, flush_connections: false);
+	}
+
+	/**
+	 * Extract ID from group name like 'user_123' -> '123'
+	 *
+	 * @return string|null
+	 */
+	private function extractIdFromGroup(): ?string
+	{
+		if (preg_match('/^[a-z]+_(\d+)$/', $this->group, $matches)) {
+			return $matches[1];
+		}
+
+		return null;
+	}
+
+	/**
+	 * Register multiple connections at once
+	 */
+	public static function registerConnections(array $connections): void
+	{
+		$existing = self::getConnections();
+		$changed = false;
+
+		foreach ($connections as $cache_group => $configs) {
+			if (!isset($existing[$cache_group])) {
+				$existing[$cache_group] = [];
+			}
+
+			foreach ($configs as $config) {
+				$duplicate = false;
+				foreach ($existing[$cache_group] as $existing_config) {
+					if ($existing_config === $config) {
+						$duplicate = true;
+						break;
+					}
+				}
+
+				if (!$duplicate) {
+					$existing[$cache_group][] = $config;
+					$changed = true;
+				}
+			}
+		}
+
+		if ($changed) {
+			update_option(self::CONNECTIONS_OPTION, $existing, false);
+			self::$connections_cache = $existing;
+		}
 	}
 
 	/**
-	 * Load relationships from JVB_CONTENT and JVB_TAXONOMY
+	 * Handle post save/update
 	 */
-	private static function loadRelationships(): void
+	public static function onPostSave(int $post_id, \WP_Post $post): void
 	{
-		if (self::$relationships_loaded) {
+		// Skip revisions and autosaves
+		if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
 			return;
 		}
 
-		// Load post type relationships
-		if (defined('JVB_CONTENT')) {
-			foreach (JVB_CONTENT as $slug => $config) {
-				$relationships = [];
+		$post_type = jvbNoBase($post->post_type);
 
-				// Author relationship
-				if (!($config['no_author'] ?? false)) {
-					$relationships['author'] = true;
-				}
+		// Invalidate post type cache
+		self::invalidateAll($post_type);
 
-				// Taxonomy relationships
-				if (!empty($config['taxonomies'])) {
-					$relationships['taxonomies'] = array_map('jvbNoBase', $config['taxonomies']);
-				}
-
-				// Custom relationships from config
-				if (!empty($config['cache_relationships'])) {
-					$relationships = array_merge($relationships, $config['cache_relationships']);
-				}
-
-				if (!empty($relationships)) {
-					self::registerRelationship($slug, $relationships);
-				}
-			}
-		}
-
-		// Load taxonomy relationships
-		if (defined('JVB_TAXONOMY')) {
-			foreach (JVB_TAXONOMY as $slug => $config) {
-				$relationships = [];
-
-				// Content type relationships
-				if (!empty($config['for_content'])) {
-					$relationships['content_types'] = array_map('jvbNoBase', $config['for_content']);
-				}
-
-				// Always include generic 'terms' cache
-				$relationships['related'] = ['terms'];
-
-				// Custom relationships from config
-				if (!empty($config['cache_relationships'])) {
-					$relationships = array_merge($relationships, $config['cache_relationships']);
-				}
-
-				if (!empty($relationships)) {
-					self::registerRelationship($slug, $relationships);
-				}
-			}
-		}
-
-		self::$relationships_loaded = true;
-
-		do_action('jvb_cache_relationships_loaded', self::$relationships);
+		// Invalidate specific post cache
+		self::invalidateAll($post_id);
+		// Clear WordPress core post object cache
+		clean_post_cache($post_id);
 	}
 
 	/**
-	 * Get relationships for a type (for debugging)
-	 *
-	 * @param string|null $type Specific type or null for all
-	 * @return array Relationships
+	 * Handle post deletion
 	 */
-	public static function getRelationships(?string $type = null): array
+	public static function onPostDelete(int $post_id): void
 	{
-		self::loadRelationships();
-
-		if ($type !== null) {
-			return self::$relationships[jvbNoBase($type)] ?? [];
-		}
-
-		return self::$relationships;
-	}
-
-	/**
-	 * Cascade invalidation to related types based on relationships
-	 *
-	 * @param string $type Primary type being invalidated
-	 * @param mixed $context Context with relationship data
-	 */
-	/**
-	 * Cascade invalidation to related types based on relationships
-	 */
-	private static function cascadeInvalidation(string $type, $context): void
-	{
-		self::loadRelationships();
-
-		$relationships = self::$relationships[$type] ?? [];
-		if (empty($relationships)) {
+		$post = get_post($post_id);
+		if (!$post) {
 			return;
 		}
 
-		$data = self::extractContext($context);
+		$post_type = jvbNoBase($post->post_type);
 
-		// Author relationship - SIMPLIFIED
-		if (!empty($relationships['author'])) {
-			$user_ids = self::extractUserIds($data, $relationships['author']);
-
-			foreach ($user_ids as $user_id) {
-				// Single clean call - handles content, profile, everything
-				self::invalidateAll("user_{$user_id}");
-			}
-		}
-
-		// Taxonomy relationships
-		if (!empty($relationships['taxonomies']) && !empty($data['ID'])) {
-			foreach ($relationships['taxonomies'] as $taxonomy) {
-				$taxonomy_full = jvbCheckBase($taxonomy);
-				$terms = wp_get_post_terms($data['ID'], $taxonomy_full, ['fields' => 'ids']);
-
-				if (!empty($terms) && !is_wp_error($terms)) {
-					self::updateTimestamp($taxonomy);
-					wp_cache_flush_group($taxonomy);
-				}
-			}
-		}
-
-		// Content type relationships (for taxonomies)
-		if (!empty($relationships['content_types'])) {
-			foreach ($relationships['content_types'] as $content_type) {
-				self::updateTimestamp($content_type);
-				wp_cache_flush_group($content_type);
-			}
-		}
-
-		// Generic related caches
-		if (!empty($relationships['related'])) {
-			foreach ($relationships['related'] as $related_type) {
-				self::updateTimestamp($related_type);
-				wp_cache_flush_group($related_type);
-			}
-		}
-
-		// Custom cascade function
-		if (!empty($relationships['cascade']) && is_callable($relationships['cascade'])) {
-			call_user_func($relationships['cascade'], $type, $data);
-		}
+		self::invalidateAll($post_type);
+		self::invalidateAll($post_id);
+		// Clear WordPress core post object cache
+		clean_post_cache($post_id);
 	}
 
 	/**
-	 * Extract user IDs from context based on relationship config
-	 * Supports multiple authors, contributors, etc.
-	 *
-	 * @param array $data Context data
-	 * @param mixed $config Author relationship config (bool or array)
-	 * @return array User IDs to invalidate
+	 * Handle term save/update
 	 */
-	private static function extractUserIds(array $data, $config): array
+	public static function onTermSave(int $term_id, int $tt_id, string $taxonomy): void
 	{
-		$user_ids = [];
+		// Clear WordPress core term cache
+		clean_term_cache($term_id, $taxonomy);
+		$taxonomy = jvbNoBase($taxonomy);
 
-		// Simple case: 'author' => true
-		if ($config === true) {
-			if (!empty($data['post_author'])) {
-				$user_ids[] = $data['post_author'];
-			}
-			return array_filter($user_ids);
-		}
+		// Invalidate taxonomy cache
+		self::invalidateAll($taxonomy);
 
-		// Advanced case: 'author' => ['post_author', 'contributors', 'linked_user']
-		if (is_array($config)) {
-			foreach ($config as $field) {
-				// Handle meta fields
-				if (str_starts_with($field, 'meta:') && !empty($data['ID'])) {
-					$meta_key = substr($field, 5);
-					$value = get_post_meta($data['ID'], BASE . $meta_key, true);
-
-					if (is_array($value)) {
-						$user_ids = array_merge($user_ids, $value);
-					} elseif ($value) {
-						$user_ids[] = $value;
-					}
-				}
-				// Handle direct data fields
-				elseif (!empty($data[$field])) {
-					if (is_array($data[$field])) {
-						$user_ids = array_merge($user_ids, $data[$field]);
-					} else {
-						$user_ids[] = $data[$field];
-					}
-				}
-			}
-		}
-
-		// Callable: 'author' => function($data) { return [...user_ids]; }
-		if (is_callable($config)) {
-			$result = call_user_func($config, $data);
-			if (is_array($result)) {
-				$user_ids = array_merge($user_ids, $result);
-			} elseif ($result) {
-				$user_ids[] = $result;
-			}
-		}
-
-		return array_unique(array_filter(array_map('intval', $user_ids)));
+		// Invalidate specific term cache
+		self::invalidateAll($term_id);
 	}
 
 	/**
-	 * Extract context data from various formats
-	 * Converts WP objects to arrays with relevant data
-	 *
-	 * @param mixed $context Post/Term object, array, or ID
-	 * @return array Normalized context data
+	 * Handle term deletion
 	 */
-	private static function extractContext($context): array
+	public static function onTermDelete(int $term_id, int $tt_id, string $taxonomy): void
 	{
-		if (is_array($context)) {
-			return $context;
-		}
+		// Clear WordPress core term cache
+		clean_term_cache($term_id, $taxonomy);
+		$taxonomy = jvbNoBase($taxonomy);
 
-		if ($context instanceof \WP_Post) {
-			return [
-				'ID' => $context->ID,
-				'post_author' => $context->post_author,
-				'post_type' => $context->post_type,
-				'post_status' => $context->post_status,
-			];
-		}
+		self::invalidateAll($taxonomy);
+		self::invalidateAll($term_id);
+	}
 
-		if ($context instanceof \WP_Term) {
-			return [
-				'term_id' => $context->term_id,
-				'taxonomy' => $context->taxonomy,
-				'parent' => $context->parent,
-			];
-		}
+	/**
+	 * Handle user update
+	 */
+	public static function onUserUpdate(int $user_id, ?\WP_User $old_user_data = null): void
+	{
+		// Invalidate user-specific cache
+		self::invalidateAll($user_id);
 
-		if (is_numeric($context)) {
-			$post = get_post($context);
-			if ($post) {
-				return self::extractContext($post);
+		// Invalidate user role caches if roles changed
+		if ($old_user_data) {
+			$user = get_userdata($user_id);
+			if ($user && $user->roles !== $old_user_data->roles) {
+				foreach (array_merge($user->roles, $old_user_data->roles) as $role) {
+					self::invalidateAll($role);
+				}
 			}
 		}
+		// Clear WordPress core user cache
+		clean_user_cache($user_id);
+	}
 
-		return [];
+	/**
+	 * Handle user deletion
+	 */
+	public static function onUserDelete(int $user_id): void
+	{
+		self::invalidateAll($user_id);
+		// Clear WordPress core user cache
+		clean_user_cache($user_id);
+	}
+
+	/**
+	 * Handle post meta updates
+	 */
+	public static function onPostMetaUpdate(int $meta_id, int $post_id, string $meta_key, mixed $meta_value): void
+	{
+		if (!str_starts_with($meta_key, BASE)) {
+			return;
+		}
+
+		$post = get_post($post_id);
+		if (!$post) {
+			return;
+		}
+
+		self::onPostSave($post_id, $post);
+	}
+	public static function onPostMetaDelete(array $meta_ids, int $post_id, string $meta_key, mixed $meta_value):void
+	{
+		if (!str_starts_with($meta_key, BASE)) {
+			return;
+		}
+
+		$post = get_post($post_id);
+		if (!$post) {
+			return;
+		}
+
+		self::onPostSave($post_id, $post);
+	}
+
+	/**
+	 * Handle term meta updates
+	 */
+	public static function onTermMetaUpdate(int $meta_id, int $term_id, string $meta_key, mixed $meta_value): void
+	{
+		if (!str_starts_with($meta_key, BASE)) {
+			return;
+		}
+
+		$term = get_term($term_id);
+		if (!$term || is_wp_error($term)) {
+			return;
+		}
+
+		self::onTermSave($term_id, $term->term_taxonomy_id, $term->taxonomy);
+	}
+
+	public static function onTermMetaDelete(array $meta_ids, int $term_id, string $meta_key, mixed $meta_value):void
+	{
+		if (!str_starts_with($meta_key, BASE)) {
+			return;
+		}
+
+		$term = get_term($term_id);
+		if (!$term || is_wp_error($term)) {
+			return;
+		}
+
+		self::onTermSave($term_id, $term->term_taxonomy_id, $term->taxonomy);
+	}
+
+		/**
+	 * Handle user meta updates
+	 */
+	public static function onUserMetaUpdate(int $meta_id, int $user_id, string $meta_key, mixed $meta_value): void
+	{
+		if (!str_starts_with($meta_key, BASE)) {
+			return;
+		}
+
+		$user = get_userdata($user_id);
+		if (!$user) {
+			return;
+		}
+
+		self::onUserUpdate($user_id, null);
+	}
+
+	public static function onUserMetaDelete(array $meta_ids, int $user_id, string $meta_key, mixed $meta_value):void
+	{
+		if (!str_starts_with($meta_key, BASE)) {
+			return;
+		}
+
+		$user = get_userdata($user_id);
+		if (!$user) {
+			return;
+		}
+
+		self::onUserUpdate($user_id, null);
 	}
 }
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index 10a8a4a..a7e6790 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -169,7 +169,7 @@
 
 		// For valid dashboard pages, check access permissions
 		if (!is_404()) {
-			$page = $this->getCurrentPage();
+			$page = $this->getCurrentPageSlug();
 
 			// Dashboard home is always accessible (if authenticated)
 			if ($page === '' || $page === 'dash') {
@@ -178,7 +178,7 @@
 
 			// Check if page exists in allowed pages
 			$allowedPages = $this->getUserAllowedPages();
-			if (!in_array($page, $allowedPages)) {
+			if (!array_key_exists($page, $allowedPages)) {
 				error_log("User not allowed to access page: {$page}");
 				$this->redirectToDashboard();
 				return;
@@ -193,15 +193,17 @@
     public function buildDashboard():void
     {
         $manageableContent = $this->getAllDashboardPages();
-        foreach ($manageableContent as $key => $slug) {
-			if ($slug === 'dash') {
+        foreach ($manageableContent as $slug => $page) {
+			if ($page === 'dash') {
 				continue;
 			}
 			$existing = get_page_by_path($slug, OBJECT, BASE.'dash');
 			if ($existing) {
 				continue;
 			}
-            $title = $this->getTitle($slug);
+            $title = $page;
+
+			$slug = $this->getSlug($slug, $page);
 
             $ID = wp_insert_post(array(
                 'post_title'    => $title,
@@ -305,11 +307,11 @@
         }
 
         // Get current page/section
-        $page = $this->getCurrentPage();
+        $page = $this->getCurrentPageTitle();
 		$config = $this->getConfig($page);
 		if(!empty($config)) {
 			add_filter('jvbLoadingIcon', function() use ($config) {
-				return jvbIcon($config['icon']);
+				return $config['icon'];
 			});
 		}
 		$integrationSlugs = array_map(function($name) {
@@ -515,7 +517,7 @@
 		wp_enqueue_script('jvb-content');
 		wp_enqueue_script('jvb-crud');
 
-		$page = $this->getCurrentPage();
+		$page = $this->getCurrentPageSlug();
 
             switch ($page) {
                 case 'notifications':
@@ -575,7 +577,20 @@
 			do_action('jvbDashScripts', $page);
     }
 
-   protected function getCurrentPage():string
+   protected function getCurrentPageTitle():string
+    {
+		if (is_post_type_archive(BASE.'dash')) {
+			return 'dash';
+		}
+
+        global $post;
+        if (!$post) {
+            return '';
+        }
+
+        return $post->post_title;
+    }
+   protected function getCurrentPageSlug():string
     {
 		if (is_post_type_archive(BASE.'dash')) {
 			return 'dash';
@@ -588,6 +603,29 @@
 
         return $post->post_name;
     }
+	protected function getIcon(string $slug, string $page):string
+	{
+		return $this->cache->remember('icon_'.sanitize_title($page), function() use ($slug, $page) {
+			$icon = sanitize_title($page);
+			if (!is_numeric($slug)) {
+				$config = Features::getConfig($slug);
+				if (array_key_exists('icon', $config)) {
+					$icon = $config['icon'];
+				}
+			}
+			return $icon;
+		});
+	}
+	protected function getSlug(string $slug, string $page):string
+	{
+		return $this->cache->remember('slug_'.sanitize_title($page), function() use ($slug, $page) {
+			if (!is_numeric($slug)) {
+				return $slug;
+			} else {
+				return sanitize_title($page);
+			}
+		});
+	}
 
     protected function renderHeader():void
     {
@@ -612,7 +650,7 @@
 			 <link rel="preconnect" href="<?= get_home_url()?>"/>
             <?php wp_head(); ?>
         </head>
-    <body class="dashboard<?= ' '.$this->getCurrentPage()?>">
+    <body class="dashboard<?= ' '.$this->getCurrentPageSlug()?>">
         <?php jvbAccessibility();?>
         <header>
             <?php
@@ -663,28 +701,28 @@
         	<?= TaxonomySelector::outputSelectorModal() ?>
             <nav class="dashboard-nav">
                 <?php
-                $current_page = $this->getCurrentPage();
+                $current_page = $this->getCurrentPageSlug();
                 $pages = $this->getUserAllowedPages()?:[];
 
                 echo '<ul>';
-                foreach ($pages as $page) {
-                    // Add data-page attribute for the navigator
-                    $active = ($current_page == $page) ? ' class="current"' : '';
-                    $current = ($current_page == $page) ? ' aria-current="page"' : '';
-					$config = $this->getConfig($page);
-					$icon = $config['icon']??$page;
-					$title = ucwords(str_replace('-', ' ', $page));
+                foreach ($pages as $slug => $page) {
+					$slug = $this->getSlug($slug, $page);
+					$icon = $this->getIcon($slug, $page);
+					// Add data-page attribute for the navigator
+                    $active = ($current_page == $slug) ? ' class="current"' : '';
+                    $current = ($current_page == $slug) ? ' aria-current="page"' : '';
 
-					$link = ($page === 'dash') ? '/'.$page : "/dash/$page";
+
+					$link = ($page === 'dash') ? '/'.$page : "/dash/$slug";
                     printf(
                         '<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
                         $active,
                         get_home_url(null, $link),
                         $current,
+                        $slug,
                         $page,
-                        $title,
-                        jvbIcon($icon, ['title'=> $title]),
-                        $title
+                        jvbIcon($icon, ['title'=> $page]),
+                        $page
                     );
                 }
 
@@ -695,7 +733,7 @@
 
 
  		<?php
-        do_action('jvbRenderDashboardSettings', $this->getCurrentPage());
+        do_action('jvbRenderDashboardSettings', $this->getCurrentPageSlug());
 		?>
         <?php wp_footer(); ?>
 
@@ -717,7 +755,6 @@
         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>';
 
@@ -729,15 +766,9 @@
             $title = $this->getTitle($page);
 
             $description = $this->getDescription($page);
-			$icon = $page;
-			if (!is_numeric($slug)) {
-				$config = Features::getConfig($slug);
-				if (array_key_exists('icon', $config)) {
-					$icon = $config['icon'];
-				}
-			} else {
-				$slug = sanitize_title($page);
-			}
+
+			$slug = $this->getSlug($slug, $page);
+			$icon = $this->getIcon($slug, $page);
             if ($title !== '') {
                 echo '<li><p><a href="'.get_home_url(null, '/dash/'.$slug.'/').'"
                     data-page="'.$slug.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>';
@@ -1104,52 +1135,52 @@
 
 			// Add feature-dependent pages (non-config)
 			if (Features::forSite()->has('referrals')) {
-				$pages[] = 'referrals';
+				$pages[] = 'Referrals';
 			}
 
 			if (Features::forMembership()->has('can_invite')) {
-				$pages[] = 'invites';
+				$pages[] = 'Invites';
 			}
 
 			if (Features::forMembership()->has('term_approval')) {
-				$pages[] = 'approvals';
+				$pages[] = 'Approvals';
 			}
 
 			if (Features::forMembership()->has('forum')) {
-				$pages[] = 'news';
+				$pages[] = 'News';
 			}
 
 			if (Features::forMembership()->has('member_content')) {
-				$pages[] = 'metrics';
+				$pages[] = 'Metrics';
 			}
 
 			if (Features::forSite()->has('favourites')) {
-				$pages[] = 'favourites';
+				$pages[] = 'Favourites';
 			}
 
 			if (Features::anyContentHas('karma') || Features::anyTaxonomyHas('karma') || Features::anyUserHas('karma')) {
-				$pages[] = 'karmic-score';
+				$pages[] = 'Karmic Score';
 			}
 
 			if (Features::forSite()->has('notifications')) {
-				$pages[] = 'notifications';
+				$pages[] = 'Notifications';
 			}
 
 			if (Features::forSite()->has('support')) {
-				$pages[] = 'support';
+				$pages[] = 'Support';
 			}
 
 			if (Features::hasAnyIntegration()) {
-				$pages[] = 'integrations';
+				$pages[] = 'Integrations';
 			}
 
 			// Add all content types (with config keys)
 			foreach (JVB_CONTENT as $slug => $config) {
-				$pages[$slug] = sanitize_title($config['plural']);
+				$pages[$slug] = $config['plural'];
 			}
 
 			foreach (JVB_TAXONOMY as $slug=>$config) {
-				$pages[$slug] = sanitize_title($config['plural']);
+				$pages[$slug] = $config['plural'];
 			}
 
 			// Allow filtering
diff --git a/inc/managers/IconsManager.php b/inc/managers/IconsManager.php
index d3710d0..8278763 100644
--- a/inc/managers/IconsManager.php
+++ b/inc/managers/IconsManager.php
@@ -17,6 +17,7 @@
 	protected array $customIcons = [];
 	protected array $usedIcons = [];
 	protected array $map = [];
+	protected const MAX_VERSIONS = 5;
 
 	/**
 	 * Get singleton instance
@@ -31,6 +32,7 @@
 	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';
@@ -44,36 +46,35 @@
 		]);
 
 
-		$this->usedIcons = get_option(BASE.'used_icons', []);
+		$this->usedIcons = get_option(BASE.'usedIcons', []);
 		$this->includeIcons();
+		// Track custom icons for CSS generation
+		$this->trackCustomIcons();
 		// Register hooks only once
 		$this->registerHooks();
 	}
 
 	/**
-	 * Include icons via filter (for JS usage, etc.)
+	 * Ensure custom icons are tracked for CSS generation
 	 */
-	protected function includeIcons():array
+	protected function trackCustomIcons(): void
 	{
-		$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);
+		if (empty($this->customIcons)) {
+			return;
 		}
 
-		return $icons;
+		foreach ($this->customIcons as $name => $path) {
+			$this->trackIconUsage($name, $this->style);
+		}
 	}
 
-	protected function addIncludeIcons(array $add = []) {
+	/**
+	 * Include icons via filter (for JS usage, etc.)
+	 */
+	protected function includeIcons():void
+	{
 		$icons = get_option(BASE.'includeIcons');
+
 		if (!$icons) {
 			$icons = [
 				'check-circle',
@@ -120,6 +121,7 @@
 				'file-txt',
 				'file-xls',
 			];
+
 			$check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
 			foreach ($check as $constant) {
 				foreach ($constant as $key => $value) {
@@ -129,20 +131,77 @@
 				}
 			}
 			$icons = apply_filters('jvbIncludeIcons', $icons);
-
+			$icons = $this->maybePrefixIcons($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);
+
+		// Ensure icons are in the correct format (handle legacy data)
+		if (!$this->isIconsArrayPrefixed($icons)) {
+			$icons = $this->maybePrefixIcons($icons);
+			update_option(BASE.'includeIcons', $icons);
 		}
-		return $icons;
+
+		$additional = apply_filters('jvbIncludeIcons', []);
+		if (!empty($additional)) {
+			$additional = $this->maybePrefixIcons($additional);
+			$merged = $this->mergeUsedIcons($icons, $additional);
+
+			if ($icons != $merged) {
+				update_option(BASE.'includeIcons', $merged);
+				$icons = $merged;
+			}
+		}
+
+		foreach ($icons as $style => $theIcons) {
+			foreach($theIcons as $icon) {
+				$this->trackIconUsage($icon, $style);
+			}
+		}
+	}
+
+	/**
+	 * Check if icons array is in the prefixed format [style => [icons]]
+	 */
+	protected function isIconsArrayPrefixed(array $icons): bool
+	{
+		if (empty($icons)) {
+			return true;
+		}
+
+		// Check if first key is a valid style name
+		$first_key = array_key_first($icons);
+		if (!in_array($first_key, $this->styles)) {
+			return false;
+		}
+
+		// Check if first value is an array
+		return is_array($icons[$first_key]);
+	}
+
+	protected function maybePrefixIcons(array $icons):array
+	{
+		$out = [];
+		foreach ($icons as $icon) {
+			if (is_array($icon) && array_key_exists('style', $icon)) {
+				if (!array_key_exists($icon['style'], $out)) {
+					$out[$icon['style']] = [];
+				}
+				if (!in_array($icon['name'], $out[$icon['style']])) {
+					$out[$icon['style']][] = $icon['name'];
+				}
+			} elseif(is_array($icon)) {
+				$icon = $icon['name'];
+			}
+			if (!is_array($icon)) {
+				if (!array_key_exists($this->style, $out)) {
+					$out[$this->style] = [];
+				}
+				if (!in_array($icon, $out[$this->style])){
+					$out[$this->style][] = $icon;
+				}
+			}
+		}
+		return $out;
 	}
 
 	protected function addMap():void
@@ -173,7 +232,8 @@
 	 */
 	protected function registerHooks(): void
 	{
-		add_action('init', [$this, 'checkCSS']);
+		add_action('init', [$this, 'includeIcons'], 1);
+		add_action('init', [$this, 'checkCSS'], 10);
 		add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
 		add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']);
 	}
@@ -192,7 +252,15 @@
 	{
 		error_log('[IconsManager]:regenerateCSS');
 		$css = $this->generateIconCSS();
-		$css_path = JVB_DIR.'/assets/css/icons.css';
+		$css_path = JVB_CHILD_DIR.'/assets/css/';
+		if (!file_exists($css_path)) {
+			wp_mkdir_p($css_path);
+		}
+		$css_path .= '/icons.css';
+
+
+		// Archive current version before overwriting
+		$this->archiveCurrentVersion($css);
 
 		if (file_put_contents($css_path, $css) !== false) {
 			CacheManager::updateTimestamp('icons');
@@ -228,24 +296,23 @@
 	 */
 	public function getIcon(string $name, array $options = []): string
 	{
-		if (!array_key_exists('style', $options)) {
-			$options['style'] = $this->style;
-		}
+		$style = 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'])) {
+		if (!$this->iconExists($name, $style)) {
 			error_log('[IconsManager] Icon not found: ' . $name);
 			return '';
 		}
 
-		$style = $options['style'] ?? $this->style;
+
 
 		// Track icon usage
 		$this->trackIconUsage($name, $style);
 
+		$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
 		// Build classes
-		$classes = ['icon', 'icon-' . $name];
+		$classes = ['icon', 'icon-' . $name.$styleClass];
 		if (!empty($options['class'])) {
 			$classes[] = $options['class'];
 		}
@@ -264,13 +331,24 @@
 	 */
 	protected function trackIconUsage(string $name, string $style): void
 	{
+		$needsUpdate = false;
+
 		if (!array_key_exists($style, $this->usedIcons)) {
 			$this->usedIcons[$style] = [];
+			$needsUpdate = true;
 		}
 
 		if (!in_array($name, $this->usedIcons[$style])) {
 			$this->usedIcons[$style][] = $name;
-			update_option(BASE.'used_icons', $this->usedIcons);
+			$needsUpdate = true;
+		}
+
+		if ($needsUpdate) {
+			// Merge with existing option to never lose icons
+			$existing = get_option(BASE.'usedIcons', []);
+			$merged = $this->mergeUsedIcons($existing, $this->usedIcons);
+			update_option(BASE.'usedIcons', $merged);
+
 			// Flag for regeneration on next init
 			update_option(BASE.'icons_needs_update', true);
 
@@ -357,7 +435,7 @@
 
 		wp_enqueue_style(
 			'jvb-icons',
-			JVB_URL.'assets/css/icons.css',
+			JVB_CHILD_URL.'assets/css/icons.css',
 			[],
 			$timestamp
 		);
@@ -369,10 +447,11 @@
 	protected function generateIconCSS(): string
 	{
 		$css = '';
+		$this->mergeUsedIcons();
+
 		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}{";
@@ -384,6 +463,34 @@
 		return $this->minifyCss($css);
 	}
 
+	protected function mergeUsedIcons(array|bool $oldIcons = true, array|bool $newIcons = true):array
+	{
+		$set = false;
+		if ($oldIcons === true) {
+			$oldIcons = $this->usedIcons;
+			$set = true;
+		}
+		if ($newIcons === true) {
+			$history = $this->getVersionHistory();
+			$newIcons = (count($history) > 0) ? $history[0]['iconList'] : [];
+		}
+		foreach ($newIcons as $style => $icons) {
+			if (!isset($oldIcons[$style])) {
+				//Style  doesn't exist in previous set, add the whole thing
+				$oldIcons[$style] = $icons;
+			} else {
+				$oldIcons[$style] = array_unique(
+					array_merge($oldIcons[$style], $icons)
+				);
+			}
+		}
+		if ($set) {
+			$this->usedIcons = $oldIcons;
+			update_option(BASE.'usedIcons', $oldIcons);
+		}
+		return $oldIcons;
+	}
+
 	protected function minifyCSS(string $css): string
 	{
 		// Remove comments
@@ -428,7 +535,123 @@
 	public function clearIconCache(): void
 	{
 		delete_option(BASE . 'icon_usage_list'); // Clear DB option
+		delete_option(BASE.'usedIcons');
+		delete_option(BASE.'includeIcons');
+		delete_option(BASE.'iconMap');
 		$this->cache->delete('icon_styles_css');
 		CacheManager::updateTimestamp('icons');
 	}
+
+	protected function archiveCurrentVersion(string $css): void
+	{
+		$history = $this->getVersionHistory();
+
+		$icon_count = 0;
+		foreach ($this->usedIcons as $style => $icons) {
+			$icon_count += count($icons);
+		}
+
+		$newEntry = [
+			'css' => $css,
+			'iconList' => $this->usedIcons,
+			'timestamp' => time(),
+			'icon_count' => $icon_count,
+			'size' => strlen($css),
+			'size_formatted' => size_format(strlen($css), 2)
+		];
+
+		array_unshift($history, $newEntry);
+
+		if (count($history) > self::MAX_VERSIONS) {
+			$history = array_slice($history, 0, self::MAX_VERSIONS);
+		}
+
+		update_option(BASE.'icon_css_history', $history);
+	}
+
+	public function getVersionHistory(): array
+	{
+		return get_option(BASE.'icon_css_history', []);
+	}
+
+	public function restoreVersion(int $timestamp): bool
+	{
+		$history = $this->getVersionHistory();
+
+		foreach ($history as $entry) {
+			if ($entry['timestamp'] === $timestamp) {
+				$css_path = JVB_DIR . '/assets/css/icons.css';
+
+				// Archive current before restoring
+				$current_css = file_get_contents($css_path);
+				if ($current_css !== false) {
+					$this->archiveCurrentVersion($current_css);
+				}
+
+				// Restore the version
+				if (file_put_contents($css_path, $entry['css']) !== false) {
+					$this->usedIcons = $entry['iconList'];
+					update_option(BASE.'usedIcons', $this->usedIcons);
+					CacheManager::updateTimestamp('icons');
+					return true;
+				}
+
+				return false;
+			}
+		}
+
+		error_log("[IconsManager] Version {$timestamp} not found in history");
+		return false;
+	}
+
+	public function forceRefresh(): void
+	{
+		$this->clearIconCache();
+		update_option(BASE.'icons_needs_update', true);
+		CacheManager::updateTimestamp('icons');
+	}
+
+	public function mergeVersions(array $timestamps): bool
+	{
+		if (empty($timestamps)) {
+			return false;
+		}
+
+		$history = get_option(BASE.'icon_css_history', []);
+		$merged_icons = [];
+		// Collect icons from selected versions
+		foreach ($history as $entry) {
+			if (in_array($entry['timestamp'], $timestamps)) {
+				foreach ($entry['iconList'] as $style => $icons) {
+					if (!isset($merged_icons[$style])) {
+						$merged_icons[$style] = [];
+					}
+					// Merge and keep unique
+					$merged_icons[$style] = array_unique(
+						array_merge($merged_icons[$style], $icons)
+					);
+				}
+			}
+		}
+
+		if (empty($merged_icons)) {
+			error_log('[IconsManager] No icons found in selected versions');
+			return false;
+		}
+
+		// Archive current version
+		$current_css = file_get_contents(JVB_DIR . '/assets/css/icons.css');
+		if ($current_css !== false) {
+			$this->archiveCurrentVersion($current_css);
+		}
+
+		// Update used icons and regenerate
+		$this->usedIcons = $merged_icons;
+		update_option(BASE.'usedIcons', $this->usedIcons);
+
+		// Force regeneration
+		$this->regenerateCSS();
+
+		return true;
+	}
 }
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 7ded3ba..525a53f 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -45,7 +45,7 @@
 	{
 		$this->siteFeatures = Features::forSite();
 		$this->emailManager = new EmailManager();
-		$this->rateLimiter = new AjaxRateLimiter();
+
 
 		$this->cache = CacheManager::for('login');
 
@@ -664,10 +664,7 @@
 	/*************************************************************************
  	*	SECURITY & VALIDATION
 	*************************************************************************/
-	protected function checkAjaxRateLimit(string $action): bool
-	{
-		return $this->rateLimiter->checkLimit($action);
-	}
+
 	protected function checkRequestId(): bool
 	{
 		$request_id = $_POST['request_id'] ?? '';
@@ -841,8 +838,7 @@
 		$action = $this->getAction();
 		ob_start();
 		?>
-		<script type="text/javascript">
-		window.checkedEmails = new Set();
+
 		document.addEventListener('DOMContentLoaded', () => {
 			const form = document.querySelector('.login form');
 			if (!form) return;
@@ -863,33 +859,34 @@
 			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');
+			let realFormData = data.fullData;
+			const { formId, config, data: formData } = data;
 
-				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 form = config.element;
+
+			const submit = form.querySelector('[type=submit]');
+			let oldText = submit.textContent;
+
+
+			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(realFormData)
+				});
 
 					const result = await response.json();
 
@@ -927,68 +924,11 @@
 					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();
 
diff --git a/inc/managers/OperationQueue.php b/inc/managers/OperationQueue.php
index 07f2104..4e07c6f 100644
--- a/inc/managers/OperationQueue.php
+++ b/inc/managers/OperationQueue.php
@@ -599,9 +599,7 @@
 	}
 
 	protected function updateLastModified(int $user_id) {
-		if (Features::forSite()->has('dashboard')) {
-			JVB()->routes('queue')->updateUserQueueTimestamp($user_id);
-		}
+		CacheManager::updateTimestamp("user_{$user_id}");
 	}
 
 	protected function deepMerge(array $existing, array $new): array
@@ -1559,8 +1557,8 @@
 
     protected function updateUserQueueTimestamp(int $user_id)
     {
-        $key = "{$user_id}_queue_timestamp";
-		$this->cache->set($key, time());
+
+		CacheManager::updateTimestamp("user_{$user_id}");
     }
 
 	/**
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 21895b9..953450f 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -624,7 +624,6 @@
 
 	private function renderRepeaterField(string $name, mixed $value, array $field):void
 	{
-		error_log('Rendering Repeater Field!');
 		$values = is_array($value) ? $value : array();
 
 		$conditional = $this->handleConditionalField($field);
@@ -722,9 +721,11 @@
 	protected function renderGroupField(string $name, mixed $value, array $field): void
 	{
 		if (!array_key_exists('fields', $field) || empty($field['fields'])) {
+			error_log('No fields to render');
 			return;
 		}
 
+
 		$values = is_array($value) ? $value : [];
 		$original = $name;
 
@@ -744,14 +745,15 @@
 		$conditional = $this->handleConditionalField($field);
 		$validationAttrs = $this->buildValidationAttributes($field);
 		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
-
+		$fieldset = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'details' : 'fieldset';
+		$legend = (array_key_exists('wrap', $field) && $field['wrap'] === 'details') ? 'summary' : 'legend';
 		?>
-		<fieldset class="field group <?= esc_attr($name) ?>"
+		<<?= $fieldset?> class="field group <?= esc_attr($name) ?>"
 			<?= $conditional ?>
 				  data-field="<?= esc_attr($name) ?>"
 			<?= $validationAttrs ?>
 			<?= $describedBy ?>>
-			<legend><?= esc_html($field['label']) ?></legend>
+			<<?=$legend?>><?= esc_html($field['label']) ?></<?=$legend?>>
 
 			<?php $this->renderHintAndDescription($field, $name); ?>
 
@@ -760,7 +762,7 @@
 			</div>
 
 			<span class="validation-message" hidden role="alert"></span>
-		</fieldset>
+		</<?= $fieldset?>>
 		<?php
 	}
 
@@ -1072,7 +1074,7 @@
 					<?php jvbRenderProgressBar('',true) ?>
 					<input type="checkbox" class="upload-select" name="select-item" id="select-item<?=$addID?>">
 					<label for="select-item<?=$addID?>" aria-label="Select image">
-						<?= ($attachment) ? $attachment : '<img>
+						<?= ($attachment) ?: '<img>
 						<video></video>
 						<span></span>' ?>
 					</label>
diff --git a/inc/meta/MetaManager.php b/inc/meta/MetaManager.php
index f8081aa..a1d7dbc 100644
--- a/inc/meta/MetaManager.php
+++ b/inc/meta/MetaManager.php
@@ -333,11 +333,7 @@
 					}
 			}
 			if ($field_config['type'] == 'taxonomy' && (!array_key_exists('taxonomy_type', $field_config))) {
-				error_log('Attempting to set taxonomies: ' . print_r($this->object_id, true));
-				error_log('Sanitized data: ' . print_r($sanitized, true));
-				error_log('Taxonomy: ' . print_r($field_config['taxonomy'], true));
 				$set = wp_set_post_terms($this->object_id, $sanitized, jvbCheckBase($field_config['taxonomy']), false);
-				error_log('Set post terms: ' . print_r($set, true));
 			}
 			if ($field_config['type'] === 'location' && empty($sanitized)) {
 				$this->addMeta('has_map', false);
@@ -1020,7 +1016,8 @@
 					}
 
 					if ($field_config['type'] === 'taxonomy' && !array_key_exists('taxonomy_type', $field_config)){
-						$set = wp_set_post_terms($this->object_id, $sanitized, jvbCheckBase($field_config['taxonomy']), false);
+						$term_ids = array_map('intval', explode(',', trim($sanitized)));
+						$set = wp_set_post_terms($this->object_id, $term_ids, jvbCheckBase($field_config['taxonomy']), false);
 					}
 
 					if ($field_config['type'] === 'location' && empty($sanitized)) {
diff --git a/inc/meta/MetaValidator.php b/inc/meta/MetaValidator.php
index 1a01611..40b1725 100644
--- a/inc/meta/MetaValidator.php
+++ b/inc/meta/MetaValidator.php
@@ -43,7 +43,6 @@
 
     protected function validateNumber(float $value, array $field_config):bool|WP_Error
     {
-        error_log('Number: '.print_r($value, true));
         if (empty($value)) {
             if (!empty($field['required'])) {
                 return new WP_Error('required_field', 'This field is required');
diff --git a/inc/registry/PostTypeRegistrar.php b/inc/registry/PostTypeRegistrar.php
index 1d6c2f4..cd753c4 100644
--- a/inc/registry/PostTypeRegistrar.php
+++ b/inc/registry/PostTypeRegistrar.php
@@ -130,45 +130,6 @@
 			add_action('post_type_link', [$this, 'rewriteTaxonomySingle'], 15, 2);
 			add_filter('post_type_archive_link', [$this, 'rewriteTaxonomyArchive'], 15, 2);
 		}
-
-		$postType = $this->post_type;
-		add_action("save_post_{$this->post_type}", function($post_id, $post, $update) use ($postType) {
-			if (jvbNoSaveIt($post_id, $post)) {
-				return;
-			}
-			$this->invalidatePostCache($postType, $post, $update ? 'update' : 'create');
-		}, 10, 3);
-
-		add_action("delete_post", function($post_id, $post) use ($postType) {
-			$post = get_post($post_id);
-			if ($post && $post->post_type === $postType) {
-				$this->invalidatePostCache($postType, $post, 'delete');
-			}
-		}, 10, 2);
-
-		add_action("transition_post_status", function($new_status, $old_status, $post) use ($postType) {
-			if ($post->post_type === $postType && $new_status !== $old_status) {
-				$this->invalidatePostCache($postType, $post, 'status_change');
-			}
-		}, 10, 3);
-	}
-
-	protected function invalidatePostCache(string $type, $post, string $action) {
-		error_log('Clearing Cache for '.print_r($type, true));
-
-		$cache = CacheManager::for(jvbNoBase($type))->invalidate();
-
-		// Clear related caches (taxonomies attached to this post)
-		$taxonomies = get_object_taxonomies($post->post_type);
-		foreach ($taxonomies as $taxonomy) {
-			$terms = wp_get_post_terms($post->ID, $taxonomy, ['fields' => 'ids']);
-			if (!empty($terms)) {
-				CacheManager::for(jvbNoBase($taxonomy))->invalidate();
-			}
-		}
-
-		// Trigger custom action for additional handling
-		do_action("jvb_cache_invalidated_{$type}", $post, $action);
 	}
 
 	public function hideFromPublic(bool $is_viewable): bool
diff --git a/inc/registry/TaxonomyRegistrar.php b/inc/registry/TaxonomyRegistrar.php
index e31fc5a..489dd12 100644
--- a/inc/registry/TaxonomyRegistrar.php
+++ b/inc/registry/TaxonomyRegistrar.php
@@ -55,12 +55,32 @@
 		$post_types = array_map(fn($type) => BASE . $type, $this->config['for_content'] ?? []);
 		register_taxonomy($this->taxonomy, $post_types, $args);
 
+		$this->maybeAddRewriteRule($args['rewrite']);
 		if (!empty($this->fields)) {
 			$meta_registry = new MetaRegistry($this->fields, $this->slug, 'term');
 			$meta_registry->registerMetaFields();
 		}
 	}
 
+	/**
+	 * Add custom rewrite rule for hierarchical taxonomy slugs (e.g., post-type/by/taxonomy-slug)
+	 */
+	protected function maybeAddRewriteRule(array $rewrite_config): void
+	{
+		$slug = $rewrite_config['slug'] ?? $this->slug;
+
+		// Only add custom rule if slug contains slashes (hierarchical path)
+		if (!str_contains($slug, '/')) {
+			return;
+		}
+
+		add_rewrite_rule(
+			"^{$slug}/([^/]+)/?$",
+			'index.php?' . $this->taxonomy . '=$matches[1]',
+			'top'
+		);
+	}
+
     private function buildLabels(string $singular, string $plural): array
     {
         return [
diff --git a/inc/rest/RegisterRoutes.php b/inc/rest/RegisterRoutes.php
index 1022b56..04c00c9 100644
--- a/inc/rest/RegisterRoutes.php
+++ b/inc/rest/RegisterRoutes.php
@@ -106,7 +106,6 @@
 	public function handlePOST(WP_REST_Request $request):WP_REST_Response
 	{
 		$data = $request->get_params();
-		error_log('Handling update with this data: '.print_r($data, true));
 		$user = $data['user'];
 		if (!$this->checkUser($user) || !$this->userCheck($user)) {
 			return new WP_REST_Response([
@@ -114,7 +113,6 @@
 				'message'   => 'Looks like you may not be who you say you are...'
 			]);
 		}
-		error_log('User passed');
 		if ($this->route === 'shop' && !$this->checkTerm([
 			'term_id'	=> $data['shop'],
 			'taxonomy'	=> $this->route
@@ -124,7 +122,6 @@
 				'message'   => 'This shop doesn\'t exist?'
 			]);
 		}
-		error_log('Processing...');
 		$queue = JVB()->queue();
 		unset($data['user']);
 		$operationID = $data['id'];
@@ -156,7 +153,6 @@
 	}
 	protected function handleUpdateOperation(int $userID, array $data):WP_Error|array
 	{
-		error_log('Handling update Operation with route: '.print_r($this->route, true));
 		if ($this->route === 'options') {
 			if (!user_can($userID, 'manage_options')) {
 				return [
@@ -182,7 +178,6 @@
 			return array_key_exists($v, $this->config['fields']??[]);
 		}, ARRAY_FILTER_USE_BOTH);
 
-		error_log('Allowed Result: '.print_r($allowed, true));
 
 		foreach ($allowed as $name => $value) {
 			if (empty($value)) {
diff --git a/inc/rest/RestRouteManager.php b/inc/rest/RestRouteManager.php
index 96ccfb7..3d9b2a4 100644
--- a/inc/rest/RestRouteManager.php
+++ b/inc/rest/RestRouteManager.php
@@ -6,6 +6,7 @@
 use JVBase\managers\OperationQueue;
 use JVBase\managers\CacheManager;
 use JVBase\managers\NotificationManager;
+use JVBase\utility\Features;
 use WP_REST_Request;
 use WP_Error;
 use Exception;
@@ -36,7 +37,7 @@
     protected NotificationManager $notifications;
     protected string $cache_name ='';
     protected int $cache_ttl = 3600; //1 hour default
-
+	protected array $response_headers = [];
 
     // Error code constants for consistency
     const ERROR_MISSING_PARAMS = 'missing_parameters';
@@ -906,6 +907,19 @@
 		$lock_key = 'op_lock_' . md5($operation_key);
 		delete_transient($lock_key);
 	}
+
+	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);
+	}
 }
 //
 //Simple example:
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index dfa9cb6..e1b457a 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -88,17 +88,44 @@
 		$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){
+		$this->timelineSharedFields = $this->getTimelineSharedFields($content);
+		array_unshift($this->timelineSharedFields, 'post_thumbnail');
+		array_unshift($this->timelineSharedFields, 'post_title');
+		array_unshift($this->timelineSharedFields, 'post_status');
+
+		$this->timelineUniqueFields = $this->getTimelineUniqueFields($content);
+	}
+	public function getTimelineUniqueFields(string $content):array
+	{
+		$content = jvbNoBase($content);
+		if (!Features::forContent($content)->has('is_timeline')){
+			return [];
+		}
+		$config = Features::getConfig($content);
+		$allFields = $config['fields'];
+
+		return array_keys(array_filter($allFields, function ($field) {
+			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
 				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) {
+	public function getTimelineSharedFields(string $content):array
+	{
+		$content = jvbNoBase($content);
+		if (!Features::forContent($content)->has('is_timeline')){
+			return [];
+		}
+		$config = Features::getConfig($content);
+		if (!$config || empty($config)) {
+			return [];
+		}
+		$allFields = $config['fields']??[];
+
+		return array_keys(array_filter($allFields, function ($field) {
+			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
 				return true;
 			}
 			return false;
@@ -198,9 +225,6 @@
     public function handleContentRequest(WP_REST_Request $request):WP_REST_Response
     {
         $params = $request->get_params();
-		error_log('handleContentRequest params: '.print_r($params, true));
-
-		error_log('Fetching content. Params: '.print_r($params, true));
         $user_id = $params['user'];
         if (!$this->userCheck($user_id)) {
             return new WP_REST_Response([
@@ -251,10 +275,6 @@
 			$args['s'] = sanitize_text_field($params['search']);
 		}
 
-
-
-		error_log('Content Routes final args: '.print_r($args, true));
-
         $key = $this->cache->generateKey($args);
 		// Check HTTP cache headers with the specific content type
 		$content_type = $params['content'] ?? $params['type'];
@@ -339,7 +359,7 @@
         $results = [];
 
         foreach ($posts as $ID => $post_data) {
-			if (Features::forContent($post_data['content'])->has('is_timeline')) {
+			if (Features::forContent($post_data['content'])->has('is_timeline') && array_key_exists('timeline', $post_data)) {
 				$results[$ID] =$this->processTimelinePost($ID, $post_data);
 				continue;
 			}
@@ -416,7 +436,6 @@
 				error_log('Allowed Fields: '.print_r($allowedFields, true));
 				$meta = new MetaManager($ID, 'post');
 				$success = $meta->setAll($allowedFields);
-				error_log('Should be set?');
 				$results[$ID] = [
 					'success'	=> $success
 				];
@@ -458,62 +477,138 @@
 			return ['success' => false, 'message' => 'No permission'];
 		}
 
+		$ignore = ['content', 'user'];
 		$this->fields = jvbGetFields($post_data['content']);
 		$this->initTimelineFields($post_data['content']);
-		error_log('Received Data: '.print_r($post_data, true));
 
-		// First, process the main fields that will apply to all posts
-		$sharedData = array_filter($post_data, function ($key) {
-			return in_array($key, $this->timelineSharedFields);
+		// Get parent post details
+		$parent_post = get_post($parent_id);
+		$parent_title = $parent_post->post_title;
+		$parent_is_published = ($parent_post->post_status === 'publish');
+
+		// Extract shared data from top level (excluding post_thumbnail which is unique per post)
+		$sharedData = array_filter($post_data, function ($key) use ($ignore) {
+			return in_array($key, $this->timelineSharedFields)
+				&& !in_array($key, $ignore)
+				&& $key !== 'post_thumbnail';
 		}, ARRAY_FILTER_USE_KEY);
 
-		//Next, process any individual posts, including any menu order changes
+		// If no shared post_title at top level, extract from first timeline entry
+		if (!isset($sharedData['post_title']) && isset($post_data['timeline'][0]['post_title'])) {
+			$sharedData['post_title'] = $post_data['timeline'][0]['post_title'];
+		}
+		$clearParent = false;
 		if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
-			$sharedTaxonomies = $sharedData;
-			unset($sharedTaxonomies['post_title']);
+			// Remove post_title and post_thumbnail from shared taxonomies
+			$sharedTaxonomies = array_filter($sharedData, function($key) {
+				return $key !== 'post_title' && $key !== 'post_thumbnail';
+			}, ARRAY_FILTER_USE_KEY);
 
-			//Ensure the parent post exists and is still first in the array
+			// 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'
+					'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);
 			}
+
+			if ($index !== 0) {
+				$new_parent_id = $post_data['timeline'][0]['id'];
+
+				if (is_numeric($new_parent_id) && (int)$new_parent_id > 0) {
+					$new_parent_id = (int)$new_parent_id;
+					wp_update_post([
+						'ID' => $new_parent_id,
+						'post_parent' => 0
+					]);
+
+					wp_update_post([
+						'ID' => $parent_id,
+						'post_parent' => $new_parent_id
+					]);
+
+					$existing_children = get_children([
+						'post_parent' => $parent_id,
+						'fields' => 'ids'
+					]);
+
+					foreach ($existing_children as $child_id) {
+						if ($child_id !== $new_parent_id) {
+							wp_update_post([
+								'ID' => $child_id,
+								'post_parent' => $new_parent_id
+							]);
+						}
+					}
+
+					// Update parent references
+					$parent_id = $new_parent_id;
+					$parent_post = get_post($parent_id);
+					$parent_title = $parent_post->post_title;
+					$parent_is_published = ($parent_post->post_status === 'publish');
+				} else {
+					$item = $post_data['timeline'][$index];
+					unset($post_data['timeline'][$index]);
+					array_unshift($post_data['timeline'], $item);
+				}
+			}
+
 			$errors = [];
 			$success = [];
-			// Get existing children to track deletions
 			$existing_children = get_children([
 				'post_parent' => $parent_id,
-				'post_type' => jvbCheckBase($post_data['content']),
-				'fields' => 'ids'
+				'orderby' => 'menu_order',
+				'post_status' => ['publish', 'draft'],
+				'fields'=> 'ids'
 			]);
-			//Iterate through the timeline posts
+
+			$prevDate = null;
+
 			foreach($post_data['timeline'] as $order => $timeline) {
-				$allowedFields = array_filter($timeline, function($key) {
-					return in_array($key, $this->timelineUniqueFields);
+				// Get unique fields for this specific timeline entry
+				$allowedFields = array_filter($timeline, function($key) use ($ignore) {
+					return in_array($key, $this->timelineUniqueFields) && !in_array($key, $ignore);
 				}, ARRAY_FILTER_USE_KEY);
 
-				$allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order;
-				$allowedFields = array_merge($allowedFields, $sharedTaxonomies);
+				// Determine the post title
+				$is_parent = ((int)$timeline['id'] === $parent_id);
+				$provided_title = $timeline['post_title'] ?? '';
+				$auto_generated_pattern = '/^.+Treatment #?\d+$/'; // Matches "Title - Treatment #1" or "Title - Treatment 1"
+
+				if ($is_parent) {
+					// Parent keeps its own title or uses shared title
+					$allowedFields['post_title'] = $provided_title ?: ($sharedData['post_title'] ?? $parent_title);
+				} else {
+					// For child posts, auto-generate if:
+					// 1. No title provided, OR
+					// 2. Title matches auto-generated pattern (meaning it wasn't customized)
+					if (empty($provided_title) || preg_match($auto_generated_pattern, $provided_title)) {
+						$allowedFields['post_title'] = 'Treatment ' . $order;
+					} else {
+						// Keep custom title
+						$allowedFields['post_title'] = $provided_title;
+					}
+				}
+
+				// Merge with shared taxonomies AFTER setting unique fields
+				$allowedFields = array_merge($sharedTaxonomies, $allowedFields);
+
+				// Handle post creation if needed
 				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
+						'post_author' => $this->user_id,
+						'post_type' => jvbCheckBase($post_data['content']),
+						'post_title' => $allowedFields['post_title'],
+						'post_parent' => $parent_id,
+						'menu_order' => $order,
+						'post_status' => $parent_is_published ? 'publish' : 'draft'
 					]);
 					if (!$newChild || is_wp_error($newChild)) {
 						$errors[] = [
-							'message'	=> 'Could not create child post',
-							'data'		=> $timeline
+							'message' => 'Could not create child post',
+							'data' => $timeline
 						];
 						continue;
 					}
@@ -524,42 +619,98 @@
 					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
-							];
+				// Update post status and menu order
+				$post_updates = ['ID' => $timeline['id']];
+
+				if (!$is_parent) {
+					$post_updates['menu_order'] = $order;
+
+					// Auto-publish child if parent is published
+					if ($parent_is_published) {
+						$current_post = get_post($timeline['id']);
+						if ($current_post && $current_post->post_status !== 'publish') {
+							$post_updates['post_status'] = 'publish';
 						}
 					}
 				}
+
+				if (count($post_updates) > 1) {
+					$result = wp_update_post($post_updates);
+					error_log('Updated post '.$timeline['id'].' with: '.print_r($post_updates, true).' Result: '.$result);
+					$clearParent = true;
+				}
+
+				// Update metadata
+				$meta = new MetaManager($timeline['id'], 'post');
+				$oldValues = $meta->getAll(array_keys($allowedFields));
+
+				// Set number taxonomy to menu_order (always update for reordering)
+				if (!$is_parent) {
+					$number_value = $order;
+					$term = get_term_by('name', (string)$number_value, BASE.'number');
+					if (!$term) {
+						$result = wp_insert_term((string)$number_value, BASE.'number');
+						if ($result && !is_wp_error($result)) {
+							$term = $result['term_id'];
+						}
+					} else {
+						$term = $term->term_id;
+					}
+					$allowedFields['number'] = $term;
+				}
+
+				// Auto-timeline logic
+				if ($prevDate) {
+					$newDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : null);
+					if ($newDate) {
+						$date1 = new \DateTime($prevDate);
+						$date2 = new \DateTime($newDate);
+						$weeks = floor($date1->diff($date2)->days / 7);
+						if ($weeks > 0) {
+							$termToCheck = $weeks.' Weeks';
+							$term = get_term_by('name', $termToCheck, BASE.'timeline');
+							if (!$term) {
+								$result = wp_insert_term($termToCheck, BASE.'timeline');
+								if ($result && !is_wp_error($result)) {
+									$term = $result['term_id'];
+								}
+							} else {
+								$term = $term->term_id;
+							}
+							$allowedFields['timeline'] = $term;
+						}
+					}
+				}
+				$prevDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : $prevDate);
+
+				$updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
+					return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
+				}, ARRAY_FILTER_USE_BOTH);
+				error_log('Setting values for '.$timeline['id'].': '.print_r($updateValues, true));
+
+				$meta->setAll($updateValues);
+				$timeline['id'] = (int) $timeline['id'];
+
 				$success[] = $timeline['id'];
 			}
 		}
-		//Delete any remaining children that no longer exist
+
+		// Delete any remaining children that no longer exist
 		if (!empty($existing_children)) {
 			foreach ($existing_children as $ID) {
 				wp_delete_post($ID);
 			}
 		}
 
+		if ($clearParent) {
+			$this->cache->clear();
+			CacheManager::onPostSave($parent_id, $parent_post);
+		}
+
+
 		return ['success' => true, 'data' => [
-			'success'	=> $success,
-			'errors'	=> $errors
+			'success' => $success,
+			'errors' => $errors
 		]];
 	}
 
@@ -650,6 +801,7 @@
         $this->meta = new MetaManager($post->ID, 'post');
         $data = [
             'id'        => $post->ID,
+			'title'		=> $post->post_title,
             'status'    => $post->post_status,
             'date'      => $post->post_date,
             'modified'  => $post->post_modified,
@@ -719,7 +871,7 @@
 		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
 		//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']);
+		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
 		array_unshift($children, $post->ID);
 
 		$subFields = [];
@@ -728,7 +880,7 @@
 			$meta = new MetaManager($child, 'post');
 			$f = $meta->getAll($this->timelineUniqueFields);
 			$f =  ['id' => $child] + $f;
-			$subFields[$f['post_thumbnail']] = $f;
+			$subFields[] = $f;
 
 			$images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
 		}
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index 90993b4..daadec6 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -1,10 +1,13 @@
 <?php
 namespace JVBase\rest\routes;
 
+use JVBase\managers\CacheManager;
 use JVBase\rest\RestRouteManager;
 use JVBase\integrations\Umami;
 use JVBase\meta\MetaManager;
 use JVBase\managers\TaxonomyRelationships;
+use JVBase\utility\Checker;
+use JVBase\utility\Features;
 use WP_Query;
 use WP_Post;
 use WP_Term;
@@ -14,1020 +17,1265 @@
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
 }
-/**
- * FeedRoutes - Optimized API endpoints for the feed block
- * TODO: Look at Content Routes' setup and make this more like that one; it's a bit more organized
- * Or NewsRoutes
- */
+
 class FeedRoutes extends RestRouteManager
 {
-    protected int $per_page = 36;
-    protected Umami $tracker;
+	protected int $per_page = 36;
+	protected ?Umami $tracker = null;
+	protected ?Checker $checker = null;
+	protected ?array $fields = null;
+	protected ?array $timelineSharedFields = null;
+	protected ?array $timelineUniqueFields = null;
 
-    public function __construct()
-    {
-        $this->cache_name = 'feed';
-        $this->cache_ttl = 86400;
+	public function __construct()
+	{
+		$this->cache_name = 'feed';
+		$this->cache_ttl = 86400;
+		parent::__construct();
+	}
 
-        if (jvbSiteUsesUmami()) {
-            $this->tracker = JVB()->connect('umami');
-        }
-        parent::__construct();
-    }
+	public function init():void
+	{
+		$this->checker = Checker::getInstance();
 
-    /**
-     * Registers feed routes
-     * @return void
-     */
-    public function registerRoutes():void
-    {
-        register_rest_route($this->namespace, '/feed', [
-            'methods' => ['GET', 'POST'],
-            'callback' => [$this, 'handleFeedRequest'],
-            'permission_callback' => [$this, 'checkPermission'],
-        ]);
-    }
-
-    /**
-     * Formats an item
-     * @param int $postID
-     *
-     * @return array
-     */
-    protected function formatItem(int $postID, string $type = 'post'):array
-    {
-        switch ($type) {
-            case 'post':
-                $post = get_post($postID);
-                $type = jvbNoBase($post->post_type);
-                $metaType = 'post';
-                break;
-            default:
-                $post = get_term($postID, jvbCheckBase($type));
-                $type = jvbNoBase($type);
-                $metaType = 'term';
-                break;
-        }
-        if (!$post || is_wp_error($post)) {
-            return [];
-        }
-        $formatted = $this->cache->get($postID, $type);
-//        $formatted = false;
-        if ($formatted) {
-            return $formatted;
-        }
-
-
-        $fields = apply_filters(
-            'jvbFeedFields',
-            [],
-            $type
-        );
-
-        $meta = new MetaManager($postID, $metaType);
-        $formatted = [
-            'id'    => $postID,
-            'icon'  => $type,
-        ];
-
-        if (jvbSiteUsesUmami()) {
-            $args = ($metaType === 'post') ? [ 'owner_id' => $post->post_author] : [];
-            $formatted['umami_view'] = $this->tracker->trackFeedView($postID, $type, $args);
-            $formatted['umami_fav'] = $this->tracker->trackFavouriteToggle($postID, $type, false);
-        }
-        switch ($metaType) {
-            case 'term':
-                if (jvbSiteUsesUmami()) {
-                    $formatted['umami_click'] = $this->tracker->trackTaxonomyClick($postID, $type);
-                }
-                $owner = (in_array($type, jvbContentTaxonomies()) ? $meta->getValue('owner') : null);
-                if (!is_null($owner)) {
-                    $formatted['user_id'] = $owner;
-                }
-                $formatted['url'] = get_term_link($postID, $type);
-                break;
-            default:
-                $formatted = array_merge($formatted, [
-                    'date'      => $post->post_date,
-                    'user_id'   => (int)$post->post_author,
-                    'url'       => get_the_permalink($postID),
-                ]);
-                break;
-        }
-        $order = array_keys($fields);
-        foreach ($fields as $field => $config) {
-            $value = [];
-            if ($field === 'umami_click') {
-                if ($config === 'profile') {
-                    $formatted['umami_click'] = $this->tracker->trackProfileClick($postID, $type);
-                }
-                if ($config === 'contentTaxonomy') {
-                    $formatted['umami_click'] = $this->tracker->trackContentTaxonomyClick($postID, $type);
-                }
-            } else {
-                if (array_key_exists('field', $config)) {
-                    if ($field === 'image' && array_key_exists('gallery', $config)) {
-                        //Array === post types
-                        if (is_array($config['gallery'])) {
-                            $posts = get_posts([
-                                'post_type' => array_map(function ($item) { return BASE.$item; }, $config['gallery']),
-                                'author'    => $post->post_author,
-                                'posts_per_page'    => 5,
-                                'orderby'   => 'date',
-                                'order'     => 'DESC',
-                            ]);
-                            $formatted['content'] = array_map(function ($content) {
-                                return [
-                                    'url'   => get_permalink($content->ID),
-                                    'title' => $content->post_title,
-                                    'image' => jvbImageData((int)get_post_thumbnail_id($content->ID)),
-                                ];
-                            }, $posts);
-                        } else {
-                            //String === $meta
-                            $ids = explode(',', $meta->getValue($config['gallery']));
-                            $formatted['content'] = array_map(function ($id) {
-                                return [
-                                    'image' => jvbImageData((int)$id)
-                                ];
-                            }, $ids);
-                        }
-                    }
-                    switch ($config['field']) {
-                        case 'post_author':
-                            $author = $this->getAuthorData($post);
-                            $value = [
-                                'value' => $author['value'],
-                                'url'   => $author['url']
-                            ];
-                            break;
-                        case 'name':
-                            $value = $post->name;
-                            break;
-                        case 'post_title':
-                            $value = $post->post_title;
-                            break;
-                        case 'image':
-                        case 'image_portrait':
-                        case 'featured_image':
-                            $value = $meta->getValue($config['field']);
-                            $value =  jvbImageData((int)$value);
-                            break;
-                        case 'top_style':
-                        case 'city':
-                        case 'top_theme':
-                            $terms = explode(',', $meta->getValue($field));
-                            $terms = array_filter(array_map(function ($termID) use ($config, $postID, $type) {
-                                $term = get_term($termID, jvbCheckBase($config['icon']));
-                                if ($term && !is_wp_error($term)) {
-                                    return $this->formatTaxonomy($term, $postID, $type);
-                                }
-                                return [];
-                            }, $terms));
-                            $value = [
-                                'terms' => $terms
-                            ];
-                            break;
-                        default:
-                            $value = [
-                                'value' => $meta->getValue($field)
-                            ];
-                    }
-
-                } elseif (array_key_exists('taxonomy', $config)) {
-                    $terms = get_the_terms($postID, BASE.$config['taxonomy']);
-                    if ($terms && !is_wp_error($terms)) {
-                        $terms = array_map(function ($term) use ($postID, $type) {
-                            return $this->formatTaxonomy($term, $postID, $type);
-                        }, $terms);
-                        $value = [
-                            'terms'     => $terms
-                        ];
-                    }
-                }
-
-                if (array_key_exists('label', $config)) {
-                    $value['label'] = $config['label'];
-                }
-                if (array_key_exists('icon', $config)) {
-                    $value['icon'] = $config['icon'];
-                }
-                $formatted[$field] = $value;
-            }
-        }
-
-        $formatted['order'] = $order;
-        $this->cache->set($postID, $formatted, $type);
-
-        return $formatted;
-    }
-
-    protected function formatTaxonomy(WP_Term $term, int $postID, string $type)
-    {
-        return [
-            'ID'    => $term->term_id,
-            'title' => htmlspecialchars_decode($term->name),
-            'url'   => get_term_link($term->term_id, $term->taxonomy),
-            'umami_click'   => $this->tracker->trackTaxonomyClick($term->term_id, $term->taxonomy, [
-                'from'  => $type.'_'.$postID
-            ])
-        ];
-    }
-    protected function getAuthorData(WP_Post $post)
-    {
-        $author = $this->cache->get($post->post_author, 'author_data');
-        if (!$author) {
-            $author = [
-                'id'    => $post->post_author,
-                'label' => 'Artist',
-                'value' => get_the_author_meta('display_name', $post->post_author),
-                'icon'  => 'artist',
-                'url'   => get_the_permalink(get_user_meta($post->post_author, BASE.'link', true)),
-            ];
-            $this->cache->set($post->post_author, $author, 'author_data');
-        }
-        return $author;
-    }
-
-    protected function getTaxonomies(int $postID, string $content):array
-    {
-        $taxonomies = jvbTaxonomiesForContent($content);
-        $out = [];
-        foreach ($taxonomies as $tax) {
-            $terms = get_the_terms($postID, $tax);
-            $t = [];
-            if ($terms && !is_wp_error($terms)) {
-                $config = jvbNoBase($tax);
-                $out[] = [
-                    'icon'  => $config,
-                    'title' => JVB_TAXONOMY[$config]['plural'],
-                    'terms' => array_map(function ($term) use ($tax, $postID, $content) {
-                        return [
-                            'ID'    => $term->term_id,
-                            'title' => htmlspecialchars_decode($term->name),
-                            'url'   => get_term_link($term->term_id, $tax),
-                            'umami_click'   => $this->tracker->trackTaxonomyClick($term->term_id, $tax, [
-                                'from'  => $content.'_'.$postID
-                            ])
-                        ];
-                    }, $terms),
-                ];
-            }
-        }
-        return $out;
-    }
-
-
-    protected function buildRequestArgs(WP_REST_Request $request):array
-    {
-        global $jvb_everything;
-        $data = $request->get_params();
-        error_log('Feed Request: '.print_r($data, true));
-        $args = [
-            'post_type'         => (array_key_exists($data['content'], $jvb_everything)) ?
-                BASE.$data['content'] :
-                BASE.array_key_first(JVB_CONTENT),
-            'paged'             => intval($data['page'] ?? 1),
-            'posts_per_page'    => $this->per_page,
-        ];
-        if (!empty($data['context'])) {
-            $args = $this->applyContextFilters(
-                $args,
-                [
-                    'id' => $data['source'],
-                    'type'=>$data['context']
-                ]
-            );
-        }
-        $args = $this->applyContextFilters($args, $data);
-        $args = $this->applyTaxonomyFilters($args, $data);
-        $args = $this->applyOrderFilters($args, $data);
-        $args = $this->applyDateFilters($args, $data);
-
-        $args = $this->applyFavouritesFilter($args, $data);
-
-        return $args;
-    }
-    /**
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleFeedRequest(WP_REST_Request $request):WP_REST_Response
-    {
-        $args = $this->buildRequestArgs($request);
-
-        error_log('Final Args: '.print_r($args, true));
-
-		// Determine content type(s) for cache checking
-		$content_types = [];
-		if (!empty($data['content'])) {
-			$content_types[] = $data['content'];
+		if (jvbSiteUsesUmami()) {
+			$this->tracker = JVB()->connect('umami');
 		}
-		if (!empty($data['type'])) {
-			$types = is_array($data['type']) ? $data['type'] : [$data['type']];
-			$content_types = array_merge($content_types, $types);
+
+		$this->cache->clear();
+		$this->setupCacheConnections();
+	}
+
+	/**
+	 * Set up cache connections for automatic invalidation
+	 */
+	protected function setupCacheConnections(): void
+	{
+		// Connect to all content types with show_feed
+		$contentTypes = Features::getTypesWithFeature('show_feed', 'content');
+		foreach ($contentTypes as $type) {
+			CacheManager::for('feed_item_'.$type)->connectTo('post');
+			$this->cache->connectTo('post', $type);
 		}
+
+		// Connect to all taxonomies with show_feed
+		$taxonomies = Features::getTypesWithFeature('show_feed', 'taxonomy');
+		foreach ($taxonomies as $tax) {
+			CacheManager::for('feed_item_'.$tax)->connectTo('taxonomy');
+			$this->cache->connectTo('taxonomy', $tax);
+		}
+
+	}
+
+	/**
+	 * Registers feed routes
+	 * @return void
+	 */
+	public function registerRoutes(): void
+	{
+		register_rest_route($this->namespace, '/feed', [
+			'methods' => ['GET', 'POST'],
+			'callback' => [$this, 'handleFeedRequest'],
+			'permission_callback' => [$this, 'checkPermission'],
+		]);
+
+		register_rest_route($this->namespace, 'feed/types', [
+			'permission_callback' => [$this, 'checkPermission'],
+			'methods' => 'GET',
+			'callback' => [$this, 'getFeedTypes']
+		]);
+	}
+
+	/**
+	 * Formats an item
+	 * @param int $postID
+	 *
+	 * @return array
+	 */
+	protected function formatItem(int $postID, string $type = 'post', $skip = false): array
+	{
+		switch ($type) {
+			case 'post':
+				$post = get_post($postID);
+				$type = jvbNoBase($post->post_type);
+				$metaType = 'post';
+				$cache = CacheManager::for('feed_item_'.$type);
+				break;
+			default:
+				$post = get_term($postID, jvbCheckBase($type));
+				$type = jvbNoBase($type);
+				$metaType = 'term';
+				$cache = CacheManager::for('feed_item_'.$type);
+				break;
+		}
+		if (!$post || is_wp_error($post)) {
+			return [];
+		}
+
+		return $cache->remember($postID,
+			function() use ($postID, $type, $metaType, $post, $skip) {
+				$config = null;
+				switch ($metaType) {
+					case 'post':
+						$config = JVB_CONTENT[$type];
+						if (!$skip && array_key_exists('is_timeline', $config) && $config['is_timeline']) {
+							return $this->formatTimeline($postID, $post);
+						}
+						break;
+					case 'term':
+						$config = JVB_TAXONOMY[$type];
+						break;
+				}
+				if (!$config) {
+					return [];
+				}
+				$fields = $config['fields'];
+
+				//Allow custom filtering for public fields
+				if (array_key_exists('feed', $config) && array_key_exists('fields', $config['feed'])) {
+					$fields = array_filter($fields, function($field) use ($config) {
+						return in_array($field, $config['feed']['fields']);
+					}, ARRAY_FILTER_USE_KEY);
+				}
+
+				$meta = new MetaManager($postID, $metaType);
+				$values = $meta->getAll(array_keys($fields));
+
+				$out = [
+					'fields' => $values,
+				];
+
+				//Format Taxonomies
+				$temp = array_filter($fields, function($field) {
+					return $field['type'] === 'taxonomy';
+				});
+				foreach ($temp as $key => $config) {
+					if (array_key_exists($key, $out) && $out[$key] !== '') {
+						$IDs = array_map('absint', explode(',', $out[$key]));
+						$data = [];
+						$icon = JVB_TAXONOMY[$config['taxonomy']]['icon']??'triangle';
+						foreach ($IDs as $ID) {
+							$term = get_term($ID, jvbCheckBase($config['taxonomy']));
+							if ($term && !is_wp_error($term)) {
+								$data[$ID] = [
+									'id' 	=> $ID,
+									'icon'	=> $icon,
+									'name'	=> $term->name,
+									'url'	=> get_term_link($ID, jvbCheckBase($config['taxonomy'])),
+								];
+								if ($this->tracker) {
+									$data[$ID]['umami_click'] = $this->tracker->trackClick($ID, $config['taxonomy'], ['from' => $type.'_'.$postID]);
+								}
+							}
+						}
+						if (!empty($data)) {
+							$out['fields'][$key] = $data;
+						}
+					}
+				}
+
+				//Add images
+				$imgIDs = [];
+				$temp = array_filter($fields, function($field) {
+					return in_array($field['type'], [ 'upload', 'image', 'gallery']);
+				});
+				foreach ($temp as $key => $config) {
+					if (array_key_exists($key, $out) && $out[$key] !== '') {
+						$IDs = array_map('absint', explode(',',$out[$key]));
+						foreach ($IDs as $ID) {
+							$imgIDs[$ID] = jvbImageData($ID);
+						}
+					}
+				}
+				$out['images'] = $imgIDs;
+
+				$out['id'] = $postID;
+				$out['content'] = $type;
+				$out['icon'] = $config['icon']??'triangle';
+
+				if ($this->tracker) {
+					$args = ($metaType === 'post') ? ['owner_id' => $post->post_author] : [];
+					$out['umami_view'] = $this->tracker->trackFeedView($postID, $type, $args);
+					$out['umami_fav'] = $this->tracker->trackFavouriteToggle($postID, $type, false);
+					$out['umami_click'] = $this->tracker->trackClick($postID, $type);
+				}
+
+				switch ($metaType) {
+					case 'term':
+						$owner = (in_array($type, jvbContentTaxonomies()) ? $meta->getValue('owner') : null);
+						if (!is_null($owner)) {
+							$out['user_id'] = $owner;
+						}
+						$out['url'] = get_term_link($postID, $type);
+						break;
+					case 'post':
+						$out['date'] = $post->post_date;
+						$out['user_id'] = (int)$post->post_author;
+						$out['url'] = get_the_permalink($postID);
+						break;
+				}
+				return $out;
+			}
+		);
+
+	}
+
+	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;
+		}));
+	}
+
+	protected function formatTimeline(int $postID, WP_Post $post):array
+	{
+		if (!$this->timelineSharedFields || !$this->timelineUniqueFields){
+			$this->initTimelineFields($post->post_type);
+		}
+		$item = $this->formatItem($postID, 'post', true);
+		//Step 1: Get the fields that apply to all posts
+		$mainMeta = new MetaManager($post->ID, 'post');
+		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
+
+		//Step 2: Get the fields for each individual posts
+		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish'], 'fields'=> 'ids']);
+		array_unshift($children, $post->ID);
+
+		$subFields = [];
+		$images = [];
+		foreach ($children as $child) {
+			$meta = new MetaManager($child, 'post');
+			$f = $meta->getAll($this->timelineUniqueFields);
+			$f =  ['id' => $child] + $f;
+			$subFields[] = $f;
+
+			$images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
+		}
+		$item['fields']['order'] = $subFields;
+		$item['images'] = $item['images'] + $images;
+
+		return $item;
+	}
+
+	protected function formatTaxonomy(WP_Term $term, int $postID, string $type)
+	{
+		return [
+			'ID' => $term->term_id,
+			'title' => htmlspecialchars_decode($term->name),
+			'url' => get_term_link($term->term_id, $term->taxonomy),
+			'umami_click' => $this->tracker->trackTaxonomyClick($term->term_id, $term->taxonomy, [
+				'from' => $type . '_' . $postID
+			])
+		];
+	}
+
+	protected function getAuthorData(WP_Post $post)
+	{
+		$author = $this->cache->get($post->post_author, 'author_data');
+		if (!$author) {
+			$author = [
+				'id' => $post->post_author,
+				'label' => 'Artist',
+				'value' => get_the_author_meta('display_name', $post->post_author),
+				'icon' => 'artist',
+				'url' => get_the_permalink(get_user_meta($post->post_author, BASE . 'link', true)),
+			];
+			$this->cache->set($post->post_author, $author, 'author_data');
+		}
+		return $author;
+	}
+
+	protected function getTaxonomies(int $postID, string $content): array
+	{
+		$taxonomies = jvbTaxonomiesForContent($content);
+		$out = [];
+		foreach ($taxonomies as $tax) {
+			$terms = get_the_terms($postID, $tax);
+			$t = [];
+			if ($terms && !is_wp_error($terms)) {
+				$config = jvbNoBase($tax);
+				$out[] = [
+					'icon' => $config,
+					'title' => JVB_TAXONOMY[$config]['plural'],
+					'terms' => array_map(function ($term) use ($tax, $postID, $content) {
+						return [
+							'ID' => $term->term_id,
+							'title' => htmlspecialchars_decode($term->name),
+							'url' => get_term_link($term->term_id, $tax),
+							'umami_click' => $this->tracker->trackTaxonomyClick($term->term_id, $tax, [
+								'from' => $content . '_' . $postID
+							])
+						];
+					}, $terms),
+				];
+			}
+		}
+		return $out;
+	}
+
+
+	protected function buildRequestArgs(WP_REST_Request $request): array
+	{
+		$data = $request->get_params();
+		error_log('Feed Request: ' . print_r($data, true));
+		$args = [
+			'post_type' => (array_key_exists($data['content'], $this->buildFeedTypesConfig())) ?
+				BASE . $data['content'] :
+				BASE . array_key_first(JVB_CONTENT),
+			'paged' => intval($data['page'] ?? 1),
+			'posts_per_page' => $this->per_page,
+		];
+		if (!empty($data['context'])) {
+			$args = $this->applyContextFilters(
+				$args,
+				[
+					'id' => $data['source']??'0',
+					'type' => $data['context']
+				]
+			);
+		}
+		$args = $this->applyContextFilters($args, $data);
+		$args = $this->applyTaxonomyFilters($args, $data);
+		$args = $this->applyOrderFilters($args, $data);
+		$args = $this->applyDateFilters($args, $data);
+
+		$args = $this->applyFavouritesFilter($args, $data);
+		error_log('Final Args: '.print_r($args, true));
+		return $args;
+	}
+
+	protected function applyTaxonomyFilters(array $args, array $data): array
+	{
+		if (!isset($data['taxonomy']) || empty($data['taxonomy'])) {
+			return $args;
+		}
+
+		$taxonomyFilters = $data['taxonomy'];
+
+		// Validate taxonomies exist and sanitize
+		$validFilters = [];
+		foreach ($taxonomyFilters as $taxonomy => $terms) {
+			if (!taxonomy_exists(jvbCheckBase($taxonomy))) {
+				continue;
+			}
+
+			$validFilters[] = [
+				'taxonomy' => jvbCheckBase($taxonomy),
+				'field' => 'term_id',
+				'terms' => array_map('absint', (array)$terms),
+				'operator' => 'IN'
+			];
+		}
+
+		if (empty($validFilters)) {
+			return $args;
+		}
+
+		// Determine relation based on match filter
+		$relation = ($data['match'] ?? 'all') === 'all' ? 'AND' : 'OR';
+
+		$args['tax_query'] = array_merge(
+			['relation' => $relation],
+			$validFilters
+		);
+
+		return $args;
+	}
+
+	/**
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function handleFeedRequest(WP_REST_Request $request): WP_REST_Response
+	{
+		$args = $this->buildRequestArgs($request);
+		$cacheContext = $this->buildCacheContext($args, $request);
+
 		// Check HTTP cache headers first
-		$cache_check = $this->checkHeaders($request, $content_types ?: ['feed']);
+		$cache_check = $this->checkHeaders(
+			$request,
+			$cacheContext['content_types'],
+			$cacheContext['additional_params']
+		);
 		if ($cache_check) {
 			return $cache_check; // Returns 304 Not Modified
 		}
-
-        $key = $this->cache->generateKey($args);
-        $cached = $this->cache->get($key);
-        if ($cached) {
-            if ($request->get_param('highlight')) {
-                $highlight = json_decode($request->get_param('highlight'), true);
-                $args['highlight'] = $highlight;
-            }
-            $cached['items'] = $this->processHighlightedItem($cached['items'], $args);
-            $response = new WP_REST_Response($cached);
+		error_log('Feed Request Args: '.print_r($args, true));
+		$key = $this->cache->generateKey($args);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			if ($request->get_param('highlight')) {
+				$highlight = json_decode($request->get_param('highlight'), true);
+				$args['highlight'] = $highlight;
+			}
+			$cached['items'] = $this->processHighlightedItem($cached['items'], $args);
+			$response = new WP_REST_Response($cached);
 			return $this->addCacheHeaders($response);
-        }
-        // Fetch and format items
-        $items = $this->fetchFeedItems($args);
+		}
+		// Fetch and format items
+		$items = $this->fetchFeedItems($args);
+
+		error_log('Feed Got items: ' .print_r($items, true));
 
 
-        $ttl = (str_contains($args['orderby'], 'RAND')) ? 1800 : $this->cache_ttl;
-        $this->cache->set($key, $items, $ttl);
+		$ttl = (str_contains($args['orderby'], 'RAND')) ? 1800 : $this->cache_ttl;
+		$this->cache->set($key, $items, $ttl);
 
-        if ($request->get_param('highlight')) {
-            $highlight = json_decode($request->get_param('highlight'), true);
-            $args['highlight'] = $highlight;
-        }
+		if ($request->get_param('highlight')) {
+			$highlight = json_decode($request->get_param('highlight'), true);
+			$args['highlight'] = $highlight;
+		}
 
-        $items['items'] = $this->processHighlightedItem($items['items'], $args);
+		$items['items'] = $this->processHighlightedItem($items['items'], $args);
 		$response = new WP_REST_Response($items);
 		return $this->addCacheHeaders($response);
-    }
+	}
 
-    /**
-     * @param array $args Formatted Args for WP_Query
-     * @param array $data parsed Request Data
-     *
-     * @return array|null
-     */
-    protected function processHighlightedItem(array $items, array $data):array
-    {
-        error_log('Data passed to processHighlightedItem:'.print_r($data, true));
-        if (empty($data['highlight']??null)) {
-            return $items;
-        }
+	/**
+	 * Build cache context from query args
+	 * Extracts content types and parameters needed for proper cache checking
+	 *
+	 * @param array $args Built WP_Query arguments
+	 * @param WP_REST_Request $request Original request
+	 * @return array Cache context with content_types and additional_params
+	 */
+	protected function buildCacheContext(array $args, WP_REST_Request $request): array
+	{
+		// Extract content types from post_type in args
+		$post_types = is_array($args['post_type'])
+			? $args['post_type']
+			: [$args['post_type']];
 
-        // Convert to array if string
-        if (is_string($data['highlight'])) {
-            $data['highlight'] = json_decode($data['highlight'], true);
-        }
+		$content_types = array_map('jvbNoBase', $post_types);
+		$content_types[] = 'feed'; // Always include base feed type
 
-        // Extract key and value
-        $key = array_keys($data['highlight'])[0] ?? false;
-        $value = array_values($data['highlight'])[0] ?? false;
-        error_log('Highlighted item: '.$key);
-        error_log('Highlighted item: '.$value);
-        error_log('No Single Content Types: '.print_r(jvbNoSingleContentTypes(), true));
-        error_log('Page: '.print_r($data['paged'], true));
-        if (in_array($key, jvbNoSingleContentTypes()) && $value && $data['paged'] === 1) {
-            error_log('Formatted Highlighted item: '.print_r($this->formatItem($value), true));
-            error_log('Items: '.print_r($items, true));
-            array_unshift($items, $this->formatItem($value));
-            error_log('Items after unshift: '.print_r($items, true));
-        }
-        return $items;
-    }
+		// Build additional params for ETag uniqueness
+		$additional_params = [
+			'order' => $args['orderby'] ?? 'date',
+			'direction' => $args['order'] ?? 'DESC',
+			'page' => $args['paged'] ?? 1,
+		];
 
-    /**
-     * @param array $args
-     * @param array $context
-     *
-     * @return array
-     */
-    protected function applyContextFilters(array $args, array $context):array
-    {
+		if ($request->get_param('favourites')) {
+			$additional_params['user'] = (int)$request->get_param('user');
+		}
 
-        error_log('Args at Context Filters: '.print_r($args, true));
-        error_log('Request at Context Filters: '.print_r($context, true));
-        if (!isset($context['type'])) {
-            return $args;
-        }
+		// Include author filter if present (from context or favourites)
+		if (!empty($args['author'])) {
+			$additional_params['author'] = $args['author'];
+		}
 
-        switch (true) {
-            case contentIsJVBUserType($context['type']):
-                $args['author'] = (int)get_post_meta($context['id'], BASE.'link', true);
-                break;
-            case taxIsJVBContentTax($context['type']):
-                $args['post_type'] = (is_array($args['post_type'])) ? $args['post_type'] : explode(',',$args['post_type']);
-                if (array_intersect($args['post_type'], array_map(function ($type) { return jvbCheckBase($type); },array_keys(jvbGlobalFeedContent())))) {
-                    $artists = jvbGetContentUsers($context['id']);
-                    if (!empty($artists)) {
-                        $args['author__in'] = $artists;
-                    }
-                } else {
-                    $args['tax_query'] = [
-                        'relation'  => 'AND',
-                        [
-                            'taxonomy' => BASE.$context['type'],
-                            'terms' => $context['id'],
-                        ]
-                    ];
-                }
-                break;
-            case taxonomy_exists(jvbCheckBase($context['type'])):
-                $args['tax_query'] = [
-                    'relation'  => 'AND',
-                    [
-                        'taxonomy' => BASE.$context['type'],
-                        'terms' => $context['id'],
-                    ]
-                ];
-                break;
-        }
-        return $args;
-    }
+		if (!empty($args['author__in'])) {
+			$additional_params['author__in'] = $args['author__in'];
+		}
 
-    /**
-     * @param array $args
-     * @param array $filters
-     *
-     * @return array
-     */
-    protected function applyFavouritesFilter(array $args, array $filters):array
-    {
-        if (!array_key_exists('favourites', $filters)){
-            return $args;
-        }
-        error_log('Proceeding to check for favourites:');
-        global $wpdb;
+		// Include taxonomy filters if present
+		if (!empty($args['tax_query'])) {
+			$tax_filters = [];
+			foreach ($args['tax_query'] as $key => $query) {
+				if ($key === 'relation' || !is_array($query)) {
+					continue;
+				}
 
-        // Get post types for the current filter
-        $post_types = explode(',', $args['post_type']);
+				$taxonomy = jvbNoBase($query['taxonomy'] ?? '');
+				if ($taxonomy) {
+					$tax_filters[$taxonomy] = $query['terms'] ?? [];
+					// Also add taxonomy to content_types for timestamp checking
+					$content_types[] = $taxonomy;
+				}
+			}
+			if (!empty($tax_filters)) {
+				$additional_params['taxonomies'] = $tax_filters;
+			}
+		}
 
-        $favourites_table = $wpdb->prefix . BASE . 'favourites';
-        $placeholders = implode(',', array_fill(0, count($post_types), '%s'));
-        error_log('CurrentUser ID: '.print_r(get_current_user_id(), true));
-        $favourited_ids = $wpdb->get_col($wpdb->prepare(
-            "SELECT target_id FROM {$favourites_table}
+		// Include date filters if present
+		if (!empty($args['date_query'])) {
+			$additional_params['date_filter'] = md5(serialize($args['date_query']));
+		}
+
+		// Include meta queries if present
+		if (!empty($args['meta_query'])) {
+			$additional_params['meta_filter'] = md5(serialize($args['meta_query']));
+		}
+
+		return [
+			'content_types' => array_unique($content_types),
+			'additional_params' => $additional_params
+		];
+	}
+
+	/**
+	 * @param array $args Formatted Args for WP_Query
+	 * @param array $data parsed Request Data
+	 *
+	 * @return array|null
+	 */
+	protected function processHighlightedItem(array $items, array $data): array
+	{
+		error_log('Data passed to processHighlightedItem:' . print_r($data, true));
+		if (empty($data['highlight'] ?? null)) {
+			return $items;
+		}
+
+		// Convert to array if string
+		if (is_string($data['highlight'])) {
+			$data['highlight'] = json_decode($data['highlight'], true);
+		}
+
+		// Extract key and value
+		$key = array_keys($data['highlight'])[0] ?? false;
+		$value = array_values($data['highlight'])[0] ?? false;
+		error_log('Highlighted item: ' . $key);
+		error_log('Highlighted item: ' . $value);
+		error_log('No Single Content Types: ' . print_r(jvbNoSingleContentTypes(), true));
+		error_log('Page: ' . print_r($data['paged'], true));
+		if (in_array($key, jvbNoSingleContentTypes()) && $value && $data['paged'] === 1) {
+			error_log('Formatted Highlighted item: ' . print_r($this->formatItem($value), true));
+			error_log('Items: ' . print_r($items, true));
+			array_unshift($items, $this->formatItem($value));
+			error_log('Items after unshift: ' . print_r($items, true));
+		}
+		return $items;
+	}
+
+	/**
+	 * @param array $args
+	 * @param array $context
+	 *
+	 * @return array
+	 */
+	protected function applyContextFilters(array $args, array $context): array
+	{
+		if (!isset($context['type'])) {
+			return $args;
+		}
+
+		switch (true) {
+			case contentIsJVBUserType($context['type']):
+				$args['author'] = (int)get_post_meta($context['id'], BASE . 'link', true);
+				break;
+			case taxIsJVBContentTax($context['type']):
+				$args['post_type'] = is_array($args['post_type'])
+					? $args['post_type']
+					: explode(',', $args['post_type']);
+
+				// Check if filtering global feed content
+				$globalFeedTypes = array_map('jvbCheckBase',
+					array_keys(Features::getTypesWithFeature('show_feed', 'content'))
+				);
+
+				if (array_intersect($args['post_type'], $globalFeedTypes)) {
+					$artists = jvbGetContentUsers($context['id']);
+					if (!empty($artists)) {
+						$args['author__in'] = $artists;
+					}
+				} else {
+					$args['tax_query'] = [
+						'relation' => 'AND',
+						[
+							'taxonomy' => BASE . $context['type'],
+							'terms' => $context['id'],
+						]
+					];
+				}
+				break;
+			case taxonomy_exists(jvbCheckBase($context['type'])):
+				$args['tax_query'] = [
+					'relation' => 'AND',
+					[
+						'taxonomy' => BASE . $context['type'],
+						'terms' => $context['id'],
+					]
+				];
+				break;
+		}
+		return $args;
+	}
+
+	/**
+	 * @param array $args
+	 * @param array $filters
+	 *
+	 * @return array
+	 */
+	protected function applyFavouritesFilter(array $args, array $filters): array
+	{
+		if (!array_key_exists('favourites', $filters)) {
+			return $args;
+		}
+		error_log('Proceeding to check for favourites:');
+		global $wpdb;
+
+		// Get post types for the current filter
+		$post_types = is_array($args['post_type'])
+			? $args['post_type']
+			: [$args['post_type']];
+
+		$favourites_table = $wpdb->prefix . BASE . 'favourites';
+		$placeholders = implode(',', array_fill(0, count($post_types), '%s'));
+		error_log('CurrentUser ID: ' . print_r(get_current_user_id(), true));
+		$favourited_ids = $wpdb->get_col($wpdb->prepare(
+			"SELECT target_id FROM {$favourites_table}
             WHERE user_id = %d AND type IN ($placeholders)",
-            array_merge(
-                [get_current_user_id()],
-                $post_types
-            )
-        ));
+			array_merge(
+				[get_current_user_id()],
+				$post_types
+			)
+		));
 
-        if (empty($favourited_ids)) {
-            // Force empty results
-            $args['post__in'] = [0];
-            return $args;
-        }
+		if (empty($favourited_ids)) {
+			// Force empty results
+			$args['post__in'] = [0];
+			return $args;
+		}
 
-        $args['post__in'] = isset($args['post__in'])
-            ? array_intersect($args['post__in'], $favourited_ids)
-            : $favourited_ids;
+		$args['post__in'] = isset($args['post__in'])
+			? array_intersect($args['post__in'], $favourited_ids)
+			: $favourited_ids;
 
-        return $args;
-    }
+		return $args;
+	}
 
-    /**
-     * @param array $args
-     *
-     * @return array
-     */
-    protected function fetchFeedItems(array $args):array
-    {
-        if (in_array($args['post_type'], jvbContentTaxonomies())) {
-            return $this->handleContentTaxonomies($args);
-        }
-        $args['fields'] = 'ids';
-        // Get post IDs
-        $query = new WP_Query($args);
+	/**
+	 * @param array $args
+	 *
+	 * @return array
+	 */
+	protected function fetchFeedItems(array $args): array
+	{
+		$postType = is_array($args['post_type']) ? $args['post_type'][0] : $args['post_type'];
+		$slug = jvbNoBase($postType);
 
-        // Batch prefetch related data
-        update_meta_cache('post', $query->posts);
-        update_object_term_cache($query->posts, $args['post_type']);
+		if (Features::forContent($slug)->has('is_timeline')) {
+			$args['post_parent'] = 0;
+		}
+		if (in_array($slug, Features::getTypesWithFeature('is_content', 'taxonomy'))) {
+			return $this->handleContentTaxonomies($args);
+		}
+		$args['fields'] = 'ids';
+		// Get post IDs
+		$query = new WP_Query($args);
 
-        // Format regular items
-        $items = array_map(function ($post) {
-            return $this->formatItem($post);
-        }, $query->posts);
+		// Batch prefetch related data
+		update_meta_cache('post', $query->posts);
+		update_object_term_cache($query->posts, $args['post_type']);
 
-        wp_reset_postdata();
-        return [
-            'items' => $items,
-            'has_more' => $query->max_num_pages > $args['paged'],
-            'total' => $query->found_posts
-        ];
-    }
+		// Format regular items
+		$items = array_map(fn($post) => $this->formatItem($post), $query->posts);
 
-    protected function handleContentTaxonomies(array $args):array
-    {
+		wp_reset_postdata();
+		return [
+			'items' => $items,
+			'has_more' => $query->max_num_pages > $args['paged'],
+			'total' => $query->found_posts
+		];
+	}
 
-        $taxonomy = jvbNoBase($args['post_type']);
-        global $wpdb;
-        $table = $wpdb->prefix.BASE.'content_'.$taxonomy;
+	protected function handleContentTaxonomies(array $args): array
+	{
 
-        // Check if table exists
-        if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
-            return [
-                'items' => [],
-                'has_more' => false,
-                'total' => 0
-            ];
-        }
+		$taxonomy = jvbNoBase($args['post_type']);
+		global $wpdb;
+		$table = $wpdb->prefix . BASE . 'content_' . $taxonomy;
 
-        // Build the query components
-        $queryBuilder = $this->buildCustomTableQuery($args, $table, $taxonomy);
+		// Check if table exists
+		if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
+			return [
+				'items' => [],
+				'has_more' => false,
+				'total' => 0
+			];
+		}
 
-        // Execute count query first
-        $total = (int) $wpdb->get_var($queryBuilder['count_query']);
+		// Build the query components
+		$queryBuilder = $this->buildCustomTableQuery($args, $table, $taxonomy);
 
-        // Execute main query if we have results
-        $items = [];
-        if ($total > 0) {
-            $results = $wpdb->get_results($queryBuilder['main_query'],ARRAY_A);
-            $items = array_map(function ($ID) use ($taxonomy) {
-                return $this->formatItem($ID['term_id'], $taxonomy);
-            }, $results);
-        }
+		// Execute count query first
+		$total = (int)$wpdb->get_var($queryBuilder['count_query']);
 
-        $page = $args['paged'] ?? 1;
-        $per_page = $args['posts_per_page'] ?? $this->per_page;
-        $has_more = ($page * $per_page) < $total;
+		// Execute main query if we have results
+		$items = [];
+		if ($total > 0) {
+			$results = $wpdb->get_results($queryBuilder['main_query'], ARRAY_A);
+			$items = array_map(
+				fn($ID) => $this->formatItem($ID['term_id'], $taxonomy),
+				$results
+			);
+		}
 
-        return [
-            'items' => $items,
-            'has_more' => $has_more,
-            'total' => $total
-        ];
-    }
-    /**
-     * Build SQL query components for custom table
-     * @param array $args WP_Query style arguments
-     * @param string $table Table name
-     * @param string $taxonomy Taxonomy type
-     * @return array Query components
-     */
-    protected function buildCustomTableQuery(array $args, string $table, string $taxonomy): array
-    {
-        global $wpdb;
+		$page = $args['paged'] ?? 1;
+		$per_page = $args['posts_per_page'] ?? $this->per_page;
+		$has_more = ($page * $per_page) < $total;
 
-        $where_conditions = ['1=1'];
-        $joins = [];
-        $params = [];
+		return [
+			'items' => $items,
+			'has_more' => $has_more,
+			'total' => $total
+		];
+	}
 
-        // Handle search
-        if (!empty($args['s'])) {
-            $search = '%' . $wpdb->esc_like($args['s']) . '%';
-            $where_conditions[] = "(ct.name LIKE %s OR ct.slug LIKE %s)";
-            $params[] = $search;
-            $params[] = $search;
-        }
+	/**
+	 * Build SQL query components for custom table
+	 * @param array $args WP_Query style arguments
+	 * @param string $table Table name
+	 * @param string $taxonomy Taxonomy type
+	 * @return array Query components
+	 */
+	protected function buildCustomTableQuery(array $args, string $table, string $taxonomy): array
+	{
+		global $wpdb;
 
-        // Handle context filters (e.g., filtering shops by style through relationships)
-        if (!empty($args['context_filter'])) {
-            $context_conditions = $this->buildContextConditions($args['context_filter'], $joins, $params, $taxonomy);
-            if (!empty($context_conditions)) {
-                $where_conditions[] = $context_conditions;
-            }
-        }
+		$where_conditions = ['1=1'];
+		$joins = [];
+		$params = [];
 
-        // Handle taxonomy filters (tax_query)
-        if (!empty($args['tax_query'])) {
-            $tax_conditions = $this->buildTaxonomyConditions($args['tax_query'], $joins, $params, $taxonomy);
-            if (!empty($tax_conditions)) {
-                $where_conditions[] = $tax_conditions;
-            }
-        }
+		// Handle search
+		if (!empty($args['s'])) {
+			$search = '%' . $wpdb->esc_like($args['s']) . '%';
+			$where_conditions[] = "(ct.name LIKE %s OR ct.slug LIKE %s)";
+			$params[] = $search;
+			$params[] = $search;
+		}
 
-        // Handle meta queries (custom fields in the table)
-        if (!empty($args['meta_query'])) {
-            $meta_conditions = $this->buildMetaConditions($args['meta_query'], $joins, $params, $taxonomy);
-            if (!empty($meta_conditions)) {
-                $where_conditions[] = $meta_conditions;
-            }
-        }
+		// Handle context filters (e.g., filtering shops by style through relationships)
+		if (!empty($args['context_filter'])) {
+			$context_conditions = $this->buildContextConditions($args['context_filter'], $joins, $params, $taxonomy);
+			if (!empty($context_conditions)) {
+				$where_conditions[] = $context_conditions;
+			}
+		}
 
-        // Handle date queries
-        if (!empty($args['date_query'])) {
-            $date_conditions = $this->buildDateConditions($args['date_query'], $params);
-            if (!empty($date_conditions)) {
-                $where_conditions[] = $date_conditions;
-            }
-        }
+		// Handle taxonomy filters (tax_query)
+		if (!empty($args['tax_query'])) {
+			$tax_conditions = $this->buildTaxonomyConditions($args['tax_query'], $joins, $params, $taxonomy);
+			if (!empty($tax_conditions)) {
+				$where_conditions[] = $tax_conditions;
+			}
+		}
 
-        // Handle specific IDs
-        if (!empty($args['include'])) {
-            $placeholders = implode(',', array_fill(0, count($args['include']), '%d'));
-            $where_conditions[] = "ct.term_id IN ({$placeholders})";
-            $params = array_merge($params, $args['include']);
-        }
+		// Handle meta queries (custom fields in the table)
+		if (!empty($args['meta_query'])) {
+			$meta_conditions = $this->buildMetaConditions($args['meta_query'], $joins, $params, $taxonomy);
+			if (!empty($meta_conditions)) {
+				$where_conditions[] = $meta_conditions;
+			}
+		}
 
-        if (!empty($args['exclude'])) {
-            $placeholders = implode(',', array_fill(0, count($args['exclude']), '%d'));
-            $where_conditions[] = "ct.term_id NOT IN ({$placeholders})";
-            $params = array_merge($params, $args['exclude']);
-        }
+		// Handle date queries
+		if (!empty($args['date_query'])) {
+			$date_conditions = $this->buildDateConditions($args['date_query'], $params);
+			if (!empty($date_conditions)) {
+				$where_conditions[] = $date_conditions;
+			}
+		}
 
-        // Build ORDER BY
-        $order_by = $this->buildOrderBy($args, $taxonomy);
+		// Handle specific IDs
+		if (!empty($args['include'])) {
+			$placeholders = implode(',', array_fill(0, count($args['include']), '%d'));
+			$where_conditions[] = "ct.term_id IN ({$placeholders})";
+			$params = array_merge($params, $args['include']);
+		}
 
-        // Build LIMIT
-        $page = $args['paged'] ?? 1;
-        $per_page = $args['posts_per_page'] ?? $this->per_page;
-        $offset = ($page - 1) * $per_page;
+		if (!empty($args['exclude'])) {
+			$placeholders = implode(',', array_fill(0, count($args['exclude']), '%d'));
+			$where_conditions[] = "ct.term_id NOT IN ({$placeholders})";
+			$params = array_merge($params, $args['exclude']);
+		}
 
-        // Combine everything
-        $joins_sql = !empty($joins) ? implode(' ', $joins) : '';
-        $where_sql = implode(' AND ', $where_conditions);
+		// Build ORDER BY
+		$order_by = $this->buildOrderBy($args, $taxonomy);
 
-        $base_query = "FROM {$table} ct
+		// Build LIMIT
+		$page = $args['paged'] ?? 1;
+		$per_page = $args['posts_per_page'] ?? $this->per_page;
+		$offset = ($page - 1) * $per_page;
+
+		// Combine everything
+		$joins_sql = !empty($joins) ? implode(' ', $joins) : '';
+		$where_sql = implode(' AND ', $where_conditions);
+
+		$base_query = "FROM {$table} ct
                    LEFT JOIN {$wpdb->terms} t ON ct.term_id = t.term_id
                    LEFT JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                    {$joins_sql}
                    WHERE {$where_sql}";
 
-        $count_query = "SELECT COUNT(DISTINCT ct.term_id) {$base_query}";
+		$count_query = "SELECT COUNT(DISTINCT ct.term_id) {$base_query}";
 
-        $main_query = "SELECT ct.term_id
+		$main_query = "SELECT ct.term_id
                    {$base_query}
                    {$order_by}
                    LIMIT %d OFFSET %d";
 
-        // Add limit parameters
-        $count_params = $params;
-        $main_params = array_merge($params, [$per_page, $offset]);
+		// Add limit parameters
+		$count_params = $params;
+		$main_params = array_merge($params, [$per_page, $offset]);
 
-        return [
-            'main_query' => $wpdb->prepare($main_query, $main_params),
-            'count_query' => $wpdb->prepare($count_query, $count_params)
-        ];
-    }
+		return [
+			'main_query' => $wpdb->prepare($main_query, $main_params),
+			'count_query' => $wpdb->prepare($count_query, $count_params)
+		];
+	}
 
-    /**
-     * Build context-based filter conditions (e.g., shops filtered by style relationships)
-     * @param array $context_filter Context filter data
-     * @param array &$joins Reference to joins array
-     * @param array &$params Reference to params array
-     * @param string $taxonomy Current taxonomy type
-     * @return string SQL condition
-     */
-    protected function buildContextConditions(array $context_filter, array &$joins, array &$params, string $taxonomy): string
-    {
-        global $wpdb;
+	/**
+	 * Build context-based filter conditions (e.g., shops filtered by style relationships)
+	 * @param array $context_filter Context filter data
+	 * @param array &$joins Reference to joins array
+	 * @param array &$params Reference to params array
+	 * @param string $taxonomy Current taxonomy type
+	 * @return string SQL condition
+	 */
+	protected function buildContextConditions(array $context_filter, array &$joins, array &$params, string $taxonomy): string
+	{
+		global $wpdb;
 
-        $context_type = $context_filter['type'] ?? '';
-        $context_id = $context_filter['id'] ?? 0;
+		$context_type = $context_filter['type'] ?? '';
+		$context_id = $context_filter['id'] ?? 0;
 
-        if (empty($context_type) || empty($context_id)) {
-            return '';
-        }
+		if (empty($context_type) || empty($context_id)) {
+			return '';
+		}
 
-        $relationships_table = $wpdb->prefix . BASE . 'taxonomy_relationships';
+		$relationships_table = $wpdb->prefix . BASE . 'taxonomy_relationships';
 
-        switch ($context_type) {
-            case 'style':
-            case 'theme':
-            case 'pstyle':
-                // For shops filtered by style/theme through relationships
-                if ($taxonomy === 'shop') {
-                    $joins[] = "INNER JOIN {$relationships_table} tr ON ct.term_id = tr.term_id";
-                    $where_condition = "tr.related_term_id = %d AND tr.related_taxonomy = %s";
-                    $params[] = $context_id;
-                    $params[] = BASE . $context_type;
-                    return $where_condition;
-                }
-                break;
+		switch ($context_type) {
+			case 'style':
+			case 'theme':
+			case 'pstyle':
+				// For shops filtered by style/theme through relationships
+				if ($taxonomy === 'shop') {
+					$joins[] = "INNER JOIN {$relationships_table} tr ON ct.term_id = tr.term_id";
+					$where_condition = "tr.related_term_id = %d AND tr.related_taxonomy = %s";
+					$params[] = $context_id;
+					$params[] = BASE . $context_type;
+					return $where_condition;
+				}
+				break;
 
-            case 'city':
-                // Filter by city
-                if (isset($this->getCustomTableFields($taxonomy)['city'])) {
-                    $params[] = $context_id;
-                    return "ct.city = %d";
-                }
-                break;
-        }
+			case 'city':
+				// Filter by city
+				if (isset($this->getCustomTableFields($taxonomy)['city'])) {
+					$params[] = $context_id;
+					return "ct.city = %d";
+				}
+				break;
+		}
 
-        return '';
-    }
+		return '';
+	}
 
-    /**
-     * Build taxonomy filter conditions for custom table
-     * @param array $tax_query Tax query array
-     * @param array &$joins Reference to joins array
-     * @param array &$params Reference to params array
-     * @param string $taxonomy Current taxonomy type
-     * @return string SQL condition
-     */
-    protected function buildTaxonomyConditions(array $tax_query, array &$joins, array &$params, string $taxonomy): string
-    {
-        global $wpdb;
+	/**
+	 * Build taxonomy filter conditions for custom table
+	 * @param array $tax_query Tax query array
+	 * @param array &$joins Reference to joins array
+	 * @param array &$params Reference to params array
+	 * @param string $taxonomy Current taxonomy type
+	 * @return string SQL condition
+	 */
+	protected function buildTaxonomyConditions(array $tax_query, array &$joins, array &$params, string $taxonomy): string
+	{
+		global $wpdb;
 
-        $conditions = [];
-        $relation = $tax_query['relation'] ?? 'AND';
+		$conditions = [];
+		$relation = $tax_query['relation'] ?? 'AND';
 
-        foreach ($tax_query as $key => $query) {
-            if ($key === 'relation' || !is_array($query)) {
-                continue;
-            }
+		foreach ($tax_query as $key => $query) {
+			if ($key === 'relation' || !is_array($query)) {
+				continue;
+			}
 
-            $query_taxonomy = $query['taxonomy'] ?? '';
-            $terms = (array)($query['terms'] ?? []);
-            $field = $query['field'] ?? 'term_id';
-            $operator = $query['operator'] ?? 'IN';
+			$query_taxonomy = $query['taxonomy'] ?? '';
+			$terms = (array)($query['terms'] ?? []);
+			$field = $query['field'] ?? 'term_id';
+			$operator = $query['operator'] ?? 'IN';
 
-            if (empty($query_taxonomy) || empty($terms)) {
-                continue;
-            }
+			if (empty($query_taxonomy) || empty($terms)) {
+				continue;
+			}
 
-            // Check if this taxonomy field exists in our custom table
-            $custom_fields = $this->getCustomTableFields($taxonomy);
-            $taxonomy_clean = str_replace(BASE, '', $query_taxonomy);
+			// Check if this taxonomy field exists in our custom table
+			$custom_fields = $this->getCustomTableFields($taxonomy);
+			$taxonomy_clean = str_replace(BASE, '', $query_taxonomy);
 
-            if (isset($custom_fields[$taxonomy_clean])) {
-                // Field exists in custom table - direct query
-                $field_column = "ct.{$taxonomy_clean}";
+			if (isset($custom_fields[$taxonomy_clean])) {
+				// Field exists in custom table - direct query
+				$field_column = "ct.{$taxonomy_clean}";
 
-                if ($field === 'slug') {
-                    // Need to convert slugs to IDs first
-                    $term_ids = [];
-                    foreach ($terms as $slug) {
-                        $term = get_term_by('slug', $slug, $query_taxonomy);
-                        if ($term) {
-                            $term_ids[] = $term->term_id;
-                        }
-                    }
-                    $terms = $term_ids;
-                }
+				if ($field === 'slug') {
+					// Need to convert slugs to IDs first
+					$term_ids = [];
+					foreach ($terms as $slug) {
+						$term = get_term_by('slug', $slug, $query_taxonomy);
+						if ($term) {
+							$term_ids[] = $term->term_id;
+						}
+					}
+					$terms = $term_ids;
+				}
 
-                if (!empty($terms)) {
-                    $placeholders = implode(',', array_fill(0, count($terms), '%d'));
-                    $conditions[] = "{$field_column} {$operator} ({$placeholders})";
-                    $params = array_merge($params, $terms);
-                }
-            } else {
-                // Need to join with term relationships
-                $join_alias = "tr_{$key}";
-                $joins[] = "LEFT JOIN {$wpdb->term_relationships} {$join_alias} ON ct.term_id = {$join_alias}.object_id";
-                $joins[] = "LEFT JOIN {$wpdb->term_taxonomy} tt_{$key} ON {$join_alias}.term_taxonomy_id = tt_{$key}.term_taxonomy_id";
+				if (!empty($terms)) {
+					$placeholders = implode(',', array_fill(0, count($terms), '%d'));
+					$conditions[] = "{$field_column} {$operator} ({$placeholders})";
+					$params = array_merge($params, $terms);
+				}
+			} else {
+				// Need to join with term relationships
+				$join_alias = "tr_{$key}";
+				$joins[] = "LEFT JOIN {$wpdb->term_relationships} {$join_alias} ON ct.term_id = {$join_alias}.object_id";
+				$joins[] = "LEFT JOIN {$wpdb->term_taxonomy} tt_{$key} ON {$join_alias}.term_taxonomy_id = tt_{$key}.term_taxonomy_id";
 
-                if ($field === 'slug') {
-                    $joins[] = "LEFT JOIN {$wpdb->terms} t_{$key} ON tt_{$key}.term_id = t_{$key}.term_id";
-                    $field_column = "t_{$key}.slug";
-                    $term_values = $terms; // Use slugs directly
-                } else {
-                    $field_column = "tt_{$key}.term_id";
-                    $term_values = array_map('intval', $terms);
-                }
+				if ($field === 'slug') {
+					$joins[] = "LEFT JOIN {$wpdb->terms} t_{$key} ON tt_{$key}.term_id = t_{$key}.term_id";
+					$field_column = "t_{$key}.slug";
+					$term_values = $terms; // Use slugs directly
+				} else {
+					$field_column = "tt_{$key}.term_id";
+					$term_values = array_map('intval', $terms);
+				}
 
-                $placeholders = implode(',', array_fill(0, count($term_values), $field === 'slug' ? '%s' : '%d'));
-                $taxonomy_condition = "tt_{$key}.taxonomy = %s";
-                $terms_condition = "{$field_column} {$operator} ({$placeholders})";
+				$placeholders = implode(',', array_fill(0, count($term_values), $field === 'slug' ? '%s' : '%d'));
+				$taxonomy_condition = "tt_{$key}.taxonomy = %s";
+				$terms_condition = "{$field_column} {$operator} ({$placeholders})";
 
-                $conditions[] = "({$taxonomy_condition} AND {$terms_condition})";
-                $params[] = $query_taxonomy;
-                $params = array_merge($params, $term_values);
-            }
-        }
+				$conditions[] = "({$taxonomy_condition} AND {$terms_condition})";
+				$params[] = $query_taxonomy;
+				$params = array_merge($params, $term_values);
+			}
+		}
 
-        return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
-    }
+		return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
+	}
 
-    /**
-     * Build meta query conditions for custom table fields
-     * @param array $meta_query Meta query array
-     * @param array &$joins Reference to joins array
-     * @param array &$params Reference to params array
-     * @param string $taxonomy Taxonomy type
-     * @return string SQL condition
-     */
-    protected function buildMetaConditions(array $meta_query, array &$joins, array &$params, string $taxonomy): string
-    {
-        global $wpdb;
-        $conditions = [];
-        $relation = $meta_query['relation'] ?? 'AND';
+	/**
+	 * Build meta query conditions for custom table fields
+	 * @param array $meta_query Meta query array
+	 * @param array &$joins Reference to joins array
+	 * @param array &$params Reference to params array
+	 * @param string $taxonomy Taxonomy type
+	 * @return string SQL condition
+	 */
+	protected function buildMetaConditions(array $meta_query, array &$joins, array &$params, string $taxonomy): string
+	{
+		global $wpdb;
+		$conditions = [];
+		$relation = $meta_query['relation'] ?? 'AND';
 
-        // Get fields for this taxonomy to know which are in the custom table
-        $custom_fields = $this->getCustomTableFields($taxonomy);
+		// Get fields for this taxonomy to know which are in the custom table
+		$custom_fields = $this->getCustomTableFields($taxonomy);
 
-        foreach ($meta_query as $key => $query) {
-            if ($key === 'relation' || !is_array($query)) {
-                continue;
-            }
+		foreach ($meta_query as $key => $query) {
+			if ($key === 'relation' || !is_array($query)) {
+				continue;
+			}
 
-            $meta_key = $query['key'] ?? '';
-            $meta_value = $query['value'] ?? '';
-            $compare = $query['compare'] ?? '=';
+			$meta_key = $query['key'] ?? '';
+			$meta_value = $query['value'] ?? '';
+			$compare = $query['compare'] ?? '=';
 
-            if (empty($meta_key)) {
-                continue;
-            }
+			if (empty($meta_key)) {
+				continue;
+			}
 
-            // Remove BASE prefix if present
-            $clean_key = str_replace(BASE, '', $meta_key);
+			// Remove BASE prefix if present
+			$clean_key = str_replace(BASE, '', $meta_key);
 
-            // Check if this field exists in our custom table
-            if (isset($custom_fields[$clean_key])) {
-                // Field is in custom table, query directly
-                $column = "ct.{$clean_key}";
-                $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
-                if ($condition) {
-                    $conditions[] = $condition;
-                }
-            } else {
-                // Field is in term meta, need to join
-                $join_alias = "tm_{$key}";
-                $joins[] = "LEFT JOIN {$wpdb->termmeta} {$join_alias} ON ct.term_id = {$join_alias}.term_id AND {$join_alias}.meta_key = %s";
-                $params[] = $meta_key;
+			// Check if this field exists in our custom table
+			if (isset($custom_fields[$clean_key])) {
+				// Field is in custom table, query directly
+				$column = "ct.{$clean_key}";
+				$condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
+				if ($condition) {
+					$conditions[] = $condition;
+				}
+			} else {
+				// Field is in term meta, need to join
+				$join_alias = "tm_{$key}";
+				$joins[] = "LEFT JOIN {$wpdb->termmeta} {$join_alias} ON ct.term_id = {$join_alias}.term_id AND {$join_alias}.meta_key = %s";
+				$params[] = $meta_key;
 
-                $column = "{$join_alias}.meta_value";
-                $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
-                if ($condition) {
-                    $conditions[] = $condition;
-                }
-            }
-        }
+				$column = "{$join_alias}.meta_value";
+				$condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
+				if ($condition) {
+					$conditions[] = $condition;
+				}
+			}
+		}
 
-        return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
-    }
+		return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
+	}
 
-    /**
-     * Build comparison condition for meta fields
-     * @param string $column Column name
-     * @param mixed $value Value to compare
-     * @param string $compare Comparison operator
-     * @param array &$params Reference to params array
-     * @return string SQL condition
-     */
-    protected function buildMetaComparison(string $column, $value, string $compare, array &$params): string
-    {
-        switch (strtoupper($compare)) {
-            case 'LIKE':
-                $params[] = '%' . $value . '%';
-                return "{$column} LIKE %s";
+	/**
+	 * Build comparison condition for meta fields
+	 * @param string $column Column name
+	 * @param mixed $value Value to compare
+	 * @param string $compare Comparison operator
+	 * @param array &$params Reference to params array
+	 * @return string SQL condition
+	 */
+	protected function buildMetaComparison(string $column, $value, string $compare, array &$params): string
+	{
+		switch (strtoupper($compare)) {
+			case 'LIKE':
+				$params[] = '%' . $value . '%';
+				return "{$column} LIKE %s";
 
-            case 'NOT LIKE':
-                $params[] = '%' . $value . '%';
-                return "{$column} NOT LIKE %s";
+			case 'NOT LIKE':
+				$params[] = '%' . $value . '%';
+				return "{$column} NOT LIKE %s";
 
-            case 'IN':
-                if (is_array($value)) {
-                    $placeholders = implode(',', array_fill(0, count($value), '%s'));
-                    $params = array_merge($params, $value);
-                    return "{$column} IN ({$placeholders})";
-                }
-                break;
+			case 'IN':
+				if (is_array($value)) {
+					$placeholders = implode(',', array_fill(0, count($value), '%s'));
+					$params = array_merge($params, $value);
+					return "{$column} IN ({$placeholders})";
+				}
+				break;
 
-            case 'NOT IN':
-                if (is_array($value)) {
-                    $placeholders = implode(',', array_fill(0, count($value), '%s'));
-                    $params = array_merge($params, $value);
-                    return "{$column} NOT IN ({$placeholders})";
-                }
-                break;
+			case 'NOT IN':
+				if (is_array($value)) {
+					$placeholders = implode(',', array_fill(0, count($value), '%s'));
+					$params = array_merge($params, $value);
+					return "{$column} NOT IN ({$placeholders})";
+				}
+				break;
 
-            case 'BETWEEN':
-                if (is_array($value) && count($value) === 2) {
-                    $params[] = $value[0];
-                    $params[] = $value[1];
-                    return "{$column} BETWEEN %s AND %s";
-                }
-                break;
+			case 'BETWEEN':
+				if (is_array($value) && count($value) === 2) {
+					$params[] = $value[0];
+					$params[] = $value[1];
+					return "{$column} BETWEEN %s AND %s";
+				}
+				break;
 
-            case '!=':
-            case '<>':
-                $params[] = $value;
-                return "{$column} != %s";
+			case '!=':
+			case '<>':
+				$params[] = $value;
+				return "{$column} != %s";
 
-            case '>':
-                $params[] = $value;
-                return "{$column} > %s";
+			case '>':
+				$params[] = $value;
+				return "{$column} > %s";
 
-            case '>=':
-                $params[] = $value;
-                return "{$column} >= %s";
+			case '>=':
+				$params[] = $value;
+				return "{$column} >= %s";
 
-            case '<':
-                $params[] = $value;
-                return "{$column} < %s";
+			case '<':
+				$params[] = $value;
+				return "{$column} < %s";
 
-            case '<=':
-                $params[] = $value;
-                return "{$column} <= %s";
+			case '<=':
+				$params[] = $value;
+				return "{$column} <= %s";
 
-            case '=':
-            default:
-                $params[] = $value;
-                return "{$column} = %s";
-        }
+			case '=':
+			default:
+				$params[] = $value;
+				return "{$column} = %s";
+		}
 
-        return '';
-    }
+		return '';
+	}
 
-    /**
-     * Build date query conditions
-     * @param array $date_query Date query array
-     * @param array &$params Reference to params array
-     * @return string SQL condition
-     */
-    protected function buildDateConditions(array $date_query, array &$params): string
-    {
-        $conditions = [];
+	/**
+	 * Build date query conditions
+	 * @param array $date_query Date query array
+	 * @param array &$params Reference to params array
+	 * @return string SQL condition
+	 */
+	protected function buildDateConditions(array $date_query, array &$params): string
+	{
+		$conditions = [];
 
-        foreach ($date_query as $query) {
-            if (!is_array($query)) continue;
+		foreach ($date_query as $query) {
+			if (!is_array($query)) continue;
 
-            $column = $query['column'] ?? 'updated_at';
-            $year = $query['year'] ?? null;
-            $month = $query['month'] ?? null;
-            $day = $query['day'] ?? null;
-            $after = $query['after'] ?? null;
-            $before = $query['before'] ?? null;
+			$column = $query['column'] ?? 'updated_at';
+			$year = $query['year'] ?? null;
+			$month = $query['month'] ?? null;
+			$day = $query['day'] ?? null;
+			$after = $query['after'] ?? null;
+			$before = $query['before'] ?? null;
 
-            if ($year) {
-                $params[] = $year;
-                $conditions[] = "YEAR(ct.{$column}) = %d";
-            }
+			if ($year) {
+				$params[] = $year;
+				$conditions[] = "YEAR(ct.{$column}) = %d";
+			}
 
-            if ($month) {
-                $params[] = $month;
-                $conditions[] = "MONTH(ct.{$column}) = %d";
-            }
+			if ($month) {
+				$params[] = $month;
+				$conditions[] = "MONTH(ct.{$column}) = %d";
+			}
 
-            if ($day) {
-                $params[] = $day;
-                $conditions[] = "DAY(ct.{$column}) = %d";
-            }
+			if ($day) {
+				$params[] = $day;
+				$conditions[] = "DAY(ct.{$column}) = %d";
+			}
 
-            if ($after) {
-                $params[] = $after;
-                $conditions[] = "ct.{$column} > %s";
-            }
+			if ($after) {
+				$params[] = $after;
+				$conditions[] = "ct.{$column} > %s";
+			}
 
-            if ($before) {
-                $params[] = $before;
-                $conditions[] = "ct.{$column} < %s";
-            }
-        }
+			if ($before) {
+				$params[] = $before;
+				$conditions[] = "ct.{$column} < %s";
+			}
+		}
 
-        return !empty($conditions) ? '(' . implode(' AND ', $conditions) . ')' : '';
-    }
+		return !empty($conditions) ? '(' . implode(' AND ', $conditions) . ')' : '';
+	}
 
-    /**
-     * Build ORDER BY clause
-     * @param array $args Query arguments
-     * @param string $taxonomy Taxonomy type
-     * @return string ORDER BY clause
-     */
-    protected function buildOrderBy(array $args, string $taxonomy): string
-    {
-        $orderby = $args['orderby'] ?? 'name';
-        $order = $args['order'] ?? 'ASC';
+	/**
+	 * Build ORDER BY clause
+	 * @param array $args Query arguments
+	 * @param string $taxonomy Taxonomy type
+	 * @return string ORDER BY clause
+	 */
+	protected function buildOrderBy(array $args, string $taxonomy): string
+	{
+		$orderby = $args['orderby'] ?? 'name';
+		$order = $args['order'] ?? 'ASC';
 
-        // Validate order direction
-        if (!in_array($order, ['ASC', 'DESC'])) {
-            $order = 'ASC';
-        }
+		// Validate order direction
+		if (!in_array($order, ['ASC', 'DESC'])) {
+			$order = 'ASC';
+		}
 
-        if (str_contains($orderby, 'RAND')) {
-            return "ORDER BY {$orderby}";
-        }
+		if (str_contains($orderby, 'RAND')) {
+			return "ORDER BY {$orderby}";
+		}
 
-        switch ($orderby) {
-            case 'name':
-                return "ORDER BY ct.name {$order}";
+		switch ($orderby) {
+			case 'name':
+				return "ORDER BY ct.name {$order}";
 
-            case 'count':
-                return "ORDER BY tt.count {$order}";
+			case 'count':
+				return "ORDER BY tt.count {$order}";
 
-            case 'term_id':
-            case 'id':
-                return "ORDER BY ct.term_id {$order}";
+			case 'term_id':
+			case 'id':
+				return "ORDER BY ct.term_id {$order}";
 
-            case 'slug':
-                return "ORDER BY t.slug {$order}";
+			case 'slug':
+				return "ORDER BY t.slug {$order}";
 
-            case 'date':
-            case 'updated':
-                return "ORDER BY ct.updated_at {$order}";
+			case 'date':
+			case 'updated':
+				return "ORDER BY ct.updated_at {$order}";
 
-            default:
-                // Check if it's a custom field in our table
-                $custom_fields = $this->getCustomTableFields($taxonomy);
-                if (isset($custom_fields[$orderby])) {
-                    return "ORDER BY ct.{$orderby} {$order}";
-                }
+			default:
+				// Check if it's a custom field in our table
+				$custom_fields = $this->getCustomTableFields($taxonomy);
+				if (isset($custom_fields[$orderby])) {
+					return "ORDER BY ct.{$orderby} {$order}";
+				}
 
-                // Default to name
-                return "ORDER BY ct.name {$order}";
-        }
-    }
+				// Default to name
+				return "ORDER BY ct.name {$order}";
+		}
+	}
 
-    /**
-     * Get custom table fields for a taxonomy
-     * @param string $taxonomy Taxonomy type
-     * @return array Field definitions
-     */
-    protected function getCustomTableFields(string $taxonomy): array
-    {
-        return jvbContentTaxonomiesTableFields($taxonomy)['fields'] ?? [];
-    }
+	/**
+	 * Get custom table fields for a taxonomy
+	 * @param string $taxonomy Taxonomy type
+	 * @return array Field definitions
+	 */
+	protected function getCustomTableFields(string $taxonomy): array
+	{
+		return jvbContentTaxonomiesTableFields($taxonomy)['fields'] ?? [];
+	}
+
+	/**
+	 * Get available feed types (for block editor)
+	 * Returns structured data about content types that can be shown in feed
+	 */
+	public function getFeedTypes(WP_REST_Request $request): WP_REST_Response
+	{
+		// Check HTTP cache
+		$cache_check = $this->checkHeaders($request, ['feed_types']);
+		if ($cache_check) {
+			return $cache_check;
+		}
+
+		$feedTypes = $this->buildFeedTypesConfig();
+
+		$response = new WP_REST_Response($feedTypes);
+		return $this->addCacheHeaders($response);
+	}
+
+	public function getFeedTypesConfig():array
+	{
+		return $this->buildFeedTypesConfig();
+	}
+	/**
+	 * Build feed types configuration from Features
+	 */
+	protected function buildFeedTypesConfig(): array
+	{
+		if (!$this->checker) {
+			$this->checker = Checker::getInstance();
+		}
+		return $this->cache->remember(
+			'contentTypes',
+			function () {
+				$config = [];
+
+				// Get content types with show_feed
+				$contentTypes = Features::getTypesWithFeature('show_feed', 'content');
+				foreach ($contentTypes as $slug) {
+					$contentConfig = JVB_CONTENT[$slug] ?? null;
+					if (!$contentConfig) continue;
+
+					$config[$slug] = [
+						'type' => 'content',
+						'singular' => $contentConfig['singular'] ?? ucfirst($slug),
+						'plural' => $contentConfig['plural'] ?? ucfirst($slug) . 's',
+						'icon' => $slug,
+						'taxonomies' => $this->checker->getTaxonomiesForContent($slug),
+					];
+				}
+
+				// Get taxonomies with show_feed (content taxonomies)
+				$taxonomies = Features::getTypesWithFeature('show_feed', 'taxonomy');
+				foreach ($taxonomies as $slug) {
+					$taxConfig = JVB_TAXONOMY[$slug] ?? null;
+					if (!$taxConfig || !($taxConfig['is_content'] ?? false)) {
+						continue;
+					}
+
+					$config[$slug] = [
+						'type' => 'taxonomy',
+						'singular' => $taxConfig['singular'] ?? ucfirst($slug),
+						'plural' => $taxConfig['plural'] ?? ucfirst($slug) . 's',
+						'icon' => $slug,
+						'taxonomies' => [], // Content taxonomies don't have sub-taxonomies
+						'for_content' => $taxConfig['for_content'] ?? [],
+					];
+				}
+
+				return $config;
+			});
+	}
 }
diff --git a/inc/rest/routes/LoginRoutes.php b/inc/rest/routes/LoginRoutes.php
index ec82f0d..ad47bff 100644
--- a/inc/rest/routes/LoginRoutes.php
+++ b/inc/rest/routes/LoginRoutes.php
@@ -134,14 +134,15 @@
 
 	public function handleLogin(WP_REST_Request $request): WP_REST_Response
 	{
+		$data = $request->get_json_params();
 		// Verify Turnstile
-		if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
+		if (!$this->verifyTurnstile($data['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');
+		$username = sanitize_email($data['user_email'] ?? '');
+		$password = $data['user_password'] ?? '';
+		$remember = (bool)($data['remember_me'] ?? false);
 
 		// Check for account lockout
 		$lockout = $this->checkAccountLockout($username);
@@ -638,18 +639,6 @@
 		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
diff --git a/inc/rest/routes/MagicLinkRoutes.php b/inc/rest/routes/MagicLinkRoutes.php
index fc12edc..a422c6f 100644
--- a/inc/rest/routes/MagicLinkRoutes.php
+++ b/inc/rest/routes/MagicLinkRoutes.php
@@ -104,8 +104,14 @@
 	 */
 	public function sendMagicLink(WP_REST_Request $request): WP_REST_Response
 	{
+		$data = $request->get_json_params();
+
+		// Verify Turnstile
+		if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) {
+			return $this->error('Security verification failed', 'turnstile_failed', 403);
+		}
 		$email = sanitize_email($request->get_param('email')??$request->get_param('user_email')??'');
-		$type = sanitize_text_field($request->get_param('type'));
+		$type = sanitize_text_field($request->get_param('type')) ?? MagicLinkManager::TYPE_LOGIN;
 		$context = $request->get_param('context') ?? [];
 
 		error_log('SendMagicLink request: '.print_r($email, true));
@@ -122,12 +128,16 @@
 
 		// Check if email exists
 		$exists = email_exists($email);
-		if (!$exists) {
-
+		if ($type === MagicLinkManager::TYPE_LOGIN && !$exists) {
 			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'User account not found'
-			], 400);
+				'success' => true,
+				'message' => 'Invalid email address'
+			]);
+		}
+
+		if ($type === MagicLinkManager::TYPE_SIGNUP && $exists) {
+			// Redirect to login instead
+			$type = MagicLinkManager::TYPE_LOGIN;
 		}
 
 		// Send the magic link
diff --git a/inc/rest/routes/QueueRoutes.php b/inc/rest/routes/QueueRoutes.php
index a5070db..11b409d 100644
--- a/inc/rest/routes/QueueRoutes.php
+++ b/inc/rest/routes/QueueRoutes.php
@@ -1,7 +1,9 @@
 <?php
 namespace JVBase\rest\routes;
 
+use Exception;
 use JVBase\JVB;
+use JVBase\managers\CacheManager;
 use JVBase\rest\RestRouteManager;
 use WP_REST_Request;
 use WP_REST_Response;
@@ -92,20 +94,13 @@
 		$ids = $request->get_param('ids');
 		$limit = intval($request->get_param('limit'));
 
-		// User-specific caching (keep this)
-		$user_queue_timestamp = $this->getUserQueueTimestamp($user_id);
-
-		$if_modified_since = $request->get_header('If-Modified-Since');
-		if ($if_modified_since) {
-			$if_modified_timestamp = strtotime($if_modified_since);
-			if ($user_queue_timestamp <= $if_modified_timestamp) {
-				return new WP_REST_Response(null, 304);
-			}
+		// Use base class user-specific header checking
+		// This checks both 'queue' and 'user_{$user_id}' timestamps
+		$cache_check = $this->checkUserHeaders($request, $user_id, 'queue');
+		if ($cache_check) {
+			return $cache_check; // Returns 304 Not Modified
 		}
 
-		header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $user_queue_timestamp) . ' GMT');
-		header('Cache-Control: private, max-age=30');
-
 		global $wpdb;
 		$table = $wpdb->prefix . $this->table;
 
@@ -135,19 +130,22 @@
 
 		$operations = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
 
-		// Format operations with improved data structure
+		// Format operations
 		foreach ($operations as &$op) {
 			$op = $this->formatOperation($op);
 		}
 
-		return new WP_REST_Response([
+		$response = new WP_REST_Response([
 			'items' => $operations,
 			'total' => count($operations),
-			'timestamp' => date('c'), // ISO format
+			'timestamp' => date('c'),
 			'has_more' => count($operations) === $limit,
 			'queue_stats' => $this->getQueueStats($user_id),
-			'server_time' => date('c') // Helpful for frontend time sync
+			'server_time' => date('c')
 		]);
+
+		// Add cache headers (ETag, Last-Modified)
+		return $this->addCacheHeaders($response);
 	}
 
 
@@ -230,16 +228,6 @@
 	}
 
 	/**
-	 * Update user's queue timestamp when any operation changes
-	 * This should be called whenever an operation status changes
-	 */
-	public function updateUserQueueTimestamp(int $user_id): void
-	{
-		$key = "{$user_id}_queue_timestamp";
-		$this->cache->set($key, time());
-	}
-
-	/**
 	 * Convert MySQL datetime to ISO 8601 timestamp with proper timezone
 	 */
 	protected function formatTimestamp(?string $mysql_datetime): ?string
@@ -249,14 +237,19 @@
 		}
 
 		try {
-			// Create DateTime object from MySQL datetime (assuming UTC storage)
-			$date = new DateTime($mysql_datetime, new DateTimeZone('UTC'));
+			// Get WordPress timezone - dates are stored in this timezone
+			$wp_timezone = wp_timezone();
 
-			// Return ISO 8601 format with UTC timezone indicator
-			return $date->format('c'); // e.g., "2025-07-23T22:57:35+00:00"
+			// Parse the datetime in WordPress timezone
+			$date = new DateTime($mysql_datetime, $wp_timezone);
+
+			// Convert to UTC for API consistency
+			$date->setTimezone(new DateTimeZone('UTC'));
+
+			// Return ISO 8601 format
+			return $date->format('c');
 
 		} catch (Exception $e) {
-			// Fallback: return null if datetime is invalid
 			return null;
 		}
 	}
@@ -292,24 +285,6 @@
 		return $base_title;
 	}
 
-	/**
-	 * Get user's queue last update timestamp from cache
-	 */
-	protected function getUserQueueTimestamp(int $user_id): int
-	{
-		$key = "{$user_id}_queue_timestamp";
-
-		// Use CacheManager for consistency
-		$timestamp = $this->cache->get($key);
-
-		if ($timestamp === false) {
-			$timestamp = time();
-			$this->cache->set($key, $timestamp);
-		}
-
-		return $timestamp;
-	}
-
     /**
      * Update operation status (dismiss or retry)
      *
@@ -321,7 +296,9 @@
 		$data = $request->get_json_params();
 		$ids = $data['ids'] ?? [];
 		$action = $data['action'] ?? '';
-		$user_id = get_current_user_id();
+
+		$user_id = (int)$data['user'];
+
 
 		// Validate input
 		if (empty($ids) || !is_array($ids)) {
@@ -362,19 +339,13 @@
 		$result = $this->processQueueAction($action, $valid_operations, $user_id);
 
 		if ($result['success']) {
-			$this->invalidateUserCache($user_id);
+			// Update timestamp for this user's queue
+			CacheManager::updateTimestamp("user_{$user_id}");
 		}
 
 		return new WP_REST_Response($result);
 	}
 
-	protected function invalidateUserCache(int $user_id): void
-	{
-		$key = "{$user_id}_queue_timestamp";
-		$this->cache->invalidate($key);
-		$this->cache->set($key, time());
-	}
-
 	protected function processQueueAction(string $action, array $operations, int $user_id): array
 	{
 		global $wpdb;
diff --git a/inc/rest/routes/TermRoutes.php b/inc/rest/routes/TermRoutes.php
index 7526566..1f8c726 100644
--- a/inc/rest/routes/TermRoutes.php
+++ b/inc/rest/routes/TermRoutes.php
@@ -5,6 +5,7 @@
 use JVBase\rest\RestRouteManager;
 use JVBase\managers\TaxonomyRelationships;
 use JVBase\managers\UserTermsManager;
+use JVBase\utility\Features;
 use WP_REST_Request;
 use WP_REST_Response;
 use Exception;
@@ -180,9 +181,8 @@
     public function handleTermSelectionRequest(WP_REST_Request $request):WP_REST_Response
     {
 		$data = $request->get_params();
-		$taxonomy = jvbCheckBase($data['taxonomy']);
+		$taxonomy = jvbCheckBase($data['taxonomy'])??'';
 
-		error_log('Term Request Data for '.$taxonomy.': '.print_r($data, true));
 		// Check HTTP cache headers
 		$cache_check = $this->checkHeaders($request, $taxonomy);
 		if ($cache_check) {
@@ -190,6 +190,11 @@
 			return $cache_check;
 		}
 
+		// Handle batch request (multiple taxonomies)
+		if (str_contains($taxonomy, ',')) {
+			return $this->handleBatchTermRequest($taxonomy, $data, $request);
+		}
+
 		if (array_key_exists('termIDs', $data)) {
 			$args = [
 				'taxonomy'	=> $taxonomy,
@@ -223,9 +228,10 @@
 
         $taxonomy = BASE.$request->get_param('taxonomy');
         $search = $request->get_param('search');
-        $parent = (int)$request->get_param('parent');
-        $page = max(1, (int)$request->get_param('page'));
-        $per_page = max(20, (int)$request->get_param('per_page'));
+
+		$parent = (int)$data['parent']??0;
+		$page = max(1, (int)($data['page']??1));
+		$per_page = 25;
 
         if (!taxonomy_exists($taxonomy)) {
             return new WP_REST_Response([
@@ -371,9 +377,9 @@
 
         $key = $this->cache->generateKey($args);
         $cache = $this->cache->get($key);
-		$cache = false;
+
         if ($cache) {
-            $response = new WP_ReST_Response($cache);
+            $response = new WP_REST_Response($cache);
 			return $this->addCacheHeaders($response);
         }
 
@@ -406,48 +412,110 @@
 		return $this->addCacheHeaders($response);
     }
 
+	protected function handleBatchTermRequest(string $taxonomy, array $data, WP_REST_Request $request):WP_REST_Response
+	{
+		$taxonomies = array_map('trim', explode(',', $taxonomy));
+		$all_terms = [];
+		$parent = (int)$data['parent']??0;
+		$page = max(1, (int)($data['page']??1));
+		$per_page = 25;
+		$mainArgs = [
+			'hide_empty'=> false,
+			'parent'	=> $parent,
+			'number'	=> $per_page,
+			'orderby'	=> 'name',
+			'offset'	=> ($page -1) * $per_page,
+		];
+
+		foreach ($taxonomies as $taxonomy) {
+			if (!taxonomy_exists(BASE.$taxonomy)) {
+				continue;
+			}
+			$args = $mainArgs;
+			$args['taxonomy'] = BASE.$taxonomy;
+
+			$all_terms = array_merge($all_terms, $this->formatTerms($args, $taxonomy));
+		}
+
+		$response = [
+			'items'	=> $all_terms,
+			'pagination'=> [
+				'page' => $page,
+				'per_page'=> $per_page,
+				'has_more' => true,
+			]
+		];
+
+		$response = new WP_REST_Response($response);
+		return $this->addCacheHeaders($response);
+	}
+
+
     /**
      * @param array $args
      * @param string $taxonomy
      *
      * @return array
      */
-    protected function formatTerms(array $args, string $taxonomy):array
-    {
-        $terms = get_terms($args);
+	protected function formatTerms(array $args, string $taxonomy): array
+	{
+		return $this->cache->remember(
+			$this->cache->generateKey($args),
+			function() use ($args, $taxonomy) {
+				$terms = get_terms($args);
 
-        if (is_wp_error($terms)) {
-            return [];
-        }
+				if (is_wp_error($terms)) {
+					return [];
+				}
+
+				$formatted_terms = [];
+				foreach ($terms as $term) {
+					$formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, true);
+				}
+
+				return $formatted_terms;
+			}
+		);
 
 
+	}
 
-        $formatted_terms = [];
-        foreach ($terms as $term) {
-            // Check for children explicitly
-            $children_args = [
-                'taxonomy' => $taxonomy,
-                'parent' => $term->term_id,
-                'fields' => 'count',
-                'hide_empty' => false
-            ];
-            $count = wp_count_terms($children_args);
-            $has_children = !is_wp_error($count) && $count > 0;
+	/**
+	 * Format a single term with caching
+	 *
+	 * @param object $term WP_Term object
+	 * @param string $taxonomy Full taxonomy name
+	 *
+	 * @return array Formatted term data
+	 */
+	protected function formatSingleTerm(object $term, string $taxonomy): array
+	{
+		$cache_key = "{$term->term_id}_{$taxonomy}";
 
-            $formatted_terms[] = [
-                'id' => $term->term_id,
-                'name' => $term->name,
-				'slug'	=> $term->slug,
-                'parent' => $term->parent,
-                'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
-                'hasChildren' => $has_children,
-				'taxonomy'	=> $term->taxonomy,
-				'count'		=> $term->count,
-            ];
-        }
+		return $this->cache->remember($cache_key, function() use ($term, $taxonomy) {
+			$data = [
+				'id' => $term->term_id,
+				'name' => $term->name,
+				'slug' => $term->slug,
+				'parent' => $term->parent,
+				'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
+				'taxonomy' => jvbNoBase($term->taxonomy),
+				'count' => $term->count,
+			];
 
-        return $formatted_terms;
-    }
+			$children_args = [
+				'taxonomy' => $taxonomy,
+				'parent' => $term->term_id,
+				'fields' => 'count',
+				'hide_empty' => false
+			];
+			$count = wp_count_terms($children_args);
+			$data['hasChildren'] = !is_wp_error($count) && $count > 0;
+
+
+			return $data;
+		});
+	}
 
     /**
      * @param WP_REST_Request $request
@@ -498,16 +566,10 @@
         $total_terms = wp_count_terms($count_args);
 
         $formatted_terms = [];
-        foreach ($terms as $term) {
-            $formatted_terms[$term->term_id] = [
-                'id'            => $term->term_id,
-                'name'          => $term->name,
-                'parent'        => $term->parent,
-                'path'          => $this->getTermPath($term->term_id, $term->name, $taxonomy),
-                'hasChildren'   => (bool)$term->has_children,
-                'count'         => (int)$term->count
-            ];
-        }
+		foreach ($terms as $term) {
+			// Search results show path, so includeChildren = false for performance
+			$formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, false);
+		}
 
         // Calculate pagination info
         $total_pages = ceil($total_terms / $per_page);
@@ -698,15 +760,12 @@
             $formatted_terms = [];
             $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
 
-            foreach ($terms as $term) {
-                $formatted_terms[$term->term_id] = [
-                    'id' => $term->term_id,
-                    'name' => $term->name,
-                    'count' => $term->count,
-                    'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
-                    'relationship_strength' => $term->relationship_count ?? 0
-                ];
-            }
+			foreach ($terms as $term) {
+				$formatted = $this->formatSingleTerm($term, $taxonomy, false);
+				// Add relationship strength which is unique to this method
+				$formatted['relationship_strength'] = $term->relationship_count ?? 0;
+				$formatted_terms[] = $formatted;
+			}
 
             // Build response
             $total_pages = ceil($total / $per_page);
@@ -790,16 +849,10 @@
             $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
 
             // Format terms
-            $formatted_terms = [];
-            foreach ($terms as $term) {
-                $formatted_terms[$term->term_id] = [
-                    'id' => $term->term_id,
-                    'name' => $term->name,
-                    'count' => $term->count,
-                    'parent' => $term->parent,
-                    'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy)
-                ];
-            }
+			$formatted_terms = [];
+			foreach ($terms as $term) {
+				$formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, false);
+			}
 
             // Get total for pagination
             $total_args = array_merge($args, ['fields' => 'count', 'number' => '']);
@@ -842,13 +895,15 @@
         $name = sanitize_text_field($request->get_param('name'));
         $parent = (int)$request->get_param('parent') ?: 0;
 
+
         try {
             // Check if term already exists
             $existing = term_exists($name, jvbCheckBase($taxonomy), $parent);
 
             if ($existing) {
-                $term = get_term($existing['term_id'], jvbCheckBase($taxonomy));
 
+                $term = get_term($existing['term_id'], jvbCheckBase($taxonomy));
+				error_log('Existing Term: '.print_r($term, true));
                 return new WP_REST_Response([
                     'success' => false,
                     'message' => 'Term already exists',
@@ -861,7 +916,8 @@
             }
 
 
-			if (jvbSiteHasTermApproval()) {
+			if (Features::forMembership()->has('term_approval')) {
+				error_log('Term Approval required');
 				// Get approval routes instance
 				$approval_routes = JVB()->routes('approvals');
 				// Create approval request
@@ -885,13 +941,15 @@
 					]
 				];
 			} else {
+				error_log('Creating new Term: ');
 				$termID = wp_insert_term(
 					$name,
 					jvbCheckBase($taxonomy),
 					[
-						'parent'	=> absint($parent??0)
+						'parent'	=> absint($parent)
 					]
 				);
+				error_log('Result: '.print_r($termID, true));
 
 				if (is_wp_error($termID)) {
 					throw new Exception('Failed to create new term');
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 56daf36..4a6cf48 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -1330,7 +1330,9 @@
 			$used_upload_ids = [];
 
 			$content = jvbCheckBase($data['content']);
+			error_log('[processTimelineUploads]Got content: '.print_r($content, true));
 			$config = Features::getConfig($content);
+			error_log('[processTimelineUploads]Got config: '.print_r($config, true));
 			$defaultTitle = 'New '.$config['singular']. ' ';
 			foreach ($data['posts'] as $index=> $post) {
 				$title = !empty($post['fields']['post_title'])
diff --git a/inc/utility/Features.php b/inc/utility/Features.php
index d5f76bb..422bad3 100644
--- a/inc/utility/Features.php
+++ b/inc/utility/Features.php
@@ -752,6 +752,7 @@
 
 	public static function getConfig(string $slug, ?string $type = null): array
 	{
+		$slug = jvbNoBase($slug);
 		$all = ['post', 'content', 'taxonomy', 'user'];
 		$types = (!$type || !in_array($type, $all)) ? $all : [$type];
 
diff --git a/inc/utility/Image.php b/inc/utility/Image.php
new file mode 100644
index 0000000..8ecbd83
--- /dev/null
+++ b/inc/utility/Image.php
@@ -0,0 +1,88 @@
+<?php
+namespace JVBase\utility;
+
+use JVBase\managers\CacheManager;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+/**
+ * Validates configuration arrays for content types, taxonomies, and user roles
+ * Catches errors early in the registration process
+ */
+class Image
+{
+	protected ?CacheManager $cache = null;
+
+	public function __construct()
+	{
+		$this->cache = CacheManager::for('images')->connectTo('post', 'attachment');
+	}
+
+	public function formatImage(int $ID, string $start = 'tiny', string $replace = 'large', bool $addLink = true, ?string $postSlug = null):string
+	{
+		$return =  $this->cache->remember(
+			['ID' => $ID, 'start' => $start, 'replace' => $replace],
+			function() use ($ID, $start, $replace) {
+				$img = wp_get_attachment_image_src($ID, $start);
+				if (!$img) {
+					return'';
+				}
+				$img = $img[0];
+
+				$data = $this->getGallerySizes($ID, $replace);
+
+
+
+				$alt = get_post_meta($ID, '_wp_attachment_image_alt', true);
+				$alt = ($alt=='')? '' : ' alt="'.$alt.'" ';
+				return '<img width="100%" height="auto" src="'.$img.'"'.$alt.$data.' loading="lazy" decoding="async">';
+			}
+		);
+
+		$aOpen = $aClose = '';
+		if ($addLink) {
+			if (!$postSlug) {
+				global $post;
+				$postSlug = $post->post_name;
+			}
+
+			$imgPost = get_post($ID);
+			if (!$imgPost) {
+				return $return;
+			}
+			$imgSlug = $imgPost->post_name;
+			$aOpen = '<a class="open-gallery" target="_blank" rel="nofollow" data-opens="gallery-'.$postSlug.'" data-focus="'.$postSlug.'-'.$imgSlug.'">';
+			$aClose = '</a>';
+		}
+		return $aOpen.$return.$aClose;
+	}
+
+	public function getGallerySizes(int $ID, string $replace):string
+	{
+		if (!wp_get_attachment_image_src($ID)) {
+			return '';
+		}
+		if (!has_image_size($replace)) {
+			$replace = 'large';
+		}
+
+		if ($replace == 'large') {
+			return 'data-small="' .
+				wp_get_attachment_image_src($ID)[0] . '" data-medium="' .
+				wp_get_attachment_image_src($ID, 'large')[0] . '" data-full="' .
+				wp_get_attachment_image_src($ID, 'full')[0] . '"';
+		} elseif ($replace == 'medium') {
+			return 'data-small="'.
+				wp_get_attachment_image_src($ID, 'large')[0].'" data-medium="'.
+				wp_get_attachment_image_src($ID, 'large')[0].'" data-full="'.
+				wp_get_attachment_image_src($ID, 'large')[0].'"';
+		} elseif ($replace == 'thumbnail') {
+			return 'data-small="'.
+				wp_get_attachment_image_src($ID, 'medium')[0].'" data-medium="'.
+				wp_get_attachment_image_src($ID, 'medium')[0].'" data-full="'.
+				wp_get_attachment_image_src($ID, 'medium')[0].'"';
+		}
+		return '';
+	}
+}
diff --git a/inc/utility/setup.php b/inc/utility/setup.php
index ffc5413..9cbe572 100644
--- a/inc/utility/setup.php
+++ b/inc/utility/setup.php
@@ -4,3 +4,4 @@
 require(JVB_DIR.'/inc/utility/Features.php');
 require(JVB_DIR.'/inc/utility/helpers.php');
 require(JVB_DIR.'/inc/utility/Validator.php');
+require(JVB_DIR.'/inc/utility/Image.php');
diff --git a/jvb.php b/jvb.php
index 816a013..38dbb27 100644
--- a/jvb.php
+++ b/jvb.php
@@ -252,7 +252,7 @@
         'jvb-utility',
         JVB_URL.'assets/js/min/utility.min.js',
         [],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -266,7 +266,7 @@
 			'jvb-queue',
 			'jvb-data-store'
 		],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'    => 'defer',
 			'in_footer'    => true,
@@ -279,7 +279,7 @@
 			'jvb-queue',
 			'jvb-data-store'
 		],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'    => 'defer',
 			'in_footer'    => true,
@@ -294,7 +294,7 @@
 			'jvb-utility',
 			'jvb-data-store'
 		],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'    => 'defer',
 			'in_footer'    => true,
@@ -307,7 +307,7 @@
 		[
 			'jvb-a11y'
 		],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'	=> 'defer',
 			'in_footer'	=> true
@@ -320,7 +320,7 @@
         'jvb-media',
         JVB_URL.'assets/js/min/media.min.js',
         [],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer' => true,
@@ -336,7 +336,7 @@
 			'jvb-modal',
 			'jvb-a11y'
 		],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'	=> 'defer',
 			'in_footer' => true,
@@ -361,9 +361,11 @@
         JVB_URL.'assets/js/min/gallery.min.js',
         [
 			'jvb-utility',
-			'jvb-queue'
+//			'jvb-queue',
+			'jvb-modal',
+//			'jvb-swiper',
 		],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -371,10 +373,23 @@
     );
 
 	wp_register_script(
+		'jvb-swiper',
+		JVB_URL.'assets/js/min/swiper.min.js',
+		[
+			'jvb-utility',
+		],
+		'1.0.1',
+		[
+			'strategy'    => 'defer',
+			'in_footer'    => true,
+		]
+	);
+
+	wp_register_script(
 		'jvb-integrations',
 		JVB_URL.'assets/js/min/integrations.min.js',
 		[],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'	=> 'defer',
 			'in_footer'	=> true
@@ -412,7 +427,7 @@
         'jvb-page-nav',
         JVB_URL.'assets/js/min/page-nav.min.js',
         [],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -424,7 +439,7 @@
         'jvb-a11y',
         JVB_URL.'assets/js/min/a11y.min.js',
         [],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -438,7 +453,7 @@
         [
 
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -450,7 +465,7 @@
         'jvb-cache',
         JVB_URL.'assets/js/min/cache.min.js',
         [],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -461,7 +476,7 @@
 		'jvb-data-store',
 		JVB_URL.'assets/js/min/dataStore.min.js',
 		[],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'	=> 'defer',
 			'in_footer'	=> true,
@@ -475,7 +490,7 @@
         [
             'jvb-a11y'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -489,7 +504,7 @@
         [
             'jvb-a11y'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -507,7 +522,7 @@
             'jvb-utility',
 			'jvb-popup'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -526,7 +541,7 @@
             'jvb-modal',
 //            'jvb-loading'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -537,7 +552,7 @@
 		'jvb-creator',
 		JVB_URL.'assets/js/min/creator.min.js',
 		['jvb-selector'],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'    => 'defer',
 			'in_footer'		=> true
@@ -551,7 +566,7 @@
         [
             'jvb-selector'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -588,7 +603,7 @@
 			'jvb-a11y',
             'jvb-utility',
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -601,7 +616,7 @@
 			'jvb-a11y',
             'jvb-utility',
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -613,6 +628,7 @@
         'jvb-uploader',
         JVB_URL.'assets/js/min/uploader.min.js',
         [
+			'sortable-js',
 			'jvb-cache',
 			'jvb-a11y',
             'jvb-utility',
@@ -623,7 +639,7 @@
             'jvb-queue',
             'jvb-notifications'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -671,7 +687,7 @@
         [
             'jvb-utility',
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer' => true,
@@ -691,7 +707,7 @@
 			'jvb-populate-form',
 			'jvb-quill',
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -702,7 +718,7 @@
 		'jvb-populate-form',
 		JVB_URL.'assets/js/min/populate.min.js',
 		[],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'	=> 'defer',
 			'in_footer'	=> true,
@@ -714,7 +730,7 @@
 		[
 			'quill-js'
 		],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'	=> 'defer',
 			'in_footer'	=> true,
@@ -738,7 +754,7 @@
 			'jvb-view',
 			'jvb-modal'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -756,7 +772,7 @@
 			'jvb-error',
 			'jvb-populate-form'
 		],
-		'1.0.0',
+		'1.0.1',
 		[
 			'strategy'	=> 'defer',
 			'in_footer'	=> true,
@@ -772,7 +788,7 @@
             'jvb-form',
             'jvb-queue'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -788,7 +804,7 @@
             'jvb-form',
             'jvb-queue'
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -809,7 +825,7 @@
             'jvb-selector',
             'jvb-post-selector',
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -830,7 +846,7 @@
             'jvb-selector',
             'jvb-notifications',
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -851,7 +867,7 @@
             'jvb-selector',
             'jvb-tabs',
         ],
-        '1.0.0',
+        '1.0.1',
         [
             'strategy'    => 'defer',
             'in_footer'    => true,
@@ -1031,6 +1047,10 @@
 
     jvbAddScriptDependency('jvb-feed-view-script', 'jvb-queue');
     jvbAddScriptDependency('jvb-feed-view-script', 'jvb-selector');
+    jvbAddScriptDependency('jvb-feed-view-script', 'jvb-data-store');
+    jvbAddScriptDependency('jvb-feed-view-script', 'jvb-cache');
+    jvbAddScriptDependency('jvb-feed-view-script', 'jvb-a11y');
+    jvbAddScriptDependency('jvb-feed-view-script', 'jvb-utility');
     jvbAddScriptDependency('jvb-feed-view-script', 'jvb-gallery');
 //    jvbAddScriptDependency('jvb-feed-view-script', 'jvb-loading');
 	jvbAddScriptDependency('jvb-forms-view-script', 'jvb-queue');
diff --git a/src/feed/block.json b/src/feed/block.json
index 60f810b..3fec37e 100644
--- a/src/feed/block.json
+++ b/src/feed/block.json
@@ -50,10 +50,8 @@
             "message": "This is a notice!"
         }
     },
-    "render": "file:./render.php",
     "editorScript": "file:./index.js",
     "editorStyle": "file:./index.css",
     "style": "file:./style-index.css",
-    "viewScript": "file:./view.js",
-    "dependencies": ["jvb-taxonomy-selector", "jvb-client-queue"]
+    "viewScript": "file:./view.js"
 }
diff --git a/src/feed/edit.js b/src/feed/edit.js
index d6bc6ff..2a5228d 100644
--- a/src/feed/edit.js
+++ b/src/feed/edit.js
@@ -1,114 +1,256 @@
-import { __ } from '@wordpress/i18n';
-import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
-import { PanelBody, TextControl, ToggleControl, SelectControl, CheckboxControl } from '@wordpress/components';
+/**
+ * Feed Block - Edit Component
+ * Fetches available feed types from /jvb/v1/feed/types
+ * Allows configuration of content types and inherit query setting
+ */
+
 import { useEffect, useState } from '@wordpress/element';
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import {
+	PanelBody,
+	CheckboxControl,
+	ToggleControl,
+	Spinner,
+	Notice
+} from '@wordpress/components';
 import apiFetch from '@wordpress/api-fetch';
-import './editor.scss';
+import { __ } from '@wordpress/i18n';
 
 export default function Edit({ attributes, setAttributes }) {
-    const blockProps = useBlockProps();
-    const [availableTypes, setAvailableTypes] = useState({});
+	const [feedTypes, setFeedTypes] = useState(null);
+	const [loading, setLoading] = useState(true);
+	const [error, setError] = useState(null);
 
-    useEffect(() => {
-        apiFetch({ path: '/jvb/v1/types' }).then(types => {
-            setAvailableTypes(JSON.parse(types));
-        });
-    }, []);
+	const blockProps = useBlockProps({
+		className: 'feed-block-editor'
+	});
 
-    return (
-        <>
-            <InspectorControls>
-                <PanelBody title={__('Feed Settings', 'jvb')}>
-                    <TextControl
-                        label={__('Title', 'jvb')}
-                        value={attributes.title}
-                        onChange={(title) => setAttributes({ title })}
-                    />
+	/**
+	 * Fetch available feed types on component mount
+	 */
+	useEffect(() => {
+		apiFetch({
+			path: '/jvb/v1/feed/types',
+			headers: {
+				'If-Modified-Since': localStorage.getItem('feed_types_modified'),
+			}
+		})
+			.then(types => {
+				setFeedTypes(types);
+				setLoading(false);
 
-                    <ToggleControl
-                        label={__('Inherit Current Query', 'jvb')}
-                        help={__('Inherit filters from the current archive or taxonomy query', 'jvb')}
-                        checked={attributes.inheritQuery}
-                        onChange={(inheritQuery) => setAttributes({ inheritQuery })}
-                    />
+				// Store Last-Modified for future requests
+				// (apiFetch doesn't expose response headers easily,
+				// but the server will handle 304s)
 
-                    {!attributes.inheritQuery && (
-                        <div className="feed-content-types">
-                            <p className="components-base-control__label">
-                                {__('Content Types', 'jvb')}
-                            </p>
-                            <div className="checkbox-list">
-                                {Object.entries(availableTypes).map(([id, label]) => (
-                                    <CheckboxControl
-                                        key={id}
-                                        label={label}
-                                        checked={attributes.contentTypes.includes(id)}
-                                        onChange={(isChecked) => {
-                                            const newTypes = isChecked
-                                                ? [...attributes.contentTypes, id]
-                                                : attributes.contentTypes.filter(t => t !== id);
-                                            setAttributes({contentTypes: newTypes});
-                                        }}
-                                    />
-                                ))}
-                            </div>
-                            <div className="select-all-wrapper">
-                                <CheckboxControl
-                                    label={__('Select All', 'jvb')}
-                                    checked={attributes.contentTypes.length === Object.keys(availableTypes).length}
-                                    onChange={(isChecked) => {
-                                        setAttributes({
-                                            contentTypes: isChecked ? Object.keys(availableTypes) : []
-                                        });
-                                    }}
-                                />
-                            </div>
-                        </div>
-                    )}
+				// Initialize contentTypes if not set and not inheriting
+				if (!attributes.contentTypes && !attributes.inheritQuery) {
+					const firstType = Object.keys(types)[0];
+					if (firstType) {
+						setAttributes({ contentTypes: [firstType] });
+					}
+				}
+			})
+			.catch(err => {
+				console.error('Error loading feed types:', err);
+				setError(err.message);
+				setLoading(false);
+			});
+	}, [attributes.inheritQuery]);
 
-                    <SelectControl
-                        label={__('Items Per Page', 'jvb')}
-                        value={attributes.itemsPerPage}
-                        options={[
-                            {label: '12', value: 12},
-                            {label: '24', value: 24},
-                            {label: '36', value: 36}
-                        ]}
-                        onChange={(itemsPerPage) => setAttributes({itemsPerPage: parseInt(itemsPerPage)})}
-                    />
+	/**
+	 * Toggle a content type in the selection
+	 */
+	const toggleContentType = (slug, checked) => {
+		const currentTypes = attributes.contentTypes || [];
 
-                    <SelectControl
-                        label={__('Default Order', 'jvb')}
-                        value={attributes.defaultOrder}
-                        options={[
-                            {label: __('Newest First', 'jvb'), value: 'date_desc' },
-                            { label: __('Oldest First', 'jvb'), value: 'date_asc' },
-                            { label: __('Random', 'jvb'), value: 'random' }
-                        ]}
-                        onChange={(defaultOrder) => setAttributes({ defaultOrder })}
-                    />
-                </PanelBody>
-            </InspectorControls>
+		const newTypes = checked
+			? [...currentTypes, slug]
+			: currentTypes.filter(t => t !== slug);
 
-            <div {...blockProps}>
-                <div className="feed-block-preview">
-                    <h2>{attributes.title}</h2>
-                    <div className="feed-filters">
-                        <div className="filter-preview">
-                            {attributes.contentTypes.map(type => (
-                                <span key={type} className="content-type-badge">
-                                    {availableTypes[type]}
-                                </span>
-                            ))}
-                        </div>
-                    </div>
-                    <div className="feed-grid-placeholder">
-                        {[...Array(6)].map((_, i) => (
-                            <div key={i} className="grid-item-placeholder" />
-                        ))}
-                    </div>
-                </div>
-            </div>
-        </>
-    );
+		setAttributes({ contentTypes: newTypes });
+	};
+
+	/**
+	 * Get friendly label for content type
+	 */
+	const getTypeLabel = (slug, config) => {
+		return `${config.plural} (${config.type})`;
+	};
+
+	/**
+	 * Group types by category for better UX
+	 */
+	const groupedTypes = feedTypes ? {
+		content: Object.entries(feedTypes)
+			.filter(([_, config]) => config.type === 'content'),
+		taxonomy: Object.entries(feedTypes)
+			.filter(([_, config]) => config.type === 'taxonomy')
+	} : { content: [], taxonomy: [] };
+
+	return (
+		<div {...blockProps}>
+			<InspectorControls>
+				<PanelBody
+					title={__('Feed Settings', 'jvb')}
+					initialOpen={true}
+				>
+					<ToggleControl
+						label={__('Inherit from Page Context', 'jvb')}
+						help={
+							attributes.inheritQuery
+								? __('Feed will adapt to the current page (profile, taxonomy, etc.)', 'jvb')
+								: __('Manually select content types to display', 'jvb')
+						}
+						checked={attributes.inheritQuery}
+						onChange={(value) => setAttributes({ inheritQuery: value })}
+					/>
+
+					{!attributes.inheritQuery && (
+						<>
+							{loading && (
+								<div style={{ textAlign: 'center', padding: '20px' }}>
+									<Spinner />
+									<p>{__('Loading feed types...', 'jvb')}</p>
+								</div>
+							)}
+
+							{error && (
+								<Notice status="error" isDismissible={false}>
+									{__('Error loading feed types: ', 'jvb')} {error}
+								</Notice>
+							)}
+
+							{!loading && !error && feedTypes && (
+								<>
+									{groupedTypes.content.length > 0 && (
+										<>
+											<h4>{__('Content Types', 'jvb')}</h4>
+											{groupedTypes.content.map(([slug, config]) => (
+												<CheckboxControl
+													key={slug}
+													label={getTypeLabel(slug, config)}
+													checked={
+														attributes.contentTypes?.includes(slug) || false
+													}
+													onChange={(checked) =>
+														toggleContentType(slug, checked)
+													}
+													help={
+														config.taxonomies?.length > 0
+															? `Filters: ${config.taxonomies.join(', ')}`
+															: null
+													}
+												/>
+											))}
+										</>
+									)}
+
+									{groupedTypes.taxonomy.length > 0 && (
+										<>
+											<h4 style={{ marginTop: '20px' }}>
+												{__('Content Taxonomies', 'jvb')}
+											</h4>
+											<p style={{ fontSize: '12px', color: '#757575' }}>
+												{__('These are collections that group other content', 'jvb')}
+											</p>
+											{groupedTypes.taxonomy.map(([slug, config]) => (
+												<CheckboxControl
+													key={slug}
+													label={getTypeLabel(slug, config)}
+													checked={
+														attributes.contentTypes?.includes(slug) || false
+													}
+													onChange={(checked) =>
+														toggleContentType(slug, checked)
+													}
+													help={
+														config.for_content?.length > 0
+															? `Contains: ${config.for_content.join(', ')}`
+															: null
+													}
+												/>
+											))}
+										</>
+									)}
+
+									{!attributes.contentTypes?.length && (
+										<Notice status="warning" isDismissible={false}>
+											{__('Please select at least one content type', 'jvb')}
+										</Notice>
+									)}
+								</>
+							)}
+						</>
+					)}
+				</PanelBody>
+
+				<PanelBody
+					title={__('Display Settings', 'jvb')}
+					initialOpen={false}
+				>
+					<ToggleControl
+						label={__('Show Gallery View', 'jvb')}
+						help={__('Enable lightbox for images', 'jvb')}
+						checked={attributes.enableGallery || false}
+						onChange={(value) =>
+							setAttributes({ enableGallery: value })
+						}
+					/>
+				</PanelBody>
+			</InspectorControls>
+
+			<div className="feed-block-placeholder">
+				<div className="feed-block-icon">
+					<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
+						<rect x="3" y="3" width="7" height="7" fill="currentColor" opacity="0.3" />
+						<rect x="13" y="3" width="7" height="7" fill="currentColor" opacity="0.3" />
+						<rect x="3" y="13" width="7" height="7" fill="currentColor" opacity="0.3" />
+						<rect x="13" y="13" width="7" height="7" fill="currentColor" opacity="0.3" />
+					</svg>
+				</div>
+
+				<h3>{__('Feed Block', 'jvb')}</h3>
+
+				{attributes.inheritQuery ? (
+					<p className="feed-block-description">
+						{__('📍 Inheriting from page context', 'jvb')}
+					</p>
+				) : (
+					<div className="feed-block-description">
+						{attributes.contentTypes?.length > 0 ? (
+							<>
+								<p><strong>{__('Showing:', 'jvb')}</strong></p>
+								<ul style={{
+									listStyle: 'none',
+									padding: '0',
+									margin: '8px 0'
+								}}>
+									{attributes.contentTypes.map(type => {
+										const config = feedTypes?.[type];
+										return (
+											<li key={type} style={{
+												padding: '4px 0',
+												color: '#2271b1'
+											}}>
+												✓ {config?.plural || type}
+											</li>
+										);
+									})}
+								</ul>
+							</>
+						) : (
+							<p style={{ color: '#d63638' }}>
+								{__('⚠️  No content types selected', 'jvb')}
+							</p>
+						)}
+					</div>
+				)}
+
+				<p className="feed-block-note">
+					{__('Feed will be displayed on the frontend', 'jvb')}
+				</p>
+			</div>
+		</div>
+	);
 }
diff --git a/src/feed/style.scss b/src/feed/style.scss
index ed5cff5..b40d457 100644
--- a/src/feed/style.scss
+++ b/src/feed/style.scss
@@ -1,168 +1,690 @@
+//.feed-block {
+//	max-width: var(--full);
+//	margin: 0 auto;
+//
+//	&:target {
+//		scroll-snap-margin-top: 5rem;
+//		scroll-margin-top: 5rem;
+//		outline: 0;
+//		border-radius: 0;
+//		padding: 0;
+//
+//		.feed-item {
+//			outline: double var(--pink-0);
+//		}
+//	}
+//}
+//
+//.loading .feed-block {
+//	opacity: .7;
+//}
+//
+//.label {
+//	display: flex;
+//	align-items: center;
+//	gap: .25rem;
+//	font-size: .9rem;
+//	a:hover {
+//		color: var(--pink-0);
+//	}
+//}
+//
+///** Filters Form **/
+//.feed-filters {
+//	margin: 2rem 0;
+//	position: sticky;
+//	top: 3rem;
+//	z-index: 15;
+//	background: var(--overlay-heavy);
+//	padding: .25rem 3rem;
+//	details[open] summary {
+//		background-color: var(--overlay);
+//	}
+//	summary {
+//		justify-content: flex-start;
+//
+//		> * {
+//			order: 3;
+//		}
+//		.label {
+//			order: 1;
+//		}
+//		.filter-label {
+//			order: 2;
+//		}
+//		&::after {
+//			order: 4;
+//		}
+//		#favourites + label {
+//			margin-left: auto;
+//		}
+//		#favourites + label:hover,
+//		#favourites:checked + label {
+//			border-color: var(--pink-0);
+//			background-color: var(--pink-0);
+//			color: var(--white);
+//
+//		}
+//		#favourites:checked + label {
+//			animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+//		}
+//	}
+//
+//	details[open],
+//	summary:hover {
+//		background-color: var(--overlay-heavy);
+//	}
+//
+//	&:has(#favourites) {
+//		summary::after {
+//			margin-left: 1rem;
+//		}
+//	}
+//}
+//summary > * {
+//	order: 3;
+//}
+//summary .label {
+//	order: 1;
+//}
+//.filter-label {
+//	display: inline-block;
+//	vertical-align: middle;
+//	height: 1.3em;
+//	order: 2;
+//	margin: 0;
+//	padding: 0;
+//
+//	li {
+//		list-style: none;
+//		height: 0;
+//		overflow: hidden;
+//		&.active {
+//			height: 100%;
+//		}
+//	}
+//}
+//.filter-group {
+//	display: flex;
+//	align-items: center;
+//	gap: .5rem;
+//	flex-wrap: wrap;
+//	label {
+//		font-weight: 100;
+//	}
+//	&:has(.order-by) {
+//		justify-content: space-between;
+//	}
+//	.order-by,
+//	.order-direction {
+//		display: flex;
+//		flex-wrap: wrap;
+//		gap: .5rem;
+//		flex: 1;
+//		justify-content: flex-start;
+//		.label {
+//			width: 100%;
+//		}
+//
+//		label:has(.label) {
+//			padding: 0 .35rem;
+//			gap: .5rem;
+//			.label {
+//				font-size: 1rem;
+//			}
+//		}
+//	}
+//}
+//.filter-group >.label {
+//	width: 100%;
+//}
+//
+///**
+//Feed Grid
+// */
+//.feed-grid {
+//	display: grid;
+//	grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+//	gap: .5rem;
+//	margin-bottom: 2rem;
+//	padding: 0 4rem;
+//	--delay: 0s;
+//	--increase: .1s;
+//}
+//.feed-empty-state {
+//	grid-column: 1/-1;
+//	text-align: center;
+//	padding: 2rem;
+//	background: var(--base-100);
+//	border-radius: var(--innerRadius);
+//	margin: 0 auto;
+//	max-width: 600px;
+//}
+///**
+//Placeholders
+// */
+//.placeholder {
+//	aspect-ratio: 1;
+//	background: var(--base);
+//	border: 1rem solid var(--base-50);
+//	border-radius: 1rem;
+//	display: flex;
+//	justify-content: center;
+//	align-items: center;
+//
+//	.icon {
+//		--w: 50%;
+//		color: var(--base-200);
+//		animation: dance 2.5s ease-in-out infinite;
+//
+//	}
+//}
+//
+///**
+//Feed Items
+// */
+//.feed-item {
+//	position: relative;
+//	border-radius: 0.5rem;
+//	overflow: hidden;
+//	background: var(--base-50);
+//	box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+//	opacity: 0;
+//	transition: opacity var(--transition-base) var(--delay);
+//	height: fit-content;
+//	padding: 0;
+//
+//	img {
+//		opacity: .7;
+//		filter: grayscale(.5) sepia(.3) blur(7px);
+//	}
+//	&[data-loaded=true] {
+//		img {
+//			opacity: 1;
+//			filter: none;
+//		}
+//	}
+//
+//	a {
+//		&::before,
+//		&::after {
+//			display: none;
+//		}
+//	}
+//	details a {
+//		font-size: clamp(1rem, 0.9306rem + 0.2222vw, 1.125rem);
+//	}
+//
+//	&[data-loaded] {
+//		opacity: 1;
+//		+ [data-loaded] {
+//			--delay: var(--delay) + var(--increase);
+//		}
+//	}
+//
+//	&.highlighted {
+//		box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1);
+//		animation: highlight-puls 2s ease-in-out;
+//	}
+//	&[open],
+//	&:hover {
+//		.handle {
+//			background-color: var(--overlay-pink-medium);
+//			backdrop-filter: blur(5px);
+//		}
+//	}
+//	summary {
+//		width: calc(100% - 1rem);
+//		height: 100%;
+//		aspect-ratio: 1;
+//		.handle {
+//			position: absolute;
+//			bottom: 0;
+//			left: 0;
+//			right: 0;
+//			background-color: var(--overlay-light);
+//			backdrop-filter: blur(5px);
+//			border-radius: var(--innerRadius);
+//			z-index: 1;
+//			padding: .25rem .25rem .25rem 1.1rem;
+//		}
+//		&::after {
+//			z-index: 11;
+//			position: absolute;
+//			bottom: .35rem;
+//			right: .7rem;
+//			width: 1.5rem;
+//			height: 1.5rem;
+//			cursor: pointer;
+//		}
+//	}
+//
+//	label {
+//		font-weight: normal;
+//		text-transform: none;
+//		.icon {
+//			--w: 1.5em;
+//		}
+//	}
+//}
+///**
+//Load More Button
+// */
+//.load-more {
+//	opacity: 1;
+//	margin: 1rem auto;
+//	width: 66.666%;
+//	display: flex;
+//	align-items: center;
+//	gap: .5rem;
+//	padding: .75rem 1.5rem;
+//	background: var(--base);
+//	color: var(--contrast);
+//	border-radius: 4px;
+//	font-size: var(--medium);
+//	transition: all var(--transition-base);
+//	border: 2px solid transparent;
+//	&[hidden] {
+//		opacity: 0;
+//		transition: all var(--transition-base);
+//	}
+//	&:hover {
+//		background: var(--pink-50);
+//		border-color: var(--contrast);
+//		color: var(--white);
+//	}
+//}
+///**
+//favourite button
+// */
+//button.favourite {
+//	position: absolute;
+//	top: .5rem;
+//	right: .5rem;
+//	z-index: 10;
+//	background: var(--overlay-medium);
+//	border-radius: 50%;
+//	box-shadow: var(--subtle);
+//	border: none;
+//	width: 2rem;
+//	height: 2rem;
+//	display: flex;
+//	justify-content: center;
+//	align-items: center;
+//	backdrop-filter: blur(5px);
+//	transition: all var(--transition-base);
+//
+//	&:hover {
+//		transform: scale(1.1);
+//		color: var(--pink-0);
+//		background: var(--base);
+//		box-shadow: 0 4px 8px rgba(0,0,0,0.15);
+//	}
+//
+//	&.favourited {
+//		animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+//	}
+//}
+//
+//
+///** Images **/
+//.feed-image {
+//	display: block;
+//	aspect-ratio: 1;
+//	overflow: hidden;
+//	width: 100%;
+//	height: 100%;
+//}
+//.feed-images {
+//	width: 100%;
+//	height: 100%;
+//	&.multi {
+//		display :grid;
+//		grid-template-columns: repeat(3, 1fr);
+//		grid-auto-rows: 1fr;
+//		gap: 4px;
+//
+//		> a {
+//			width: 100%;
+//			height: 100%;
+//			aspect-ratio: 1;
+//		}
+//		.feed-image {
+//			grid-row: span 2;
+//			grid-column: span 2;
+//		}
+//	}
+//	img {
+//		width: 100%;
+//		height: 100%;
+//		object-fit: cover;
+//		transition: transform var(--timing) var(--function);
+//	}
+//	a:hover img {
+//		transform: scale(1.05);
+//	}
+//}
+//
+//.feed-item {
+//	&:nth-of-type(4n+2) .multi .feed-image {
+//		grid-column: 2 / span 2;
+//		grid-row: 1 / span 2;
+//	}
+//	&:nth-of-type(4n+3) .multi .feed-image {
+//		grid-row: 2 / span 2;
+//		grid-column: 1 / span 2;
+//	}
+//	&:nth-of-type(4n+4) .multi .feed-image {
+//		grid-column: 2 / span 2;
+//		grid-row: 2 / span 2;
+//	}
+//}
+//
+///** Item Information **/
+//.item-info {
+//	padding: .25rem;
+//	border-left: 1px solid var(--base-200);
+//	>div + div {
+//		margin-top: .5em;
+//		position: relative;
+//
+//		&::before {
+//			content: '';
+//			display: block;
+//			position: absolute;
+//			top: -.3em;
+//			left: -.25rem;
+//			width: 66.6%;
+//			border-bottom: 1px solid var(--base-200);
+//		}
+//	}
+//	h3 {
+//		margin: 0 0 .5em 0!important;
+//		font-size: 1.1rem;
+//		font-family: var(--body);
+//		font-weight: var(--bWeight);
+//	}
+//	span {
+//		text-transform: uppercase;
+//		display: flex;
+//		align-items: center;
+//	}
+//	.icon {
+//		--w: 1.1em;
+//		margin-right: .5em;
+//		display: inline-block;
+//		vertical-align: middle;
+//	}
+//}
+//.item-list {
+//	ul {
+//		margin: 0;
+//		padding: .5em 0;
+//		display: flex;
+//		flex-wrap: wrap;
+//		gap: .5rem;
+//		li {
+//			list-style: none;
+//		}
+//	}
+//	a {
+//		background-color: var(--pink-0);
+//		border: 1px solid transparent;
+//		border-radius: 4px;
+//		color: var(--light-0);
+//		padding: .25em;
+//		line-height: .8;
+//		&:visited {
+//			background-color: var(--pink-100);
+//			color: var(--white);
+//		}
+//		&:visited:hover,
+//		&:visited:focus,
+//		&:hover,
+//		&:focus {
+//			background-color: transparent;
+//			border-color: var(--contrast);
+//			color: var(--contrast);
+//		}
+//	}
+//}
+///**
+//Loading
+// */
+//.loading {
+//	opacity: .7;
+//}
+//
+//.loading-overlay {
+//	position: fixed;
+//	top: 0;
+//	left: 0;
+//	right: 0;
+//	bottom: 0;
+//	background-color: var(--overlay-medium);
+//	display: flex;
+//	align-items: center;
+//	justify-content: center;
+//	transition: opacity 0.3s ease, visibility 0.3s ease;
+//	z-index: 9999;
+//	opacity: 0;
+//	visibility: hidden;
+//}
+//.loading .loading-overlay {
+//	opacity: 1;
+//	visibility: visible;
+//
+//	&::after {
+//		content: '';
+//		position: absolute;
+//		z-index: -1;
+//		inset: 0;
+//		background: linear-gradient(
+//				90deg,
+//				var(--shimmer)
+//		);
+//		animation: shimmer 3s ease-in-out infinite;
+//	}
+//
+//	.wrapper {
+//		background-color: var(--overlay-heavy);
+//		padding: 2rem;
+//		border-radius: var(--innerRadius);
+//		text-align: center;
+//		max-width: 90%;
+//		width: 400px;
+//		height: 300px;
+//		z-index: 5;
+//		display: flex;
+//		justify-content: center;
+//		align-items: center;
+//		position: relative;
+//
+//		.spinner {
+//			--h: 150px;
+//			--w: calc(var(--h) * 2);
+//			border-top: 5px solid var(--pink-0);
+//			border-radius: 50%;
+//			position: absolute;
+//			width: var(--w);
+//			height: var(--w);
+//			top: calc(50% - var(--h));
+//			left: calc(50% - var(--h));
+//			opacity: .5;
+//			z-index: 0;
+//			animation: spin 1s var(--timing) infinite;
+//		}
+//		div.icon {
+//			height: 50px;
+//			width: 50px;
+//
+//			.icon {
+//				--w: 100%;
+//				svg {
+//					animation: dance 2s ease-in-out infinite;
+//					transition: color 0.3s ease;
+//				}
+//			}
+//		}
+//		.status {
+//			height: 200px;
+//			width: 100%;
+//			z-index: 5;
+//			display: flex;
+//			flex-direction: column;
+//			align-items: center;
+//
+//			h3 {
+//				margin: 1.5rem 0 .25rem!important;
+//				color: var(--contrast-200);
+//			}
+//			.message {
+//				margin: 0;
+//				max-width: 275px;
+//				color: var(--contrast-100);
+//				font-size: var(--small);
+//				animation: flicker 2s infinite;
+//			}
+//		}
+//	}
+//}
+//
+//
+///* Animations */
+//@keyframes highlight {
+//	0%, 100% {
+//		box-shadow: none;
+//	}
+//	50% {
+//		box-shadow: 0 0 0 4px var(--pink-0);
+//	}
+//}
+//
+//@keyframes pop {
+//	0% { transform: scale(1); }
+//	50% { transform: scale(1.3); }
+//	75% { transform: scale(0.9); }
+//	100% { transform: scale(1); }
+//}
+//
+//@keyframes bubble {
+//	50%  { box-shadow: 19px 0 0 3px, 38px 0 0 7px, 57px 0 0 3px }
+//	100% { box-shadow: 19px 0 0 0, 38px 0 0 3px, 57px 0 0 7px }
+//}
+//@keyframes highlight-pulse {
+//	0%, 100% { box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1); }
+//	50% { box-shadow: 0 0 0 8px #FF0080, 0 12px 24px rgba(0, 0, 0, 0.15); }
+//}
+//
+//@keyframes spin {
+//	to { transform: rotate(360deg); }
+//}
+//
+//@keyframes shimmer {
+//	0% { transform: translateX(-100%); }
+//	50%, 100% { transform: translateX(100%); }
+//}
+//@keyframes dance {
+//	0%, 100% { transform: rotate(-5deg) scale(1);}
+//	50% { transform: rotate(5deg) scale(1.1); }
+//}
+//@keyframes flicker {
+//	0% { opacity: 0.6; }
+//	50% { opacity: 1; }
+//	100% { opacity: 0.6; }
+//}
+//
+//
+///**
+//Accessibility
+// */
+//
+///* Keyboard navigable feed items */
+//.feed-item[tabindex="0"] {
+//	position: relative;
+//}
+//
+//.feed-item[tabindex="0"]::after {
+//	content: '';
+//	position: absolute;
+//	top: 0;
+//	left: 0;
+//	right: 0;
+//	bottom: 0;
+//	pointer-events: none;
+//	border: 2px solid transparent;
+//	transition: border-color 0.2s ease;
+//}
+//
+//.feed-item[tabindex="0"]:focus::after {
+//	border-color: #FF0080;
+//}
+//
+//
+//.feed-block .item {
+//	summary {
+//		a {
+//			background-color: var(--action-0);
+//			display: flex;
+//			gap: .25rem;
+//			flex-wrap: nowrap;
+//			aspect-ratio: 1;
+//			position: relative;
+//		}
+//		img {
+//			width: 50%;
+//			height: 100%;
+//			object-fit: cover;
+//		}
+//
+//	}
+//}
+
+
 .feed-block {
-	max-width: var(--full);
-	margin: 0 auto;
-
-	&:target {
-		scroll-snap-margin-top: 5rem;
-		scroll-margin-top: 5rem;
-		outline: 0;
-		border-radius: v0;
-		padding: 0;
-
-		.feed-item {
-			outline: double var(--pink-0);
-		}
+	.feed-filters {
+		padding: 1rem 0;
 	}
-}
-
-.loading .feed-block {
-	opacity: .7;
-}
-
-.label {
-	display: flex;
-	align-items: center;
-	gap: .25rem;
-	font-size: .9rem;
-	a:hover {
-		color: var(--pink-0);
-	}
-}
-
-/** Filters Form **/
-.feed-filters {
-	margin: 2rem 0;
-	position: sticky;
-	top: 3rem;
-	z-index: 15;
-	background: var(--overlay-heavy);
-	padding: .25rem 3rem;
-	details[open] summary {
-		background-color: var(--overlay);
-	}
-	summary {
-		justify-content: flex-start;
-
-		> * {
-			order: 3;
-		}
+	.filter-group {
+		position: relative;
+		padding: 2rem 0;
 		.label {
-			order: 1;
+			position: absolute;
+			left: 0;
 		}
-		.filter-label {
-			order: 2;
+		> .label {
+			top: 0;
 		}
-		&::after {
-			order: 4;
-		}
-		#favourites + label {
-			margin-left: auto;
-		}
-		#favourites + label:hover,
-		#favourites:checked + label {
-			border-color: var(--pink-0);
-			background-color: var(--pink-0);
-			color: var(--white);
-
-		}
-		#favourites:checked + label {
-			animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
-		}
-	}
-
-	details[open],
-	summary:hover {
-		background-color: var(--overlay-heavy);
-	}
-
-	&:has(#favourites) {
-		summary::after {
-			margin-left: 1rem;
-		}
-	}
-}
-summary > * {
-	order: 3;
-}
-summary .label {
-	order: 1;
-}
-.filter-label {
-	display: inline-block;
-	vertical-align: middle;
-	height: 1.3em;
-	order: 2;
-	margin: 0;
-	padding: 0;
-
-	li {
-		list-style: none;
-		height: 0;
-		overflow: hidden;
-		&.active {
-			height: 100%;
-		}
-	}
-}
-.filter-group {
-	display: flex;
-	align-items: center;
-	gap: .5rem;
-	flex-wrap: wrap;
-	label {
-		font-weight: 100;
-	}
-	&:has(.order-by) {
-		justify-content: space-between;
-	}
-	.order-by,
-	.order-direction {
-		display: flex;
-		flex-wrap: wrap;
-		gap: .5rem;
-		flex: 1;
-		justify-content: flex-start;
-		.label {
-			width: 100%;
-		}
-
-		label:has(.label) {
-			padding: 0 .35rem;
-			gap: .5rem;
-			.label {
-				font-size: 1rem;
+		button, label {
+			position: relative;
+			padding: .5rem;
+			height: max-content;
+			&:hover {
+				color: var(--action-contrast);
 			}
 		}
+		button:hover .label,
+		:checked + label .label,
+		label:hover .label {
+			opacity: 1;
+			visibility: visible;
+		}
+		&:has(label:hover) :checked + label .label,
+		button .label,
+		label .label {
+			--height: max-content;
+			opacity: 0;
+			visibility: hidden;
+			bottom: -2rem;
+			width: max-content;
+			white-space: nowrap;
+			font-weight: var(--bWeight);
+		}
+
+
 	}
 }
-.filter-group >.label {
-	width: 100%;
-}
-
-/**
-Feed Grid
- */
-.feed-grid {
-	display: grid;
-	grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
-	gap: .5rem;
-	margin-bottom: 2rem;
-	padding: 0 4rem;
-	--delay: 0s;
-	--increase: .1s;
-}
-.feed-empty-state {
-	grid-column: 1/-1;
-	text-align: center;
-	padding: 2rem;
-	background: var(--base-100);
-	border-radius: var(--innerRadius);
-	margin: 0 auto;
-	max-width: 600px;
-}
-/**
-Placeholders
- */
+/** PLACEHOLDERS **/
 .placeholder {
 	aspect-ratio: 1;
 	background: var(--base);
@@ -172,41 +694,74 @@
 	justify-content: center;
 	align-items: center;
 
-	.icon {
+	i.icon {
 		--w: 50%;
 		color: var(--base-200);
-
-		svg {
-			animation: dance 2.5s ease-in-out infinite;
-		}
+		animation: dance 2.5s ease-in-out infinite;
 	}
 }
 
-/**
-Feed Items
- */
-.feed-item {
+
+/** FEED ITEM **/
+.feed.item {
 	position: relative;
 	border-radius: 0.5rem;
 	overflow: hidden;
 	background: var(--base-50);
 	box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-	opacity: 0;
-	transition: opacity var(--transition-base) var(--delay);
 	height: fit-content;
 	padding: 0;
 
 	img {
 		opacity: .7;
 		filter: grayscale(.5) sepia(.3) blur(7px);
-	}
-	&[data-loaded=true] {
-		img {
+		transition: opacity var(--transition-base), filter var(--transition-base);
+		&[data-loaded=true] {
 			opacity: 1;
 			filter: none;
 		}
 	}
 
+	&[data-timeline] {
+		aspect-ratio: unset;
+		summary {
+			aspect-ratio: 3/2;
+			padding: 0 0 1rem;
+			span {
+				width: 50%;
+				position: absolute;
+				background-color: var(--action-0);
+				color: var(--action-contrast);
+				padding: .25rem .5rem;
+				&:first-of-type {
+					bottom: 0;
+					right: 50%;
+					text-align: right;
+				}
+				&:last-of-type {
+					top: 0;
+					left: 50%;
+				}
+			}
+			> a {
+				position: relative;
+				display: flex;
+				flex-wrap: nowrap;
+				width: 100%;
+				height: 100%;
+			}
+		}
+		img {
+			width: 50%;
+			object-fit: cover;
+			height: 100%;
+			&:first-of-type {
+				border-right: 1px solid var(--action-0);
+			}
+		}
+	}
+
+
 	a {
 		&::before,
 		&::after {
@@ -217,13 +772,6 @@
 		font-size: clamp(1rem, 0.9306rem + 0.2222vw, 1.125rem);
 	}
 
-	&[data-loaded] {
-		opacity: 1;
-		+ [data-loaded] {
-			--delay: var(--delay) + var(--increase);
-		}
-	}
-
 	&.highlighted {
 		box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1);
 		animation: highlight-puls 2s ease-in-out;
@@ -269,356 +817,14 @@
 		}
 	}
 }
-/**
-Load More Button
- */
-.load-more {
-	opacity: 1;
-	margin: 1rem auto;
-	width: 66.666%;
-	display: flex;
-	align-items: center;
-	gap: .5rem;
-	padding: .75rem 1.5rem;
-	background: var(--base);
-	color: var(--contrast);
-	border-radius: 4px;
-	font-size: var(--medium);
-	transition: all var(--transition-base);
-	border: 2px solid transparent;
-	&[hidden] {
-		opacity: 0;
-		transition: all var(--transition-base);
-	}
-	&:hover {
-		background: var(--pink-50);
-		border-color: var(--contrast);
-		color: var(--white);
+
+
+.item-grid:has([data-timeline]) {
+	grid-template-columns: repeat(1, 1fr);
+}
+
+@media (min-width:768px) {
+	.item-grid:has([data-timeline]) {
+		grid-template-columns: repeat(2, 1fr);
 	}
 }
-/**
-favourite button
- */
-button.favourite {
-	position: absolute;
-	top: .5rem;
-	right: .5rem;
-	z-index: 10;
-	background: var(--overlay-medium);
-	border-radius: 50%;
-	box-shadow: var(--subtle);
-	border: none;
-	width: 2rem;
-	height: 2rem;
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	backdrop-filter: blur(5px);
-	transition: all var(--transition-base);
-
-	&:hover {
-		transform: scale(1.1);
-		color: var(--pink-0);
-		background: var(--base);
-		box-shadow: 0 4px 8px rgba(0,0,0,0.15);
-	}
-
-	&.favourited {
-		animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
-	}
-}
-
-
-/** Images **/
-.feed-image {
-	display: block;
-	aspect-ratio: 1;
-	overflow: hidden;
-	width: 100%;
-	height: 100%;
-}
-.feed-images {
-	width: 100%;
-	height: 100%;
-	&.multi {
-		display :grid;
-		grid-template-columns: repeat(3, 1fr);
-		grid-auto-rows: 1fr;
-		gap: 4px;
-
-		> a {
-			width: 100%;
-			height: 100%;
-			aspect-ratio: 1;
-		}
-		.feed-image {
-			grid-row: span 2;
-			grid-column: span 2;
-		}
-	}
-	img {
-		width: 100%;
-		height: 100%;
-		object-fit: cover;
-		transition: transform var(--timing) var(--function);
-	}
-	a:hover img {
-		transform: scale(1.05);
-	}
-}
-
-.feed-item {
-	&:nth-of-type(4n+2) .multi .feed-image {
-		grid-column: 2 / span 2;
-		grid-row: 1 / span 2;
-	}
-	&:nth-of-type(4n+3) .multi .feed-image {
-		grid-row: 2 / span 2;
-		grid-column: 1 / span 2;
-	}
-	&:nth-of-type(4n+4) .multi .feed-image {
-		grid-column: 2 / span 2;
-		grid-row: 2 / span 2;
-	}
-}
-
-/** Item Information **/
-.item-info {
-	padding: .25rem;
-	border-left: 1px solid var(--base-200);
-	>div + div {
-		margin-top: .5em;
-		position: relative;
-
-		&::before {
-			content: '';
-			display: block;
-			position: absolute;
-			top: -.3em;
-			left: -.25rem;
-			width: 66.6%;
-			border-bottom: 1px solid var(--base-200);
-		}
-	}
-	h3 {
-		margin: 0 0 .5em 0!important;
-		font-size: 1.1rem;
-		font-family: var(--body);
-		font-weight: var(--bWeight);
-	}
-	span {
-		text-transform: uppercase;
-		display: flex;
-		align-items: center;
-	}
-	.icon {
-		--w: 1.1em;
-		margin-right: .5em;
-		display: inline-block;
-		vertical-align: middle;
-	}
-}
-.item-list {
-	ul {
-		margin: 0;
-		padding: .5em 0;
-		display: flex;
-		flex-wrap: wrap;
-		gap: .5rem;
-		li {
-			list-style: none;
-		}
-	}
-	a {
-		background-color: var(--pink-0);
-		border: 1px solid transparent;
-		border-radius: 4px;
-		color: var(--light-0);
-		padding: .25em;
-		line-height: .8;
-		&:visited {
-			background-color: var(--pink-100);
-			color: var(--white);
-		}
-		&:visited:hover,
-		&:visited:focus,
-		&:hover,
-		&:focus {
-			background-color: transparent;
-			border-color: var(--contrast);
-			color: var(--contrast);
-		}
-	}
-}
-/**
-Loading
- */
-.loading {
-	opacity: .7;
-}
-
-.loading-overlay {
-	position: fixed;
-	top: 0;
-	left: 0;
-	right: 0;
-	bottom: 0;
-	background-color: var(--overlay-medium);
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	transition: opacity 0.3s ease, visibility 0.3s ease;
-	z-index: 9999;
-	opacity: 0;
-	visibility: hidden;
-}
-.loading .loading-overlay {
-	opacity: 1;
-	visibility: visible;
-
-	&::after {
-		content: '';
-		position: absolute;
-		z-index: -1;
-		inset: 0;
-		background: linear-gradient(
-				90deg,
-				var(--shimmer)
-		);
-		animation: shimmer 3s ease-in-out infinite;
-	}
-
-	.wrapper {
-		background-color: var(--overlay-heavy);
-		padding: 2rem;
-		border-radius: var(--innerRadius);
-		text-align: center;
-		max-width: 90%;
-		width: 400px;
-		height: 300px;
-		z-index: 5;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		position: relative;
-
-		.spinner {
-			--h: 150px;
-			--w: calc(var(--h) * 2);
-			border-top: 5px solid var(--pink-0);
-			border-radius: 50%;
-			position: absolute;
-			width: var(--w);
-			height: var(--w);
-			top: calc(50% - var(--h));
-			left: calc(50% - var(--h));
-			opacity: .5;
-			z-index: 0;
-			animation: spin 1s var(--timing) infinite;
-		}
-		div.icon {
-			height: 50px;
-			width: 50px;
-
-			.icon {
-				--w: 100%;
-				svg {
-					animation: dance 2s ease-in-out infinite;
-					transition: color 0.3s ease;
-				}
-			}
-		}
-		.status {
-			height: 200px;
-			width: 100%;
-			z-index: 5;
-			display: flex;
-			flex-direction: column;
-			align-items: center;
-
-			h3 {
-				margin: 1.5rem 0 .25rem!important;
-				color: var(--contrast-200);
-			}
-			.message {
-				margin: 0;
-				max-width: 275px;
-				color: var(--contrast-100);
-				font-size: var(--small);
-				animation: flicker 2s infinite;
-			}
-		}
-	}
-}
-
-
-/* Animations */
-@keyframes highlight {
-	0%, 100% {
-		box-shadow: none;
-	}
-	50% {
-		box-shadow: 0 0 0 4px var(--pink-0);
-	}
-}
-
-@keyframes pop {
-	0% { transform: scale(1); }
-	50% { transform: scale(1.3); }
-	75% { transform: scale(0.9); }
-	100% { transform: scale(1); }
-}
-
-@keyframes bubble {
-	50%  { box-shadow: 19px 0 0 3px, 38px 0 0 7px, 57px 0 0 3px }
-	100% { box-shadow: 19px 0 0 0, 38px 0 0 3px, 57px 0 0 7px }
-}
-@keyframes highlight-pulse {
-	0%, 100% { box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1); }
-	50% { box-shadow: 0 0 0 8px #FF0080, 0 12px 24px rgba(0, 0, 0, 0.15); }
-}
-
-@keyframes spin {
-	to { transform: rotate(360deg); }
-}
-
-@keyframes shimmer {
-	0% { transform: translateX(-100%); }
-	50%, 100% { transform: translateX(100%); }
-}
-@keyframes dance {
-	0%, 100% { transform: rotate(-5deg) scale(1);}
-	50% { transform: rotate(5deg) scale(1.1); }
-}
-@keyframes flicker {
-	0% { opacity: 0.6; }
-	50% { opacity: 1; }
-	100% { opacity: 0.6; }
-}
-
-
-/**
-Accessibility
- */
-
-/* Keyboard navigable feed items */
-.feed-item[tabindex="0"] {
-	position: relative;
-}
-
-.feed-item[tabindex="0"]::after {
-	content: '';
-	position: absolute;
-	top: 0;
-	left: 0;
-	right: 0;
-	bottom: 0;
-	pointer-events: none;
-	border: 2px solid transparent;
-	transition: border-color 0.2s ease;
-}
-
-.feed-item[tabindex="0"]:focus::after {
-	border-color: #FF0080;
-}
-
diff --git a/src/feed/view.js b/src/feed/view.js
index 29859f9..abb91e5 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -1,745 +1,659 @@
 class FeedBlock {
 	constructor() {
-		this.cache = window.jvbCache;
-		this.a11y = window.jvbA11y;
-		this.loading = window.jvbLoading;
-		this.error = window.jvbError;
-
-
 		this.container = document.querySelector('section.feed-block');
 		if (!this.container) {
 			return;
 		}
 
-		this.openGallery = false;
+		this.a11y = window.jvbA11y;
+		this.cache = new window.jvbCache('feed');
+		this.error = window.jvbError;
 
-		this.initElements();
-		this.addPlaceholders();
 		this.config = {
-			api: feedSettings.apiUrl,
-			nonce: feedSettings.nonce,
-			user: jvbSettings.currentUser || null,
 			source: '',
 			context: '',
 			highlight: null,
 			gallery: false,
-			showAuthor: true,
-			showDate: false,
-			view: localStorage.getItem('feedViewMode') || 'grid',
+			view: this.cache.get('feedView') || 'grid',
 			... this.container.dataset
 		};
-		this.taxonomies = {};
-		this.rendered = {};
+		this.initElements();
+		this.initFilters();
 
-		this.feed = {
-			imageLoadThreshold: 5,
-			lazyLoadOffset: '100px',
-			gallery: [],
-			loaded: 0,
-			intsersectionObserver: null,
-			templates: new Map()
-		};
 
-		this.isLoading = false;
-		this.hasMore = true;
-		this.retries = {
-			count: 0,
-			max: 3,
-			delay: 1000
-		};
-		this.page = 1;
-		this.order = 'DESC';
-		this.orderby = 'date';
-
-		this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
-			imageWrapper: '.item',
-			loadMore: ()=>this.fetchFeed.bind(this)
-		}) : false;
-		this.initListeners();
-		if (this.page === 1) {
-			this.processURLFilters();
-		} else {
-			this.updateFilters();
-		}
-
+		this.loadWhenAble();
 	}
+
+	loadWhenAble() {
+		if ('requestIdleCallback' in window) {
+			requestIdleCallback(() => {
+				this.initTaxonomies();
+				this.initStore();
+				this.initListeners();
+				this.initGallery();
+			}, { timeout: 2000 });
+		} else {
+			setTimeout(() => {
+				this.initTaxonomies();
+				this.initStore();
+				this.initListeners();
+				this.initGallery();
+			}, 100);
+		}
+	}
+
 	initElements() {
-		this.filterSelector = 'form.feed-filters';
-		this.filterForm = this.container.querySelector(this.filterSelector);
-		this.grid = this.container.querySelector('.item-grid');
-		this.loadMore = this.container.querySelector('.load-more');
-		this.filterControls = this.container.querySelector('.filter-actions');
-		this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map(
-			content => {
-				return content.value;
-			});
-		this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items');
+		this.currentTaxonomies = new Set();			// Allowed Taxonomies, grabbed from active buttons
+		this.taxonomyFilters = {};
+		this.elements = {
+			filterTrigger: '[data-filter]',
+			filters: {
+				content: '[data-filter="content"]',
+				orderby: '[data-filter="orderby"]',
+				order: '[data-filter="order"]',
+				match: '[data-filter="match"]',
+				favourites: '[data-filter="favourites"]',
+				taxonomy: '[data-filter^="taxonomy"]'
+			},
+			selectedTax: '.selected-terms',
+			clearFilter: 'button.clear-filters',
+			loadMore: 'button.load-more',
+			filterContainer: '.filters',
+			grid: '.item-grid',
+		};
+		this.ui = window.uiFromSelectors(this.elements);
+
+		this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]');
+		this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
+		if (this.ui.content.length > 0) {
+			this.contentTypes = Array.from(
+				this.ui.content
+			).map(content => content.value);
+		} else {
+			this.contentTypes = [this.container.dataset['content']];
+		}
+		if (this.ui.taxonomies.length>0) {
+			this.taxonomies = Array.from(
+				this.ui.taxonomies,
+			).map(content => content.dataset.taxonomy);
+		} else {
+			this.taxonomies = [];
+		}
 	}
 
-	initListeners() {
-		window.addEventListener('popstate', this.handlePopState.bind(this));
-		document.addEventListener('click', this.handleClick.bind(this));
-		document.addEventListener('change', this.handleChange.bind(this));
+	async initTaxonomies() {
+		this.selector = window.jvbSelector;
+		const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
 
-
-		// Intersection observer for lazy loading
-		if ('IntersectionObserver' in window) {
-			this.imageObserver = new IntersectionObserver(entries => {
-				entries.forEach(entry => {
-					if (entry.isIntersecting) {
-						this.loadImage(entry.target);
-						this.imageObserver.unobserve(entry.target);
-					}
-				});
-			}, {
-				rootMargin: '100px',
-				threshold: 0.1
+		this.selector.isInitializing = true;
+		buttons.forEach((button) => {
+			const taxonomy = button.dataset.taxonomy;
+			this.currentTaxonomies.add(taxonomy);
+			this.selector.registerFilterButton(button, {
+				button: button,
+				buttonSelector: '[data-filter="taxonomy"]',
+				selected: this.ui.selectedTax
 			});
-		}
-		// Resize observer for responsive images
-		if ('ResizeObserver' in window) {
-			this.resizeObserver = new ResizeObserver(window.debounce(() => {
-				this.updateImageSizes();
-			}, 250));
+		});
 
-			// Observe the container
-			this.resizeObserver.observe(this.container);
+		// Make this non-blocking
+		setTimeout(() => {
+			this.selector.batchFetchTaxonomies();
+			this.selector.isInitializing = false;
+		}, 0);
+
+		this.selector.subscribe((event, data) => {
+			if (event === 'selected-terms') this.handleTaxonomyChange(data);
+		});
+	}
+
+	handleTaxonomyChange(data) {
+		const { terms, taxonomy } = data;
+
+		// Update only the current taxonomy's terms
+		if (terms.size > 0) {
+			this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
 		} else {
-			// Fallback to window resize
-			window.addEventListener('resize', window.debounce(() => {
-				this.updateImageSizes();
-			}, 250));
+			// Remove taxonomy if no terms selected
+			delete this.taxonomyFilters[taxonomy];
 		}
 
-		this.taxonomies = {};
-		this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => {
-			let taxonomy = selector.dataset.taxonomy;
-			if (!Object.hasOwn(this.taxonomies, taxonomy)) {
-				this.taxonomies[taxonomy] = new window.jvbTaxonomySelector(
-					selector,
-					{
-						multiple: true,
-						feed: true,
-						selected: {},
-						onClose: () => this.setSelectedTerms(taxonomy),
+		// Build filters object with all taxonomies
+		let filters = {
+			page: 1
+		};
+
+		// Add taxonomy filters if any exist
+		if (Object.keys(this.taxonomyFilters).length > 0) {
+			filters.taxonomy = this.taxonomyFilters;
+		}
+
+		this.updateFilter(filters);
+	}
+
+	clearAllTaxonomies() {
+		this.taxonomyFilters = {};
+		window.removeChildren(this.ui.selectedTax);
+
+		this.updateFilter({
+			taxonomy: null,
+			page: 1
+		});
+	}
+
+	initFilters() {
+		//defaults
+		this.filters = {
+			content: 	this.contentTypes[0],
+			orderby: 	'date',
+			order: 		'desc',
+			page: 		1
+		};
+		if (this.config.context) this.filters.context = this.config.context;
+		if (this.config.source) this.filters.source = this.config.source;
+
+		//check the cache
+		this.processCachedFilters();
+		//check url
+		this.processURLFilters();
+
+		// Set initial UI state
+		this.syncUIToFilters();
+	}
+	syncUIToFilters() {
+		// Check radio buttons
+		Object.entries(this.filters).forEach(([key, value]) => {
+			const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
+			if (input) {
+				input.checked = true;
+			}
+		});
+
+		// Update content-specific visibility
+		this.updateContentFor(this.filters.content);
+	}
+	nextPage() {
+		this.store.setFilter('page', this.store.filters.page++);
+	}
+
+	initStore() {
+		this.addPlaceholders();
+		this.store = window.jvbStore.register(
+			'feed',
+			{
+				endpoint: 'feed',
+				keyPath: 'id',
+				indexes: [
+					{ name: 'content', keyPath: 'content'},
+					{ name: 'taxonomy', keyPath: 'taxonomy'},
+					{ name: 'user', keyPath: 'user'},
+					{ name: 'date', keyPath: 'modified'},
+					{ name: 'title', keyPath: 'title'}
+				],
+				filters: this.filters,
+				TTL: 6 * 60 * 60 * 1000,
+				showLoading: true,
+				required: 'content',
+				delayFetch: true
+			}
+		);
+
+		this.store.subscribe((event, data) => {
+			switch (event) {
+				case 'data-loaded':
+					this.renderItems();
+					this.ui.loadMore.hidden = true;
+					if (this.store.lastResponse && this.store.lastResponse['has_more']) {
+						this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
 					}
-				);
+					break;
 			}
 		});
 	}
 
-
-	/**
-	 * Handle browser history navigation
-	 */
-	handlePopState(e) {
-		if (e.state && e.state.filters) {
-			if(this.processURLFilters()){
-				// Load items with updated filters
-				this.resetPage();
-				this.fetchFeed();
-
-				// Announce to screen readers
-				this.a11y.announce('Feed filters updated from browser history.');
-			}
+	initGallery() {
+		this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+		if (this.gallery) {
+			this.gallery.subscribe((event, data) => {
+				if (event === 'load-more' && this.store.lastResponse) {
+					if (this.store.lastResponse['has_more']) {
+						this.nextPage();
+					}
+				}
+			});
 		}
 	}
 
+	processCachedFilters() {
+		Object.keys(this.filters).forEach(filter => {
+			let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+			if (cached && cached !== this.filters[filter]){
+				this.filters[filter] = cached;
+			}
+		});
+	}
+
 	processURLFilters() {
-		const params = new URLSearchParams(window.location.search);
-		//No parameters to process
-		if (!params.toString()) {
-			this.updateFilters();
-			return;
+		if (this.filters.page > 1) {
+			return false;
 		}
+		const params = new URLSearchParams(window.location.search);
 
-		let filters = ['content', 'order', 'orderby', 'favourites','match'];
-
+		if (!params.toString()) {
+			return false;
+		}
+		let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
 		filters.forEach(filter => {
-			let value = params.get('f_'+filter);
-			params.delete('f_'+filter);
-			if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) {
-				this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true;
+			let value = params.get(`f_${filter}`);
+			if (value) {
+				this.filters[filter] = value;
+				let input = this.ui.filters[filter];
+				if (input) {
+					input.checked = true;
+				}
 			}
 		});
 
-		let unprocessed = {};
-		for (var [key, value] of Object.entries(Object.fromEntries(params))) {
-
-			key = key.replace('f_','');
-			if (this.contentTypes.includes(key)) {
-				this.openGallery = value;
-			} else {
-				this.taxonomies[key].addTermsFromURL(value);
-				this.setSelectedTerms(key);
+		let hasTaxonomy = false;
+		// Load taxonomy filters from URL
+		params.forEach((value, key) => {
+			if (key.startsWith('f_tax_')) {
+				hasTaxonomy = true;
+				const taxonomy = key.replace('f_tax_', '');
+				if (!this.taxonomyFilters[taxonomy]) {
+					this.taxonomyFilters[taxonomy] = [];
+				}
+				this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
 			}
-		}
-
-		this.updateFilters();
-	}
-
-	handleClick(e) {
-		if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) {
-			this.fetchFeed(false);
-			e.target.disabled = true;
-		} else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) {
-			this.resetFilters();
-		} else if (this.config.gallery && e.target.closest('.feed-image')) {
-			this.gallery.handleGalleryOpen(e);
-		} else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) {
-			let tag = e.target.closest('.selected-item');
-			let taxonomy = tag.dataset.taxonomy;
-			this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id);
-			this.setSelectedTerms(taxonomy);
-			this.updateFilters();
-		}
-	}
-	handleChange(e) {
-		if (e.target.closest(this.filterSelector)) {
-			this.resetPage();
-			window.removeChildren(this.grid);
-			this.addPlaceholders();
-			//update filters
-			this.updateFilters();
-		}
-	}
-
-	updateFilters() {
-		this.page = 1;
-		const params = new URLSearchParams(window.location.search);
-
-		let filters = Object.fromEntries(new FormData(this.filterForm));
-
-		let contents = [];
-		for (let [key, value] of Object.entries(filters)) {
-			let set = false;
-			switch (key) {
-				case 'content':
-					if (value !== this.contentTypes[0]) {
-						set = true;
+		});
+		if (hasTaxonomy) {
+			for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
+				let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
+				if (button) {
+					if (button.dataset.fieldId) {
+						let field = this.selector.get(button.dataset.fieldId);
+						field.selectedTerms = new Set(ids);
+						this.selector.initFieldDisplay(button.dataset.fieldId);
 					} else {
-						params.delete('f_'+key);
+						this.selector.registerField(button, {
+							button: button,
+							buttonSelector: '[data-filter="taxonomy"]',
+							selected: this.ui.selectedTax,
+							selectedItems: ids
+						});
 					}
-					break;
-				case 'orderby':
-					if (value !== 'date') {
-						set = true;
-					}
-					break;
-				case 'order':
-					if (value !== 'desc') {
-						set = true;
-					}
-					break;
-				default:
-					set = true;
+				}
 			}
-			if (!set) {
-				params.delete('f_'+key);
-			}
-
-
-			if (set && value !== false && value !== '') {
-				params.set('f_'+key, value);
-			}
-			if (value !== '') {
-				contents.push(value);
-			}
-
-			const newURL = `${window.location.pathname}?${params.toString()}`;
-			history.pushState(filters, '', newURL);
-
 		}
-
-		this.filters = filters;
-		this.updateContentFor(filters.content);
-
-		this.updateFilterControls();
-
-		this.loading.setContent(contents);
-		this.fetchFeed(true);
-	}
-
-	updateFilterControls() {
-		this.filterControls.hidden = this.selectedTerms.children.length < 2;
+		return true;
 	}
 
 	/**
-	 * Toggles taxonomy selectors and certain order/orderby options
-	 *  depending on current content
-	 * @param content
+	 * Update URL with current filters (for sharing/bookmarking)
 	 */
-	updateContentFor(content) {
-		this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => {
-			let hasContent = tax.dataset.for.includes(content);
-			tax.hidden = !hasContent;
-			if (!hasContent) {
-				let t = tax.dataset.taxonomy;
-				this.clearSelectedTerms(t);
+	updateURL() {
+		const params = new URLSearchParams();
+
+		// Add simple filters
+		['content', 'order', 'orderby', 'match'].forEach(key => {
+			if (this.filters[key]) {
+				params.set(`f_${key}`, this.filters[key]);
 			}
 		});
-		this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
-			toggle.hidden = !toggle.dataset.for.includes(content);
+
+		// Add taxonomy filters
+		Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
+			if (terms.length > 0) {
+				params.set(`f_tax_${taxonomy}`, terms.join(','));
+			}
 		});
-		this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
-			order.hidden = this.filters.order === 'random';
-		});
+
+		// Update URL without reload
+		const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+		window.history.pushState({ filters: this.filters }, '', newURL);
 	}
 
-	clearSelectedTerms(taxonomy) {
-		this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
-		if (Object.hasOwn(this.taxonomies, taxonomy)) {
-			this.taxonomies[taxonomy].selectedItems = {};
-		}
-	}
-
-	setSelectedTerms(taxonomy) {
-		let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`);
-		input.value = '';
-		let selected = this.taxonomies[taxonomy].selectedTerms;
-		if (!window.isEmptyObject(selected)) {
-			let ids = Object.keys(selected);
-			input.value = ids.join(',');
-		}
-		this.updateFilters();
-	}
-
-
-	nextPage() {
-		if (this.hasMore) {
-			this.page++;
-		}
-	}
-	resetPage() {
-		this.page = 1;
-		this.hasMore = true;
-	}
-	resetState() {
-		this.resetPage(true);
-		this.isLoading = false;
-		this.retries = {
-			count: 0,
-			max: 3,
-			delay: 1000
-		};
-	}
-
-	resetFilters() {
-		this.filterForm.reset();
-		//check the first content
-		this.filterForm.querySelector('input[name="content"]').checked = true;
-		this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true;
-		this.page = 1;
-		this.updateFilters();
-	}
-
-
-	buildFilterRequest() {
-
-		let filters = {};
-
-		for (let [filter, value] of Object.entries(this.filters)) {
-			if (value !== false && value !== '') {
-				filters[filter] = value;
-			}
-		}
-		filters.page = parseInt(this.page);
-		if (this.container.dataset.context) {
-			filters.context = this.container.dataset.context;
-		}
-		if (this.container.dataset.source) {
-			filters.source = this.container.dataset.source;
-		}
-		return new URLSearchParams(filters).toString();
-	}
-
-	async fetchFeed(reset = false, force = false) {
-		if (this.isLoading) {
-			return false;
-		}
-		this.loading.showLoading(this.filters);
-		try {
-			if (this.page === 1) {
-				window.removeChildren(this.grid);
-				this.addPlaceholders();
-			}
-
-			const data = await this.cache.fetchWithCache(
-				`${this.config.api}feed?${this.buildFilterRequest()}`,
-				{
-					method: 'GET',
-				},
-				{
-					context: 'feed',
-					forceRefresh: true
-					// forceRefresh: force
-				}
-			);
-
-			//Handle empty results
-			if (!data || !data.items || data.items.length === 0) {
-				if (this.page === 1) {
-					this.showEmptyState();
-				}
-				this.hasMore = false;
-				return false;
-			} else {
-				this.hasMore = data['has_more'];
-
-				this.renderItems(data.items, this.page > 1);
-
-				if (this.hasMore) {
-					this.nextPage();
-				}
-				return true;
-			}
-		} catch (error) {
-			this.handleError(error);
-		} finally {
-			this.loading.hideLoading();
-			if (this.openGallery !== false) {
-				this.gallery.openWhenReady = this.openGallery;
-				this.openGallery = false;
-			}
-			this.loadMore.disabled = false;
-			this.loadMore.hidden = !this.hasMore;
-		}
-	}
-
-	removePlaceholders() {
-		if (this.grid.querySelector('.placeholder')) {
-			window.removeChildren(this.grid);
-		}
-	}
-	showEmptyState() {
-		window.removeChildren(this.grid);
-		let template = window.getTemplate('emptyState');
-		let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true;
-		if (isFavourite) {
-			[
-				template.querySelector('h3').textContent,
-				template.querySelector('p:first-of-type').textContent,
-				template.querySelector('p:last-of-type').textContent,
-			] = [
-				'♡ BLANK CANVAS ♡',
-				'You haven\'t fallen in love with any pieces... yet!',
-				'Hit that heart icon when something stops your scroll — your dream collection is waiting to start.'
-			];
-		}
-		this.grid.append(template);
-		this.a11y.announceEmpty(isFavourite);
-
-	}
-	handleError(error){
-		return this.error.handleApiError(
-			error,
-			{
-				component: 'Feed Block',
-				action: 'loaditems'
-			},
-			() => this.fetchFeed()
-		);
-	}
-
-	addPlaceholders() {
-		let total = this.contentTypes.length - 1;
-		for (let i = 0; i < 9; i++) {
-			let template = window.getTemplate('placeholderTemplate');
-			let rand =  Math.floor(Math.random()*total+1);
-			let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true);
-
-			template.append(icon);
-			this.grid.append(template);
-		}
-	}
-	renderItems(items, append = false) {
-		//Clear the grid if we aren't appending
-		if (!append) {
-			window.removeChildren(this.grid);
+	renderItems() {
+		let items = this.store.getFiltered();
+		if (this.store.filters['page'] === 1) {
+			window.removeChildren(this.ui.grid);
 			this.addPlaceholders();
 		}
 
-
-		//Bail early if no items
 		if (items.length === 0) {
-			this.a11y.announceUpdate(0, append);
+			this.a11y.announceItems(0, this.store.filters['page'] > 0);
 			return;
 		}
 
-		//Use DocumentFragment for better performance
 		const fragment = document.createDocumentFragment();
-
 		const batchSize = 10;
+
 		const processBatch = (startIndex) => {
 			const endIndex = Math.min(startIndex + batchSize, items.length);
 
 			for (let i = startIndex; i < endIndex; i++) {
 				const item = items[i];
 				const element = this.createItemElement(item);
-				fragment.appendChild(element);
 
-				this.imageObserver.observe(element);
+				fragment.appendChild(element);
 			}
 
 			if (endIndex < items.length) {
-				requestAnimationFrame(() => {
-					processBatch(endIndex);
-				});
+				requestAnimationFrame(() => processBatch(endIndex));
 			} else {
 				this.removePlaceholders();
-				//all batches are processed, append fragment
-				this.grid.appendChild(fragment);
+				this.ui.grid.append(fragment);
+
+				// Observe images after adding to DOM
+				this.observeImages(this.ui.grid);
+
 				if (this.config.gallery) {
 					this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
 				}
-				this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
-				this.a11y.announceItems(items.length, append, this.hasMore);
+
+				this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+				this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
 			}
 		};
 
 		if (items.length > 0) {
 			processBatch(0);
 		} else {
-			this.a11y.announceUpdate(0, append);
+			this.a11y.announceItems(0, this.store.filters['page'] >1, false);
 		}
+
+		this.ui.filters.match.hidden = window.isEmptyObject(this.taxonomyFilters);
+		this.ui.clearFilter.hidden = window.isEmptyObject(this.taxonomyFilters);
 	}
 
 	/**
-	 * Creates a feed-item. Used by RenderItems
+	 *
+	 * @param {object} item
 	 */
 	createItemElement(item) {
-		if(!this.rendered[item.icon]) {
-			this.rendered[item.icon] = new Map();
+		let template = window.getTemplate('feed-item');
+		if (Object.hasOwn(template.dataset, 'timeline')) {
+			return this.createTimelineElement(item, template);
 		}
-		if (this.rendered[item.icon].has(item.id)) {
-			return this.rendered[item.icon].get(item.id);
-		}
-
-		const favourited = window.isFavourited(item.icon, item.id)??false;
-		const template = window.getTemplate('feed-item');
-
-		template.id = `${item.icon}-${item.id}`;
-		template.dataset.id = item.id;
-		template.classList.add(item.icon);
-
-		if (item['umami_view']) {
-			this.buildUmamiData(template, item['umami_view']);
-		}
-
-		let favouriteButton = template.querySelector('button.favourite');
-		[
-			favouriteButton.dataset.id,
-			favouriteButton.dataset.type,
-			favouriteButton.dataset.artist,
-			favouriteButton.title
-		] = [
-			item.id,
-			item.icon,
-			item['user_id'],
-			(favourited) ? 'Remove from Favourites' : 'Add to Favourites'
-		];
-
-		let order = item.order;
-		let single = template.querySelector('.item');
-		let list = template.querySelector('.item-list');
-		let img = template.querySelector('.feed-images');
-		let summary = template.querySelector('summary');
-		let info = template.querySelector('.item-info');
-
-		for (let [index, id] of Object.entries(order)) {
-			let target;
-			let config = item[id];
-			if (id === 'title') {
-				target = template.querySelector('h3 a');
-				if (item.title !== '') {
-					[
-						target.textContent,
-						target.href,
-						target.url
-					] = [
-						item.title,
-						item.url,
-						`Learn more about this ${item.icon}`
-					];
-					if (item.icon !== '') {
-						target.closest('h3').prepend(window.getIcon(item.icon));
-					}
-					if (item.umami_click) {
-						this.buildUmamiData(target, item.umami_click);
-					}
-				} else {
-					target.remove();
-				}
-			}  else if (Object.hasOwn(config, 'terms')) {
-				//Taxonomy list
-				if (config.terms.length === 0) {
-					continue;
-				}
-				let taxonomy = list.cloneNode(true);
-				let label = taxonomy.querySelector('.label');
-				let termList = taxonomy.querySelector('ul');
-				let listItem = taxonomy.querySelector('li');
-
-				if (config.label) {
-					label.textContent = config.label;
-				}
-				if (config.icon) {
-					label.prepend(window.getIcon(config.icon));
-				}
-				if (!config.label && !config.icon){
-					label.remove();
-				}
-
-				config.terms.forEach(term => {
-					let termItem = listItem.cloneNode(true);
-					let link = termItem.querySelector('a');
-					[
-						link.href,
-						link.title,
-						link.textContent
-					] = [
-						term.url,
-						`Learn more about ${term.title}`,
-						term.title
-					];
-					if (term.umami_click.length > 0) {
-						this.buildUmamiData(link, term.umami_click);
-					}
-					termList.append(termItem);
-				});
-
-				listItem.remove();
-				info.appendChild(taxonomy);
-			} else if (Object.hasOwn(config, 'value') && config.value !== '') {
-				let itemInfo = single.cloneNode(true);
-				let label = itemInfo.querySelector('.label');
-				let link = itemInfo.querySelector('a');
-				let p = itemInfo.querySelector('p');
-				if (Object.hasOwn(config, 'label')) {
-					label.textContent = config.label;
-				}
-				if (Object.hasOwn(config, 'icon')) {
-					label.prepend(window.getIcon(config.icon));
-				}
-				if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) {
-					label.remove();
-				}
-				if (Object.hasOwn(config, 'url')) {
-					p.remove();
-					[
-						link.textContent,
-						link.href,
-						link.title
-					] = [
-						config.value,
-						config.url,
-						`Learn more about ${config.value}`
-					];
-				} else {
-					link.remove();
-					p.textContent = config.value;
-				}
-				info.appendChild(itemInfo);
-			} else if (id === 'image') {
-				let images = summary.querySelector('.feed-images');
-				let img = images.querySelector('a');
-
-				let main = img.cloneNode(true);
-				if (!this.config.gallery) {
-					main.href = item.url;
-				}
-
-				main.classList.add('feed-image');
-				this.buildImageData(main.querySelector('img'), item.image);
-				images.append(main);
-
-				if (item.content?.length > 0) {
-					images.classList.add('multi');
-					item.content.forEach(c => {
-						let image = img.cloneNode(true);
-						if (!this.config.gallery) {
-							image.href = c.url;
-						}
-						let itemImg = image.querySelector('img');
-						itemImg.src = c.image.small;
-						itemImg.alt = c.image.alt;
-						images.append(image);
-					});
-				}
-				img.remove();
-			}
-		}
-		single.remove();
-		list.remove();
-
-		this.rendered[item.icon].set(item.id, template);
-
 		return template;
 	}
 
-	buildImageData(img, data){
-		if (typeof data.tiny !== 'string') {
-			return;
-		}
+	createTimelineElement(item, template) {
+		let [
+			main,
+			link,
+			beforeImg,
+			afterImg,
+			afterText,
+			started,
+			lastTreated,
+			total,
+			termList,
+			timeline
+		] = [
+			template,
+			template.querySelector('a'),
+			template.querySelector('img.before'),
+			template.querySelector('img.after'),
+			template.querySelector('summary span:last-of-type'),
+			template.querySelector('p.started time'),
+			template.querySelector('p.updated time'),
+			template.querySelector('p.total b'),
+			template.querySelector('.term-list'),
+			Object.values(item.fields.order)
+		];
+		let numberTreatments = timeline.length - 1;
+		let beforeImgData = item.images[timeline[0]['post_thumbnail']];
+		let afterImgData = item.images[timeline[numberTreatments]['post_thumbnail']];
+
 		[
-			img.src,
-			img.dataset.small,
-			img.dataset.medium,
-			img.dataset.large,
-			img.alt
-		] =
-			[
-				data.tiny,
-				data.small,
-				data.medium,
-				data.large,
-				data.alt
-			];
+			main.dataset.id,
+			link.href,
+			beforeImg.src,
+			beforeImg.dataset.small,
+			beforeImg.dataset.medium,
+			afterImg.src,
+			afterImg.dataset.small,
+			afterImg.dataset.medium,
+			afterText.textContent,
+			started.textContent,
+			lastTreated.textContent,
+			total.textContent
+		] = [
+			item.id,
+			item.url,
+			beforeImgData['tiny'],
+			beforeImgData.small,
+			beforeImgData.medium,
+			afterImgData['tiny'],
+			afterImgData.small,
+			afterImgData.medium,
+			`${afterText.textContent} ${numberTreatments} Tx`,
+			timeline[0].date??item.date,
+			timeline[numberTreatments].date??'',
+			`${numberTreatments} Treatments`
+		];
+		return template;
 	}
 
-	buildUmamiData(item, data){
-		for(let [key, value] of Object.entries(data)){
-			item.dataset[key] = value;
+	addPlaceholders() {
+		let total = this.contentTypes.length;
+		for (let i = 0; i < 12; i++) {
+			let template = window.getTemplate('placeholderTemplate');
+
+			let rand = Math.floor(Math.random() * total);
+			let icon;
+			if (this.ui.content.length > 0) {
+				icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
+			} else {
+				icon = window.getIcon(this.container.dataset.icon);
+			}
+			template.appendChild(icon);
+			this.ui.grid.appendChild(template);
 		}
 	}
 
-	/**
-	 * Load Image, used by renderItems
-	 * @param element
-	 */
-	loadImage(element) {
-		const img = element.querySelector('img');
-		if (!img) return;
-		const size = this.getImageSize();
+	removePlaceholders() {
+		if (this.ui.grid.querySelector('.placeholder')) {
+			window.removeChildren(this.ui.grid);
+		}
+	}
 
-		img.src = img.dataset[size] || img.dataset.src;
-		element.setAttribute('data-loaded', 'true');
+
+
+	/**
+	 *
+	 * @param {object} filters {name: value}
+	 */
+	updateFilter(filters) {
+		//double check filters are what we're expecting
+		let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
+
+		filters = Object.keys(filters)
+			.filter(key => allowed.includes(key))
+			.reduce((obj, key) => {
+				obj[key] = filters[key];
+				return obj;
+			}, {});
+
+		if (window.getDifferences.map(this.filters, filters)) {
+			this.filters = { ...this.filters, ...filters };  // Merge instead of replace
+			this.updateURL();
+			this.store.setFilters(filters);
+		}
+	}
+	/**
+	 * Update visible filters based on selected content type
+	 */
+	updateContentFor(contentType) {
+		// Update taxonomy filter visibility
+		const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
+		taxonomyButtons.forEach(button => {
+			const forTypes = button.dataset.for?.split(',') || [];
+			button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
+		});
+
+		// Update ordering options
+		const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
+		orderButtons.forEach(button => {
+			const forTypes = button.dataset.for?.split(',') || [];
+			if (forTypes.length > 0) {
+				button.hidden = !forTypes.includes(contentType);
+				// Uncheck if hiding
+				if (button.hidden && button.checked) {
+					button.checked = false;
+				}
+			}
+		});
+
+		// Update order direction visibility based on selected orderby
+		const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
+		this.updateOrderDirectionVisibility(orderBy?.value);
 	}
 
 	/**
-	 * Updates the image size according to screen size
+	 * Show/hide order direction based on orderby selection
 	 */
-	updateImageSizes() {
-		const size = this.getImageSize();
-		const items = this.grid.querySelectorAll('.item');
-		items.forEach(item => {
-			const img = item.querySelector('img');
-			if (img && img.dataset[size] && img.src !== img.dataset[size]) {
-				img.src = img.dataset[size];
+	updateOrderDirectionVisibility(orderBy) {
+		const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
+		if (orderDirection) {
+			const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
+			orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
+		}
+	}
+	/*********************************************************************
+	LISTENERS
+	 *********************************************************************/
+	initListeners() {
+		this.popStateHandler 	= this.handlePopState.bind(this);
+		this.clickHandler		= this.handleClick.bind(this);
+		this.changeHandler		= this.handleChange.bind(this);
+		this.imageObserver = null;
+		this.resizeObserver = null;
+		if ('IntersectionObserver' in window) {
+			this.imageObserver = new IntersectionObserver(entries => {
+				entries.forEach(entry => {
+					this.loadImage(entry.target);
+					this.imageObserver.unobserve(entry.target);
+				});
+			}, {
+				rootMargin: '100px',
+				threshold: .1
+			});
+		}
+
+		if ('ResizeObserver' in window) {
+			this.resizeObserver = new ResizeObserver(window.debounce(() => {
+				this.updateImageSizes();
+			}, 250));
+		} else {
+			window.addEventListener('resize', window.debounce(()=> {
+				this.updateImageSizes();
+			}, 250));
+		}
+
+		window.addEventListener('popstate', this.popStateHandler);
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+	}
+
+	loadImage(img) {
+		const src = this.getAppropriateImageSize(img);
+		if (src && src !== img.src) {
+			img.src = src;
+			img.dataset.loaded = 'true';
+		}
+	}
+
+	getAppropriateImageSize(img) {
+		const width = window.innerWidth;
+
+		if (width < 768 && img.dataset.small) {
+			return img.dataset.small;
+		} else if (img.dataset.medium) {
+			return img.dataset.medium;
+		}
+
+		return img.src; // Fallback to current src
+	}
+
+	observeImages(container) {
+		const images = container.querySelectorAll('img[data-small], img[data-medium]');
+		images.forEach(img => {
+			if (!img.dataset.loaded) {
+				this.imageObserver.observe(img);
 			}
 		});
 	}
-	/**
-	 * Get image size based on screen width
-	 */
-	getImageSize() {
-		const width = window.innerWidth;
-		if (width > 1024) return 'medium';
-		if (width > 500) return 'medium';
-		return 'small';
+
+	handlePopState(e) {
+		if (e.state?.filters) {
+			if (this.processURLFilters()) {
+				this.store.setFilters(this.filters);
+				this.a11y.announce('Feed filters updated from browser history');
+			}
+		}
 	}
 
+	handleClick(e) {
+		if (window.targetCheck(e, this.elements.loadMore)) {
+			this.nextPage();
+		} else if (window.targetCheck(e, this.elements.clearFilter)) {
+			this.clearAllTaxonomies();
+		} else if (window.targetCheck(e, '.remove-item')) {
+			this.handleRemoveSelectedTerm(e);
+		}
+	}
+
+	handleRemoveSelectedTerm(e) {
+		const selectedItem = e.target.closest('.selected-item');
+		if (!selectedItem) return;
+
+		const termId = parseInt(selectedItem.dataset.id);
+		const taxonomy = selectedItem.dataset.taxonomy;
+
+		// Remove from filters
+		if (this.taxonomyFilters[taxonomy]) {
+			this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
+				.filter(id => id !== termId);
+
+			if (this.taxonomyFilters[taxonomy].length === 0) {
+				delete this.taxonomyFilters[taxonomy];
+			}
+		}
+
+		// Remove from UI
+		selectedItem.remove();
+
+		// Update filters
+		this.updateFilter({
+			taxonomy: Object.keys(this.taxonomyFilters).length > 0
+				? this.taxonomyFilters
+				: null,
+			page: 1
+		});
+	}
+
+	handleChange(e) {
+		let target = e.target;
+		if (Object.hasOwn(target.dataset, 'filter')) {
+			if (target.dataset.filter === 'content') {
+				this.updateContentFor(target.value);
+				this.updateFilter({ content: target.value, page: 1 });
+			} else if (target.dataset.filter === 'orderby') {
+				this.updateOrderDirectionVisibility(target.value);
+				this.updateFilter({ orderby: target.value, page: 1 });
+			} else if (target.dataset.filter === 'order') {
+				this.updateFilter({ order: target.value, page: 1 });
+			} else if (target.dataset.filter === 'match') {
+				this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
+			} else if (target.dataset.filter === 'favourites') {
+				this.updateFilter({ favourites: target.checked, page: 1 });
+			}
+		}
+	}
 }
-document.addEventListener('DOMContentLoaded', () => {
+
+document.addEventListener('DOMContentLoaded', function() {
 	window.feedBlock = new FeedBlock();
 });
diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
index 347e997..29859f9 100644
--- a/src/feed/viewOld.js
+++ b/src/feed/viewOld.js
@@ -1,968 +1,529 @@
 class FeedBlock {
-    static LOADING_QUIPS = JSON.parse(feedSettings.quips);
-    constructor(container){
+	constructor() {
+		this.cache = window.jvbCache;
+		this.a11y = window.jvbA11y;
+		this.loading = window.jvbLoading;
+		this.error = window.jvbError;
 
-        this.cache = window.jvbCache;
-        this.eventHandlers = new Map();
 
-        this.rendered = {};
+		this.container = document.querySelector('section.feed-block');
+		if (!this.container) {
+			return;
+		}
 
-        // Store container references
-        this.container = container;
-        this.a11y = window.jvbA11y;
+		this.openGallery = false;
 
+		this.initElements();
+		this.addPlaceholders();
+		this.config = {
+			api: feedSettings.apiUrl,
+			nonce: feedSettings.nonce,
+			user: jvbSettings.currentUser || null,
+			source: '',
+			context: '',
+			highlight: null,
+			gallery: false,
+			showAuthor: true,
+			showDate: false,
+			view: localStorage.getItem('feedViewMode') || 'grid',
+			... this.container.dataset
+		};
+		this.taxonomies = {};
+		this.rendered = {};
 
-        //For tracking cache and current requests
-        this.currentRequest = null;
-        this.timeoutId = null;
+		this.feed = {
+			imageLoadThreshold: 5,
+			lazyLoadOffset: '100px',
+			gallery: [],
+			loaded: 0,
+			intsersectionObserver: null,
+			templates: new Map()
+		};
 
-        // Initialize Config
-        this.initConfig();
+		this.isLoading = false;
+		this.hasMore = true;
+		this.retries = {
+			count: 0,
+			max: 3,
+			delay: 1000
+		};
+		this.page = 1;
+		this.order = 'DESC';
+		this.orderby = 'date';
 
+		this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
+			imageWrapper: '.item',
+			loadMore: ()=>this.fetchFeed.bind(this)
+		}) : false;
+		this.initListeners();
+		if (this.page === 1) {
+			this.processURLFilters();
+		} else {
+			this.updateFilters();
+		}
 
-        // Setup error handler
-        this.error = window.jvbError;
-
-        this.resetState();
-        this.state.firstLoad = false;
-        this.state.URLProcessed = false;
-        this.highlightGot = false;
-
-        this.selectorInstances = {};
-
-        this.elements = {
-            filters: this.container.querySelector('form.feed-filters'),
-            selected: this.container.querySelector('.selected-items'),
-            clearFilters: this.container.querySelector('button.clear-filters'),
-            grid: this.container.querySelector('.feed-grid'),
-            loadMore: this.container.querySelector('button.load-more'),
-            spinner: this.container.querySelector('.loading-spinner'),
-            loading: this.container.querySelector('.feed-overlay'),
-            matchAll: this.container.querySelector('.filter-actions .toggle-text'),
-        }
-        this.filters = {
-            content: this.getCurrentContent(),
-            taxonomies: {},
-            favourites: false,
-            orderby: 'date',
-            order: 'desc',
-        }
-
-        this.feed = {
-            imageLoadThreshold: 5,
-            lazyLoadOffset: '100px',
-            gallery: [],
-            loaded: 0,
-            intersectionObserver: null,
-            templates: new Map()
-        }
-        this.imageObserver = null;
-        this.resizeObserver = null;
-
-        this.highlight = null;
-        //Loading settings
-        this.loadingIndex = 0;
-        this.quips = this.initializeQuips();
-        this.loadingMessage = this.container.querySelector('.loading-message');
-        this.dotsElement = this.container.querySelector('.loading-dots');
-        this.quipInterval = null;
-        this.loadingOptions = {
-            loadingMessages: {},
-            cycleInterval: 2000, //time between loading messages
-        }
-
-        this.initFilters();
-        if(this.container.dataset.gallery){
-            this.initGallery();
-            this.setupGalleryAccessibility();
-        }
-        this.initListeners();
-        if(!this.state.URLProcessed){
-            this.updateFilters();
-        }
-
-        this.selectedListeners = this.checkSelectedClicks.bind(this);
-        this.updateSelectedListeners();
-    }
-
-    initConfig() {
-        // Get settings from container data attribute
-        const settings = JSON.parse(this.container.dataset.settings || '{}');
-        let content = Array.from(this.container.querySelectorAll('input[name="content"]')).map(content=> content.value);
-        this.config = {
-            api: feedSettings.apiUrl,
-            nonce: feedSettings.nonce,
-            currentUser: jvbSettings.currentUser || null,
-
-            content: content[0],
-            contentTypes: content,
-            taxonomies: Array.from(this.container.querySelectorAll('.jvb-selector')).map(taxonomy => taxonomy.dataset.taxonomy),
-
-            // Source information for analytics
-            source: this.container.dataset.source || '',
-            context: this.container.dataset.context || '',
-
-            // Optional highlight
-            highlight: null,
-
-            // Gallery mode
-            isGallery: this.container.dataset.gallery || false,
-            showAuthor: !this.container.dataset.gallery || true,
-            showDate: this.container.dataset.gallery || false,
-
-
-            // User preferences
-            viewMode: localStorage.getItem('feedViewMode') || 'grid',
-        }
-
-        if(settings.isGallery){
-            this.config.highlight = this.getHighlight();
-        }
-    }
-
-    initFilters(){
-        this.updateContentFor(this.getCurrentContent());
-    }
-
-    checkSelectedClicks(e){
-        if(e.target.closest('.remove-item')){
-            let tag = e.target.closest('.selected-item');
-
-            // Uncheck checkbox if it exists
-            let taxonomy = tag.dataset.taxonomy;
-            this.clearSelectedTerm(tag.dataset.id, taxonomy);
-            // Remove tag
-            tag.remove();
-
-            // Update clear filters button visibility
-            this.updateClearFiltersButton();
-            this.updateFilters();
-            this.updateSelectedListeners();
-        }
-    }
-
-    updateSelectedListeners(){
-        this.elements.selected.removeEventListener('click', this.selectedListeners);
-        if(this.elements.selected.children.length>0){
-            this.elements.selected.addEventListener('click', this.selectedListeners);
-        }
-    }
-
-    handleFilterChange(e){
-        if(e.target.closest('.jvb-selector')){
-            return;
-        }
-        this.resetPage();
-        if(e.target.name === 'content'){
-            let content = e.target.value;
-            this.updateContentFor(content);
-        }
-        this.updateFilters();
-    }
-    handleLoadMore(){
-        if (this.state.loading || !this.state.hasMore) {
-            return;
-        }
-        this.fetchFeed();
-    }
-    initListeners(){
-        window.addEventListener('popstate', this.handlePopState.bind(this));
-        this.addEvent(this.elements.filters, 'change', (e) => this.handleFilterChange(e));
-        this.addEvent(this.elements.filters, 'submit', e => e.preventDefault());
-        this.addEvent(this.elements.loadMore, 'click', () => this.handleLoadMore());
-        this.addEvent(this.elements.clearFilters, 'click', () => this.clearSelectedTaxonomies());
-        if(this.config.isGallery){
-            this.addEvent(this.elements.grid, 'click', (e) =>this.handleGalleryOpen(e));
-        }
-
-        // Intersection observer for lazy loading
-        if ('IntersectionObserver' in window) {
-            this.imageObserver = new IntersectionObserver(entries => {
-                entries.forEach(entry => {
-                    if (entry.isIntersecting) {
-                        this.loadImage(entry.target);
-                        this.imageObserver.unobserve(entry.target);
-                    }
-                });
-            }, {
-                rootMargin: this.feed.lazyLoadOffset,
-                threshold: 0.1
-            });
-        }
-        // Resize observer for responsive images
-        if ('ResizeObserver' in window) {
-            this.resizeObserver = new ResizeObserver(window.debounce(() => {
-                this.updateImageSizes();
-            }, 250));
-
-            // Observe the container
-            this.resizeObserver.observe(this.container);
-        } else {
-            // Fallback to window resize
-            window.addEventListener('resize', window.debounce(() => {
-                this.updateImageSizes();
-            }, 250));
-        }
-    }
-    handleGalleryOpen(e){
-        const feedImage = e.target.closest('.feed-image');
-        if(feedImage){
-            const item = feedImage.closest('.feed-item');
-            if(item) {
-                const index = Array.from(this.container.querySelectorAll('.feed-item')).indexOf(item);
-                if(index !== -1){
-                    this.openGallery(index);
-                }
-            }
-        }
-    }
-    initGallery(){
-        this.gallery = {
-            items: this.getGalleryItems() || [],
-            index: 0,
-            touchStart: null,
-            touchEnd: null,
-            minSwipe: 50,
-            modal: this.createGalleryModal(),
-            keyHandler: null,
-            loading: false
-        }
-        document.body.appendChild(this.gallery.modal);
-    }
-
-    /**
-     * Handle browser history navigation
-     */
-    handlePopState(e) {
-        if (e.state && e.state.filters) {
-            if(this.processURLFilters()){
-
-                // Load items with updated filters
-                this.resetPage();
-                this.fetchFeed();
-
-                // Announce to screen readers
-                this.a11y.announce('Feed filters updated from browser history.');
-            }
-        }
-    }
-
-    processURLFilters(){
-        const params = new URLSearchParams(window.location.search);
-        if (!params.toString()) {
-            return false; // No parameters to process
-        }
-        // Initialize content type
-        const content = params.get('f_content');
-        params.delete('f_content');
-        if (content && this.elements.filters.querySelector(`input[value="${content}"]`)) {
-            this.elements.filters.querySelector(`input[value="${content}"]`).checked = true;
-            this.filters.content = content;
-        }
-
-        // Initialize order
-        const order = params.get('f_order');
-        params.delete('f_order');
-        if (order && this.elements.filters.querySelector(`input[name="order"][value="${order}"]`)) {
-            this.elements.filters.querySelector(`input[name="order"][value="${order}"]`).checked = true;
-            this.filters.order = order;
-        }
-
-        // Initialize orderby
-        const orderby = params.get('f_orderby');
-        params.delete('f_orderby');
-        if (orderby && this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`)) {
-            this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`).checked = true;
-            this.filters.orderby = orderby;
-        }
-
-        // Initialize favourites filter
-        if (params.get('f_favourites') === 'true' && this.config.currentUser !== null) {
-            params.delete('f_favourites');
-            const favCheckbox = this.elements.filters.querySelector(`input[name="favourites_only"]`);
-            if (favCheckbox) favCheckbox.checked = true;
-            this.filters.favourites = true;
-        }
-
-        // Initialize match, if present
-        if(params.get('f_match') === 'all'){
-            params.delete('f_match');
-            this.elements.matchAll.querySelector('input').checked = true;
-        }
-
-        window.removeChildren(this.elements.selected);
-
-        let filters = JSON.parse(JSON.stringify(this.filters));
-        let unprocessed = {};
-        for (var [key, value] of Object.entries(Object.fromEntries(params))) {
-            if (key.startsWith('f_') ) {
-                var taxName = key.replace('f_', '');
-                let cache = this.cache.getItem(taxName+'List');
-                if(!Object.keys(filters['taxonomies']).includes(taxName)){
-                    filters.taxonomies[taxName] = {};
-                }
-
-                // Handle both single values and comma-separated values
-                const termIds = value.includes(',') ? value.split(',') : [value];
-                termIds.forEach(termId=>{
-                    if(cache && cache.hasOwnProperty(termId)){
-                        filters.taxonomies[taxName][termId] = cache[termId].name;
-                        let tag = this.createFilterTag(taxName, termId, cache[termId].name);
-                        this.elements.selected.appendChild(tag);
-                    }else{
-                        if(!unprocessed.hasOwnProperty(taxName)){
-                            unprocessed[taxName] = [];
-                        }
-                        unprocessed[taxName].push(termId);
-                    }
-                });
-            }
-        }
-
-        this.filters = filters;
-
-        if(!isEmptyObject(unprocessed)){
-            this.fetchTermDetails(unprocessed);
-        }
-
-        if(this.config.isGallery){
-            this.getHighlight();
-        }
-
-        this.updateContentFor(content);
-        this.updateSelectedListeners();
-        return true;
-
-    }
-
-    async loadFromURL() {
-
-        if(this.processURLFilters()){
-            // Announce to screen readers
-            this.a11y.announce('Feed filters updated from URL.');
-        }
-        return true;
-    }
-
-    //We already checked the cache, now fetch the remaining term information
-    async fetchTermDetails(terms) {
-        let params = new URLSearchParams(terms);
-
-        // Otherwise fetch the term details
-        try {
-            // Format the URL - might need to adjust based on your API structure
-            const url = `${this.config.api}terms/check?`+params.toString();
-
-            const termData = await this.cache.fetchWithCache(
-                url, {
-                    method: 'GET',
-                },
-                {
-            });
-
-            for(const [taxonomy, terms] of Object.entries(termData.terms)){
-                if(!this.filters.taxonomies.hasOwnProperty(taxonomy)){
-                    this.filters.taxonomies[taxonomy] = {};
-                }
-
-                this.cache.setItem(taxonomy+'List', terms, taxonomy);
-
-                for(const [termId, termData] of Object.entries(terms)){
-                    this.filters.taxonomies[taxonomy][termId] = termData.name;
-                    let tag = this.createFilterTag(taxonomy, termId, termData.name);
-                    this.elements.selected.appendChild(tag);
-                }
-                this.updateSelectedListeners();
-            }
-
-        } catch (error) {
-            console.error(`Error fetching term details for ${terms}:`, error);
-
-        }
-    }
-
-    //State Management
-    nextPage(hasMore = true){
-        this.state.page++;
-        this.state.hasMore = hasMore;
-    }
-    resetPage(hasMore = true){
-        this.state.page = 1;
-        this.state.hasMore = hasMore;
-    }
-    resetState() {
-        this.state = {
-            page: 1,
-            loading: false,
-            hasMore: true,
-            retries: {
-                count: 0,
-                max: 3,
-                delay: 1000
-            }
-        }
-    }
-
-    updateFilters(reset = true){
-        let updated = false;
-        let content = this.getCurrentContent();
-        let favourites = this.getFavouritesOnly();
-        let order = this.getCurrentOrder();
-        let orderby = this.getCurrentOrderby();
-        let taxonomies = this.getSelectedTaxonomies();
-
-        if(this.filters.content !== content ||
-            this.filters.favourites !== favourites ||
-            this.filters.order !== order ||
-            this.filters.orderby !== orderby ||
-            this.filters.taxonomies !== taxonomies ){
-            updated = true;
-        }
-        (this.filters.content !== content)??this.updateContentFor(this.filters.content);
-
-        this.filters.content = content;
-        this.filters.favourites = favourites;
-        this.filters.order = order;
-        this.filters.orderby = orderby;
-        this.filters.taxonomies = taxonomies;
-
-        if(this.state.firstLoad){
-            this.updateURL();
-        }else{
-            this.state.firstLoad = true;
-        }
-
-
-        if(reset){
-            this.resetPage();
-        }
-        this.fetchFeed();
-    }
-    getCurrentContent(){
-        return this.elements.filters.querySelector('input[name="content"]:checked').value;
-    }
-    getFavouritesOnly(){
-        return this.elements.filters.querySelector('input[name="favourites_only"]')?.checked ??false;
-    }
-    getCurrentOrder(){
-        return this.elements.filters.querySelector('input[name="order"]:checked').value;
-    }
-    getCurrentOrderby(){
-        return this.elements.filters.querySelector('input[name="orderby"]:checked').value;
-    }
-
-    getCurrentTaxonomies(){
-        let taxonomies = this.filters.taxonomies;
-        let out = {};
-        if(!isEmptyObject(taxonomies)){
-            for(var [tax, terms] of Object.entries(taxonomies)){
-                if(!isEmptyObject(terms)){
-                    out[tax] =  Object.values(terms);
-                }
-            }
-        }
-        return out;
-    }
-    updateContentFor(content){
-
-        this.elements.filters.querySelectorAll('.jvb-selector').forEach(tax=> {
-            tax.hidden = !tax.dataset.for.includes(content);
-            //Ensure any selected Taxonomies are removed
-            if(!tax.dataset.for.includes(content)){
-                this.clearSelectedTerms(tax);
-            }
-            //TODO: Ensure clean up of filtered out window.jvbTaxonomySelector instances?
-            //Maybe cache them?
-            let taxonomy = tax.dataset.taxonomy;
-            if(tax.dataset.for.includes(content)){
-                if(!this.selectorInstances.hasOwnProperty(taxonomy)){
-                    this.selectorInstances[taxonomy] = new window.jvbTaxonomySelector(tax, {
-                        multiple: true,
-                        feed: true,
-                        selected: this.filters[taxonomy]??{},
-                        onClose: () => this.setSelectedTerms(taxonomy)
-                    });
-                }
-            }else if(this.selectorInstances.hasOwnProperty(taxonomy)){
-                this.clearSelectedTerms(taxonomy);
-                delete this.selectorInstances[taxonomy];
-            }
-        });
-        this.elements.filters.querySelectorAll('input[data-for]').forEach(toggle=>{
-            toggle.hidden = !toggle.dataset.for.includes(content);
-        });
-        this.elements.filters.querySelectorAll('input[name="order"]').forEach(order=>{
-            order.hidden = this.filters.orderby === 'random';
-        });
-    }
-    updateURL(){
-        const params = new URLSearchParams();
-
-        let taxonomies = this.filters.taxonomies;
-        if(!isEmptyObject(taxonomies)){
-            for(var [tax, terms] of Object.entries(taxonomies)){
-                if(!isEmptyObject(terms)){
-                    params.set('f_'+tax, Object.keys(terms));
-                }
-
-            }
-        }
-
-        // Clone to avoid modifying original
-        let filters = JSON.parse(JSON.stringify(this.filters));
-        delete filters.taxonomies;
-        for(const [key, value] of Object.entries(filters)){
-            if(value !== false){
-                params.set('f_'+key, value);
-            }
-        }
-        if(this.elements.matchAll.querySelector('input:checked')){
-            params.set('f_match', 'all');
-        }
-        const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
-        history.pushState({ filters }, '', newUrl);
-    }
-
-    /**
-     * Feed Fetching
-     * @param reset clear grid
-     * @param force force cache busting
-     * @returns {Promise<void>}
-     */
-    async fetchFeed(reset = false, force = false){
-        if(this.state.loading){
-            return;
-        }
-
-        this.updateLoading(true);
-        try{
-            // Track URL processing
-            if(this.state.page === 1){
-                reset = true;
-                await this.loadFromURL();
-            }
-
-            const params = this.buildFilterRequest();
-            if(this.elements.matchAll.querySelector('input:checked')){
-                params.append('match', true);
-            }
-            params.append('page', this.state.page);
-            params.append('source', this.config.source);
-            params.append('context', this.config.context);
-            if(this.config.highlight){
-                params.append('highlight', JSON.stringify(this.config.highlight));
-            }
-
-            //fetch data with cache busting
-            const data = await this.cache.fetchWithCache(
-                `${this.config.api}feed?${params.toString()}`,
-                {
-                    method: 'GET',
-                },
-                {
-                    context: 'feed',
-                    forceRefresh: true,
-                    // forceRefresh: force,
-                }
-            )
-
-			console.log(data, 'Fetched data: ');
-
-            //Clear grid on first page
-            if(this.state.page === 1){
-                window.removeChildren(this.elements.grid);
-            }
-
-            //Handle empty results
-            if(!data || !data.items || data.items.length === 0){
-                if(this.state.page === 1){
-                    this.showEmptyState();
-                }
-                this.state.hasMore = false;
-            }else{
-                this.state.hasMore = data.hasMore;
-                if(this.state.hasMore){
-                    this.nextPage();
-                }
-                //Render items
-                this.renderItems(data.items, this.state.page > 1);
-
-            }
-
-        }catch(error){
-            this.handleError(error);
-        }finally{
-            this.updateLoading(false);
-            this.elements.loadMore.hidden = !this.state.hasMore;
-        }
-
-    }
-
-    buildFilterRequest(){
-        // Clone to avoid modifying original
-        const filters = JSON.parse(JSON.stringify(this.filters));
-
-        if(filters.taxonomies && !isEmptyObject(filters.taxonomies)){
-            let temp = {};
-            for(var [tax, terms] of Object.entries(filters.taxonomies)){
-                if(!isEmptyObject(terms)){
-                    temp[tax] =Object.keys(terms);
-                }
-            }
-            delete filters.taxonomies;
-            if(!isEmptyObject(temp)){
-                filters.taxonomies =   JSON.stringify(temp);
-            }
-        }else{
-            delete filters.taxonomies;
-        }
-
-
-        if(!filters.favourites){
-            delete filters.favourites;
-        }
-        return new URLSearchParams(filters);
-    }
-
-
-    handleError(error){
-        return this.error.handleApiError(
-            error,
-            {
-                component: 'Feed Block',
-                action: 'loaditems'
-            },
-            () => this.fetchFeed()
-        );
-    }
-
-
-
-    getHighlight() {
-        if(!this.config.highlight){
-            const searchParams = new URLSearchParams(window.location.search);
-
-            // Check for content type parameters
-			this.config.contentTypes.forEach(type => {
-				if (searchParams.has(type)) {
-					this.config.highlight = {};
-					this.config.highlight[type] = searchParams.get(type);
-				}
+	}
+	initElements() {
+		this.filterSelector = 'form.feed-filters';
+		this.filterForm = this.container.querySelector(this.filterSelector);
+		this.grid = this.container.querySelector('.item-grid');
+		this.loadMore = this.container.querySelector('.load-more');
+		this.filterControls = this.container.querySelector('.filter-actions');
+		this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map(
+			content => {
+				return content.value;
 			});
-        }
-        return this.config.highlight;
-    }
+		this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items');
+	}
 
-    /**
-     * Loading
-     */
-    updateLoading(loading) {
-        this.state.loading = loading;
-        if (loading) {
-            this.showLoading();
-
-            let content = this.filters.content;
-            content = (content === 'artwork') ? content : content+'s';
-            let tax = '';
-            let taxonomies = this.getCurrentTaxonomies();
-            if(!isEmptyObject(taxonomies)){
-                let total = 0;
-                total = Object.values(taxonomies).map((tax)=> total+tax.length);
-                let all = [];
-                let join = (total[0] === 2) ? ' and ' : ', ';
-                tax = Object.values(taxonomies).map((tax) => all.push(tax.join(join)));
-                tax = all.join(', ');
-                let index = tax.lastIndexOf(',')+1;
-                if(index> 0){
-                    tax = tax.substr(0, index)+' and'+tax.substr(index);
-                }
-
-            }
+	initListeners() {
+		window.addEventListener('popstate', this.handlePopState.bind(this));
+		document.addEventListener('click', this.handleClick.bind(this));
+		document.addEventListener('change', this.handleChange.bind(this));
 
 
-            this.a11y.announce(`Checking for more ${tax} ${content}...`);
-        } else {
-            this.hideLoading();
-        }
+		// Intersection observer for lazy loading
+		if ('IntersectionObserver' in window) {
+			this.imageObserver = new IntersectionObserver(entries => {
+				entries.forEach(entry => {
+					if (entry.isIntersecting) {
+						this.loadImage(entry.target);
+						this.imageObserver.unobserve(entry.target);
+					}
+				});
+			}, {
+				rootMargin: '100px',
+				threshold: 0.1
+			});
+		}
+		// Resize observer for responsive images
+		if ('ResizeObserver' in window) {
+			this.resizeObserver = new ResizeObserver(window.debounce(() => {
+				this.updateImageSizes();
+			}, 250));
 
-        // Update loading spinner
-        this.elements.spinner.hidden = !loading;
+			// Observe the container
+			this.resizeObserver.observe(this.container);
+		} else {
+			// Fallback to window resize
+			window.addEventListener('resize', window.debounce(() => {
+				this.updateImageSizes();
+			}, 250));
+		}
 
-        // Update load more button
-        this.elements.loadMore.disabled = loading;
-
-    }
+		this.taxonomies = {};
+		this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => {
+			let taxonomy = selector.dataset.taxonomy;
+			if (!Object.hasOwn(this.taxonomies, taxonomy)) {
+				this.taxonomies[taxonomy] = new window.jvbTaxonomySelector(
+					selector,
+					{
+						multiple: true,
+						feed: true,
+						selected: {},
+						onClose: () => this.setSelectedTerms(taxonomy),
+					}
+				);
+			}
+		});
+	}
 
 
-    /**
-     * Show the loading overlay
-     */
-    showLoading() {
-        this.hideBody();
-        this.elements.loading.classList.add('active');
-        this.startQuipCycle();
-        document.body.classList.add('loading');
-    }
+	/**
+	 * Handle browser history navigation
+	 */
+	handlePopState(e) {
+		if (e.state && e.state.filters) {
+			if(this.processURLFilters()){
+				// Load items with updated filters
+				this.resetPage();
+				this.fetchFeed();
 
-    /**
-     * Hide the loading overlay
-     */
-    hideLoading() {
-        this.showBody();
-        this.container.classList.remove('active');
-        this.stopQuipCycle();
-        document.body.classList.remove('loading');
-    }
+				// Announce to screen readers
+				this.a11y.announce('Feed filters updated from browser history.');
+			}
+		}
+	}
+
+	processURLFilters() {
+		const params = new URLSearchParams(window.location.search);
+		//No parameters to process
+		if (!params.toString()) {
+			this.updateFilters();
+			return;
+		}
+
+		let filters = ['content', 'order', 'orderby', 'favourites','match'];
+
+		filters.forEach(filter => {
+			let value = params.get('f_'+filter);
+			params.delete('f_'+filter);
+			if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) {
+				this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true;
+			}
+		});
+
+		let unprocessed = {};
+		for (var [key, value] of Object.entries(Object.fromEntries(params))) {
+
+			key = key.replace('f_','');
+			if (this.contentTypes.includes(key)) {
+				this.openGallery = value;
+			} else {
+				this.taxonomies[key].addTermsFromURL(value);
+				this.setSelectedTerms(key);
+			}
+		}
+
+		this.updateFilters();
+	}
+
+	handleClick(e) {
+		if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) {
+			this.fetchFeed(false);
+			e.target.disabled = true;
+		} else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) {
+			this.resetFilters();
+		} else if (this.config.gallery && e.target.closest('.feed-image')) {
+			this.gallery.handleGalleryOpen(e);
+		} else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) {
+			let tag = e.target.closest('.selected-item');
+			let taxonomy = tag.dataset.taxonomy;
+			this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id);
+			this.setSelectedTerms(taxonomy);
+			this.updateFilters();
+		}
+	}
+	handleChange(e) {
+		if (e.target.closest(this.filterSelector)) {
+			this.resetPage();
+			window.removeChildren(this.grid);
+			this.addPlaceholders();
+			//update filters
+			this.updateFilters();
+		}
+	}
+
+	updateFilters() {
+		this.page = 1;
+		const params = new URLSearchParams(window.location.search);
+
+		let filters = Object.fromEntries(new FormData(this.filterForm));
+
+		let contents = [];
+		for (let [key, value] of Object.entries(filters)) {
+			let set = false;
+			switch (key) {
+				case 'content':
+					if (value !== this.contentTypes[0]) {
+						set = true;
+					} else {
+						params.delete('f_'+key);
+					}
+					break;
+				case 'orderby':
+					if (value !== 'date') {
+						set = true;
+					}
+					break;
+				case 'order':
+					if (value !== 'desc') {
+						set = true;
+					}
+					break;
+				default:
+					set = true;
+			}
+			if (!set) {
+				params.delete('f_'+key);
+			}
 
 
-    /**
-     * Start cycling through loading messages
-     */
-    startQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-        }
+			if (set && value !== false && value !== '') {
+				params.set('f_'+key, value);
+			}
+			if (value !== '') {
+				contents.push(value);
+			}
 
-        if (!this.quips.length) return;
+			const newURL = `${window.location.pathname}?${params.toString()}`;
+			history.pushState(filters, '', newURL);
 
-        // Set initial message
-        this.updateMessage(this.quips[0]);
-        this.container.classList.remove('changing');
+		}
 
-        this.quipInterval = setInterval(() => {
-            this.container.classList.add('changing');
+		this.filters = filters;
+		this.updateContentFor(filters.content);
 
-            setTimeout(() => {
-                this.loadingIndex = (this.loadingIndex + 1) % this.quips.length;
-                this.updateMessage(this.quips[this.loadingIndex]);
+		this.updateFilterControls();
 
-                setTimeout(() => {
-                    this.container.classList.remove('changing');
-                }, 50);
-            }, 350);
-        }, this.loadingOptions.cycleInterval);
-    }
+		this.loading.setContent(contents);
+		this.fetchFeed(true);
+	}
 
-    /**
-     * Stop cycling through loading messages
-     */
-    stopQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-            this.quipInterval = null;
-        }
-    }
+	updateFilterControls() {
+		this.filterControls.hidden = this.selectedTerms.children.length < 2;
+	}
 
-    /**
-     * Update the loading message
-     */
-    updateMessage(quipData) {
-        if (!this.loadingMessage) return;
+	/**
+	 * Toggles taxonomy selectors and certain order/orderby options
+	 *  depending on current content
+	 * @param content
+	 */
+	updateContentFor(content) {
+		this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => {
+			let hasContent = tax.dataset.for.includes(content);
+			tax.hidden = !hasContent;
+			if (!hasContent) {
+				let t = tax.dataset.taxonomy;
+				this.clearSelectedTerms(t);
+			}
+		});
+		this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
+			toggle.hidden = !toggle.dataset.for.includes(content);
+		});
+		this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
+			order.hidden = this.filters.order === 'random';
+		});
+	}
 
-        const icon = feedSettings?.icons?.[quipData.icon] || '';
-        this.loadingMessage.innerHTML = `${icon}<p>${quipData.quip}</p>`;
-    }
+	clearSelectedTerms(taxonomy) {
+		this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
+		if (Object.hasOwn(this.taxonomies, taxonomy)) {
+			this.taxonomies[taxonomy].selectedItems = {};
+		}
+	}
 
-    /**
-     * Set a custom loading message
-     */
-    setMessage(message, icon = null) {
-        if (!this.loadingMessage) return;
-
-        this.stopQuipCycle();
-
-        const iconHtml = icon ? feedSettings?.icons?.[icon] || '' : '';
-        this.loadingMessage.innerHTML = `${iconHtml}<p>${message}</p>`;
-    }
-
-    /**
-     * Shuffle an array (Fisher-Yates algorithm)
-     */
-    shuffleArray(array) {
-        const newArray = [...array];
-        for (let i = newArray.length - 1; i > 0; i--) {
-            const j = Math.floor(Math.random() * (i + 1));
-            [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
-        }
-        return newArray;
-    }
-
-    /**
-     * Initialize quips for loading messages
-     */
-    initializeQuips() {
-        // Map pstyle, artstyle, etc. to their base types for quips
-        const typeMapping = {
-            'pstyle': 'style',
-            'artstyle': 'style',
-            'arttheme': 'theme',
-            'artmedium': 'style',
-            'artform': 'style',
-            'placement': 'style',
-            'colour': 'style'
-        };
-
-        // Gather all quips based on content types and taxonomies
-        const allQuips = [];
-
-        // Add content type quips
-        if (FeedBlock.LOADING_QUIPS[this.filters.content]) {
-            FeedBlock.LOADING_QUIPS[this.filters.content].forEach(quip => {
-                allQuips.push({
-                    icon: this.filters.content,
-                    quip: quip
-                });
-            });
-        }
-
-        // Add taxonomy quips
-        if(this.filters.taxonomies && !isEmptyObject(this.filters.taxonomies)){
-            Object.keys(this.filters.taxonomies).forEach(taxonomy => {
-                const baseType = typeMapping[taxonomy] || taxonomy;
-                if (FeedBlock.LOADING_QUIPS[baseType]) {
-                    FeedBlock.LOADING_QUIPS[baseType].forEach(quip => {
-                        allQuips.push({
-                            icon: taxonomy,
-                            quip: quip
-                        });
-                    });
-                }
-            });
-        }
+	setSelectedTerms(taxonomy) {
+		let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`);
+		input.value = '';
+		let selected = this.taxonomies[taxonomy].selectedTerms;
+		if (!window.isEmptyObject(selected)) {
+			let ids = Object.keys(selected);
+			input.value = ids.join(',');
+		}
+		this.updateFilters();
+	}
 
 
-        // Shuffle the quips array
-        return this.shuffleArray(allQuips);
-    }
+	nextPage() {
+		if (this.hasMore) {
+			this.page++;
+		}
+	}
+	resetPage() {
+		this.page = 1;
+		this.hasMore = true;
+	}
+	resetState() {
+		this.resetPage(true);
+		this.isLoading = false;
+		this.retries = {
+			count: 0,
+			max: 3,
+			delay: 1000
+		};
+	}
 
-    /**
-     * Feed Grid
-     */
+	resetFilters() {
+		this.filterForm.reset();
+		//check the first content
+		this.filterForm.querySelector('input[name="content"]').checked = true;
+		this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true;
+		this.page = 1;
+		this.updateFilters();
+	}
 
-    renderItems(items, append = false) {
-        // Reset if not appending
-        if (!append) {
-            window.removeChildren(this.elements.grid);
-            this.feed.loaded = 0;
-            this.feed.gallery = [];
-        }
 
-        // Add items to gallery if in gallery mode
-        if (this.config.isGallery) {
-            this.feed.gallery = this.feed.gallery.concat(items);
-        }
+	buildFilterRequest() {
 
-        // Bail early if no items
-        if (items.length === 0) {
-            this.a11y.announceUpdate(0, append);
-            return;
-        }
+		let filters = {};
 
-        // Use DocumentFragment for better performance
-        const fragment = document.createDocumentFragment();
+		for (let [filter, value] of Object.entries(this.filters)) {
+			if (value !== false && value !== '') {
+				filters[filter] = value;
+			}
+		}
+		filters.page = parseInt(this.page);
+		if (this.container.dataset.context) {
+			filters.context = this.container.dataset.context;
+		}
+		if (this.container.dataset.source) {
+			filters.source = this.container.dataset.source;
+		}
+		return new URLSearchParams(filters).toString();
+	}
 
-        // Process items in batches for better performance
-        const batchSize = 10;
-        const processBatch = (startIndex) => {
-            const endIndex = Math.min(startIndex + batchSize, items.length);
+	async fetchFeed(reset = false, force = false) {
+		if (this.isLoading) {
+			return false;
+		}
+		this.loading.showLoading(this.filters);
+		try {
+			if (this.page === 1) {
+				window.removeChildren(this.grid);
+				this.addPlaceholders();
+			}
 
-            // Process this batch
-            for (let i = startIndex; i < endIndex; i++) {
-                const item = items[i];
-                const element = this.createItemElement(item);
-                fragment.appendChild(element);
+			const data = await this.cache.fetchWithCache(
+				`${this.config.api}feed?${this.buildFilterRequest()}`,
+				{
+					method: 'GET',
+				},
+				{
+					context: 'feed',
+					forceRefresh: true
+					// forceRefresh: force
+				}
+			);
 
-                // Lazy load images beyond threshold
-                if (this.feed.loaded >= this.feed.imageLoadThreshold && this.imageObserver) {
-                    this.imageObserver.observe(element);
-                } else {
-                    this.loadImage(element);
-                }
+			//Handle empty results
+			if (!data || !data.items || data.items.length === 0) {
+				if (this.page === 1) {
+					this.showEmptyState();
+				}
+				this.hasMore = false;
+				return false;
+			} else {
+				this.hasMore = data['has_more'];
 
-                this.feed.loaded++;
-            }
+				this.renderItems(data.items, this.page > 1);
 
-            // If we have more items, process next batch in next frame
-            if (endIndex < items.length) {
-                requestAnimationFrame(() => {
-                    processBatch(endIndex);
-                });
-            } else {
-                // All batches processed, append fragment
-                this.elements.grid.appendChild(fragment);
-                if(this.container.dataset.gallery){
-					console.log(this.getGalleryItems());
-                    this.updateGalleryItems(this.getGalleryItems());
-                    //pagination already updated, so it'll be page 2
-                    if(this.getHighlight() && !this.highlightGot){
-                        this.highlightGot = true;
-                        this.openGallery(0);
-                    }
-                }
-                this.a11y.makeNavigable(this.elements.grid.querySelectorAll('.feed-item:not([data-keyboard-nav])'));
-                this.a11y.announceItems(items.length, append, this.state.hasMore);
-            }
-        };
+				if (this.hasMore) {
+					this.nextPage();
+				}
+				return true;
+			}
+		} catch (error) {
+			this.handleError(error);
+		} finally {
+			this.loading.hideLoading();
+			if (this.openGallery !== false) {
+				this.gallery.openWhenReady = this.openGallery;
+				this.openGallery = false;
+			}
+			this.loadMore.disabled = false;
+			this.loadMore.hidden = !this.hasMore;
+		}
+	}
 
-        // Start processing the first batch
-        if (items.length > 0) {
-            processBatch(0);
-        } else {
-            this.a11y.announceUpdate(0, append);
-        }
-    }
+	removePlaceholders() {
+		if (this.grid.querySelector('.placeholder')) {
+			window.removeChildren(this.grid);
+		}
+	}
+	showEmptyState() {
+		window.removeChildren(this.grid);
+		let template = window.getTemplate('emptyState');
+		let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true;
+		if (isFavourite) {
+			[
+				template.querySelector('h3').textContent,
+				template.querySelector('p:first-of-type').textContent,
+				template.querySelector('p:last-of-type').textContent,
+			] = [
+				'♡ BLANK CANVAS ♡',
+				'You haven\'t fallen in love with any pieces... yet!',
+				'Hit that heart icon when something stops your scroll — your dream collection is waiting to start.'
+			];
+		}
+		this.grid.append(template);
+		this.a11y.announceEmpty(isFavourite);
 
-    /**
-     * Load image for an item
-     */
-    loadImage(element) {
-        const img = element.querySelector('img');
-        if (!img) return;
-        const size = this.getImageSize();
-        img.src = img.dataset[size] || img.dataset.src;
-        delete img.dataset.src;
+	}
+	handleError(error){
+		return this.error.handleApiError(
+			error,
+			{
+				component: 'Feed Block',
+				action: 'loaditems'
+			},
+			() => this.fetchFeed()
+		);
+	}
 
-        element.setAttribute('data-loaded', 'true');
-    }
+	addPlaceholders() {
+		let total = this.contentTypes.length - 1;
+		for (let i = 0; i < 9; i++) {
+			let template = window.getTemplate('placeholderTemplate');
+			let rand =  Math.floor(Math.random()*total+1);
+			let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true);
 
-    /**
-     * Update image sizes based on screen width
-     */
-    updateImageSizes() {
-        const size = this.getImageSize();
+			template.append(icon);
+			this.grid.append(template);
+		}
+	}
+	renderItems(items, append = false) {
+		//Clear the grid if we aren't appending
+		if (!append) {
+			window.removeChildren(this.grid);
+			this.addPlaceholders();
+		}
 
-        // Update only visible images that aren't already loaded with the right size
-        const items = this.elements.grid.querySelectorAll('.feed-item[data-loaded="true"]');
-        items.forEach(item => {
-            const img = item.querySelector('img');
-            if (img && img.dataset[size] && img.src !== img.dataset[size]) {
-                img.src = img.dataset[size];
-            }
-        });
-    }
 
-    /**
-     * Create an element for a feed item
-     */
-    createItemElement(item) {
+		//Bail early if no items
+		if (items.length === 0) {
+			this.a11y.announceUpdate(0, append);
+			return;
+		}
 
-        if(!this.rendered[item.icon]){
-            this.rendered[item.icon] = new Map();
-        }
+		//Use DocumentFragment for better performance
+		const fragment = document.createDocumentFragment();
 
-        if(this.rendered[item.icon].has(item.id)){
-            return this.rendered[item.icon].get(item.id);
-        }
+		const batchSize = 10;
+		const processBatch = (startIndex) => {
+			const endIndex = Math.min(startIndex + batchSize, items.length);
 
-        const favourited = window.isFavourited(item.icon, item.id)??false;
-        const template = window.getTemplate('feed-item');
+			for (let i = startIndex; i < endIndex; i++) {
+				const item = items[i];
+				const element = this.createItemElement(item);
+				fragment.appendChild(element);
 
-        // Set unique attributes
-        template.id = `${item.icon}-${item.id}`;
-        template.classList.add(item.icon);
+				this.imageObserver.observe(element);
+			}
 
-		if (item.umami_view) {
-			this.buildUmamiData(template, item.umami_view);
+			if (endIndex < items.length) {
+				requestAnimationFrame(() => {
+					processBatch(endIndex);
+				});
+			} else {
+				this.removePlaceholders();
+				//all batches are processed, append fragment
+				this.grid.appendChild(fragment);
+				if (this.config.gallery) {
+					this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
+				}
+				this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+				this.a11y.announceItems(items.length, append, this.hasMore);
+			}
+		};
+
+		if (items.length > 0) {
+			processBatch(0);
+		} else {
+			this.a11y.announceUpdate(0, append);
+		}
+	}
+
+	/**
+	 * Creates a feed-item. Used by RenderItems
+	 */
+	createItemElement(item) {
+		if(!this.rendered[item.icon]) {
+			this.rendered[item.icon] = new Map();
+		}
+		if (this.rendered[item.icon].has(item.id)) {
+			return this.rendered[item.icon].get(item.id);
+		}
+
+		const favourited = window.isFavourited(item.icon, item.id)??false;
+		const template = window.getTemplate('feed-item');
+
+		template.id = `${item.icon}-${item.id}`;
+		template.dataset.id = item.id;
+		template.classList.add(item.icon);
+
+		if (item['umami_view']) {
+			this.buildUmamiData(template, item['umami_view']);
 		}
 
 		let favouriteButton = template.querySelector('button.favourite');
@@ -974,7 +535,7 @@
 		] = [
 			item.id,
 			item.icon,
-			item.user_id,
+			item['user_id'],
 			(favourited) ? 'Remove from Favourites' : 'Add to Favourites'
 		];
 
@@ -985,7 +546,7 @@
 		let summary = template.querySelector('summary');
 		let info = template.querySelector('.item-info');
 
-		for (let [index, id] of Object.entries(order)){
+		for (let [index, id] of Object.entries(order)) {
 			let target;
 			let config = item[id];
 			if (id === 'title') {
@@ -1009,7 +570,7 @@
 				} else {
 					target.remove();
 				}
-			} else if (Object.hasOwn(config, 'terms')) {
+			}  else if (Object.hasOwn(config, 'terms')) {
 				//Taxonomy list
 				if (config.terms.length === 0) {
 					continue;
@@ -1084,7 +645,10 @@
 				let img = images.querySelector('a');
 
 				let main = img.cloneNode(true);
-				main.href = item.url;
+				if (!this.config.gallery) {
+					main.href = item.url;
+				}
+
 				main.classList.add('feed-image');
 				this.buildImageData(main.querySelector('img'), item.image);
 				images.append(main);
@@ -1093,7 +657,9 @@
 					images.classList.add('multi');
 					item.content.forEach(c => {
 						let image = img.cloneNode(true);
-						image.href = c.url;
+						if (!this.config.gallery) {
+							image.href = c.url;
+						}
 						let itemImg = image.querySelector('img');
 						itemImg.src = c.image.small;
 						itemImg.alt = c.image.alt;
@@ -1106,552 +672,74 @@
 		single.remove();
 		list.remove();
 
-        this.rendered[item.icon].set(item.id, template);
+		this.rendered[item.icon].set(item.id, template);
 
-        return template;
-    }
+		return template;
+	}
 
-    buildImageData(img, data){
+	buildImageData(img, data){
 		if (typeof data.tiny !== 'string') {
 			return;
 		}
-        [
-            img.src,
-            img.dataset.small,
-            img.dataset.medium,
-            img.dataset.large,
-            img.alt
-        ] =
-        [
-            data.tiny,
-            data.small,
-            data.medium,
-            data.large,
-            data.alt
-        ];
-    }
+		[
+			img.src,
+			img.dataset.small,
+			img.dataset.medium,
+			img.dataset.large,
+			img.alt
+		] =
+			[
+				data.tiny,
+				data.small,
+				data.medium,
+				data.large,
+				data.alt
+			];
+	}
 
-    buildUmamiData(item, data){
-        for(let [key, value] of Object.entries(data)){
-            item.dataset[key] = value;
-        }
-    }
+	buildUmamiData(item, data){
+		for(let [key, value] of Object.entries(data)){
+			item.dataset[key] = value;
+		}
+	}
 
-    /**
-     * Show empty state
-     */
-    showEmptyState() {
-        const message = this.filters.favourites
-            ? `<div class="feed-empty-state">
-                <h3>♡ BLANK CANVAS ♡</h3>
-                <p>You haven't fallen in love with any pieces... yet!</p>
-                <p>Hit that heart icon when something stops your scroll.</p>
-                <p>Your dream collection is waiting to start.</p>
-            </div>`
-            : `<div class="feed-empty-state">
-                <h3>NOTHING HERE...</h3>
-                <p>Try tweaking those filters.</p>
-                <p>Edmonton's got talent - let's find it.</p>
-            </div>`;
+	/**
+	 * Load Image, used by renderItems
+	 * @param element
+	 */
+	loadImage(element) {
+		const img = element.querySelector('img');
+		if (!img) return;
+		const size = this.getImageSize();
 
-        this.elements.grid.innerHTML = message;
-        this.a11y.announceEmpty(this.filters.favourites);
-    }
+		img.src = img.dataset[size] || img.dataset.src;
+		element.setAttribute('data-loaded', 'true');
+	}
 
-    /**
-     * Clear the grid
-     */
-    clearGrid() {
-        this.a11y.announce('Items cleared.');
-        window.removeChildren(this.elements.grid);
-        this.feed.loaded = 0;
-    }
-
-    /**
-     * Get image size based on screen width
-     */
-    getImageSize() {
-        const width = window.innerWidth;
-        if (width > 1024) return 'medium';
-        if (width > 500) return 'medium';
-        return 'small';
-    }
-
-    /**
-     * Get gallery items for the gallery modal
-     */
-    getGalleryItems() {
-        return Array.from(this.container.querySelectorAll('.feed-item'))
-            .map(item => {
-                const img = item.querySelector('img');
-                if (!img) return null;
-
-                return {
-                    id: item.querySelector('button.favourite').dataset.id,
-                    small: img.dataset.small || img.src,
-                    large: img.dataset.medium || img.src,
-                    full: img.dataset.full || img.src,
-                    alt: img.alt || '',
-                    fav: item.querySelector('button.favourite')?.cloneNode(true),
-                    info: item.querySelector('.item-info')?.cloneNode(true)
-                };
-            })
-            .filter(Boolean);
-    }
-
-    /**
-     * Clean up resources when component is destroyed
-     */
-    addEvent(element, event, handler, options) {
-        if (!element) return;
-
-        const boundHandler = handler.bind(this);
-        element.addEventListener(event, boundHandler, options);
-
-        // Store for cleanup
-        if (!this.eventHandlers.has(element)) {
-            this.eventHandlers.set(element, []);
-        }
-        this.eventHandlers.get(element).push({ event, handler: boundHandler });
-    }
-    destroy() {
-        // Clean up observers
-        if (this.imageObserver) {
-            this.imageObserver.disconnect();
-            this.imageObserver = null;
-        }
-
-        if (this.resizeObserver) {
-            this.resizeObserver.disconnect();
-            this.resizeObserver = null;
-        }
-
-        // Clean up all event listeners
-        this.eventHandlers.forEach((handlers, element) => {
-            handlers.forEach(({ event, handler }) => {
-                element.removeEventListener(event, handler);
-            });
-        });
-        this.eventHandlers.clear();
-
-        // Clean up timers
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-        }
-
-        if (this.timeoutId) {
-            clearTimeout(this.timeoutId);
-        }
-
-        // Clear template cache and other state
-        this.feed.templates.clear();
-        this.feed.gallery = [];
-        this.feed.loaded = 0;
-    }
-    /** Extra Term Handling **/
-
-    getSelectedTaxonomies(){
-        let taxonomies = {};
-        for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
-            taxonomies[taxonomy] = instance.selectedItems;
-        }
-        return taxonomies;
-    }
-    /**
-     * Get selected values for a taxonomy
-     */
-    getSelectedTerms(taxonomy) {
-        const selectedItems = this.elements.selected.querySelectorAll(
-            `.selected-item[data-taxonomy="${taxonomy}"]`
-        );
-
-        return Array.from(selectedItems).map(item => item.dataset.id);
-    }
-
-    clearSelectedTaxonomies(){
-        window.removeChildren(this.elements.selected);
-        if(!isEmptyObject(this.selectorInstances)){
-            for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
-                instance.selectedItems = {};
-            }
-        }
-
-        this.elements.matchAll.querySelector(input).checked = false;
-
-        this.filters.taxonomies = {};
-        this.updateFilters();
-    }
-    setSelectedTerms(taxonomy){
-        if(this.selectorInstances[taxonomy]){
-            let selected = this.selectorInstances[taxonomy].selectedItems;
-            if(!isEmptyObject(selected)){
-
-                this.filters.taxonomies[taxonomy] = selected;
-                for(var [id, name] of Object.entries(selected)){
-                    this.elements.selected.appendChild(this.createFilterTag(taxonomy, id, name));
-                }
-                this.updateFilters();
-                this.updateSelectedListeners();
-            }else{
-                delete this.filters.taxonomies[taxonomy];
-            }
-
-        }
-    }
-    clearSelectedTerm(termId, taxonomy){
-        let container = this.container.querySelector('.filters');
-        let input = container.querySelector(`li[data-id="${termId}"] input`);
-        input.checked = false
-        delete this.selectorInstances[taxonomy].selectedItems[termId];
-    }
-    clearSelectedTerms(taxonomy){
-
-        if(!isEmptyObject(this.filters.taxonomies) && this.filters.taxonomies.hasOwnProperty(taxonomy)){
-            delete this.filters.taxonomies[taxonomy];
-        }
-        if(!isEmptyObject(this.selectorInstances) && this.selectorInstances.hasOwnProperty(taxonomy)){
-            this.selectorInstances[taxonomy].selectedItems = {};
-        }
-
-
-        const selectedItems = this.elements.selected.querySelectorAll(
-            `.selected-item[data-taxonomy="${taxonomy}"]`
-        );
-
-        if(selectedItems.length > 0){
-            selectedItems.forEach(item => {
-                item.remove();
-            });
-        }
-
-        // Update clear filters button visibility
-        this.updateClearFiltersButton();
-    }
-    updateClearFiltersButton(){
-        if (!this.elements.clearFilters) return;
-
-        let filters = this.elements.selected.children.length;
-
-        const hasFilters = filters > 0;
-
-        const hasMultiple = filters > 1;
-
-        this.elements.clearFilters.hidden = !hasFilters;
-
-        this.elements.filters.classList.toggle('has-filters', hasFilters);
-
-        this.elements.matchAll.hidden = !hasMultiple;
-    }
-
-
-    /**
-     * Create a filter tag element
-     */
-    createFilterTag(taxonomy, id, name) {
-        const tag = window.getTemplate('selectedTerm');
-
-        tag.dataset.taxonomy = taxonomy;
-        tag.dataset.id = id;
-        let icon = window.getIcon(taxonomy);
-        let span = tag.querySelector('span');
-        let button = tag.querySelector('button');
-        tag.prepend(icon);
-        span.classList.add('filter-name');
-        span.classList.remove('item-name');
-        [span.textContent, button.remove] =
-            [escapeHtml(name), `Remove ${escapeHtml(name)}`];
-
-
-
-        return tag;
-    }
-
-    /**
-     * Gallery
-     **/
-    openGallery(index){
-        this.gallery.index = index;
-        this.gallery.modal.showModal();
-        this.hideBody();
-
-        this.bindGalleryEvents();
-        //show current image
-        this.updateDisplay(index);
-        //preload adjacent images
-        this.preloadImages();
-
-        // Announce initial state
-        this.a11y.announce(`Image ${this.gallery.index + 1} of ${this.gallery.items.length}. Use arrow keys to navigate.`)
-        this.a11y.trapFocus(this.gallery.modal);
-
-    }
-
-    /**
-     * Create the modal element
-     */
-    createGalleryModal() {
-        const modal = document.createElement('dialog');
-        modal.className = 'gallery-modal';
-        modal.setAttribute('aria-modal', 'true');
-        modal.setAttribute('aria-label', 'Image Gallery');
-
-        modal.innerHTML = `
-        <button class="gallery-close" aria-label="Close gallery">
-          ${jvbSettings.icons.close}
-        </button>
-
-        <button class="gallery-nav gallery-prev" aria-label="Previous image">
-          ${jvbSettings.icons.prev}
-        </button>
-
-        <button class="gallery-nav gallery-next" aria-label="Next image">
-             ${jvbSettings.icons.next}
-        </button>
-
-        <div class="gallery-content">
-          <img src="" alt="" class="gallery-image">
-          <details>
-            <summary>DETAILS</summary>
-            <div class="item-info"></div>
-          </details>
-        </div>
-
-        <div class="gallery-favourite"></div>
-        <div class="gallery-counter"><span id="gallery-index">1</span> / <span class="total"></span></div>
-    `;
-
-        return modal;
-    }
-
-
-    /**
-     * Bind event handlers
-     */
-    bindGalleryEvents() {
-        // Close button
-        this.gallery.modal.querySelector('.gallery-close').addEventListener('click', () => this.closeGallery());
-
-        // Navigation buttons
-        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
-        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
-
-        prevBtn.addEventListener('click', () => this.navigate(-1));
-        nextBtn.addEventListener('click', () => this.navigate(1));
-
-        // Keyboard navigation
-        this.gallery.keyHandler = (e) => {
-            switch (e.key) {
-                case 'ArrowLeft':
-                    this.navigate(-1);
-                    break;
-                case 'ArrowRight':
-                    this.navigate(1);
-                    break;
-                case 'Escape':
-                    this.closeGallery();
-                    break;
-            }
-        };
-        document.addEventListener('keydown', this.gallery.keyHandler);
-
-        // Touch events
-        this.gallery.modal.addEventListener('touchstart', (e) => {
-            this.gallery.touchStart = e.touches[0].clientX;
-        });
-
-        this.gallery.modal.addEventListener('touchmove', (e) => {
-            this.gallery.touchEnd = e.touches[0].clientX;
-        });
-
-        this.gallery.modal.addEventListener('touchend', () => {
-            if (!this.gallery.touchStart || !this.gallery.touchEnd) return;
-
-            const distance = this.gallery.touchStart - this.gallery.touchEnd;
-            const isLeftSwipe = distance > this.gallery.minSwipe;
-            const isRightSwipe = distance < -this.gallery.minSwipe;
-
-            if (isLeftSwipe) {
-                this.navigate(1);
-            } else if (isRightSwipe) {
-                this.navigate(-1);
-            }
-
-            this.gallery.touchStart = null;
-            this.gallery.touchEnd = null;
-        });
-    }
-
-    /**
-     * Navigate to previous/next image
-     */
-    async navigate(direction) {
-        const newIndex = this.gallery.index + direction;
-
-        // Check if out of bounds
-        if (newIndex < 0 || newIndex >= this.gallery.items.length) {
-            this.a11y.announceNavigation(newIndex, this.gallery.items.length,direction < 0, direction > 0)
-            return;
-        }
-
-        // Update current index
-        this.gallery.index = newIndex;
-
-        // Update display
-        this.updateDisplay(newIndex);
-
-        // Preload adjacent images
-        this.preloadImages();
-
-        // Announce to screen readers
-        this.a11y.announceNavigation(this.gallery.index,this.gallery.items.length);
-
-        //Load more if we're near the end
-        if (direction > 0 && newIndex >= (this.gallery.items.length - 3) && this.state.hasMore) {
-            await this.fetchFeed();
-            this.updateGalleryItems(this.getGalleryItems());
-        }
-    }
-
-    /**
-     * Preload adjacent images
-     */
-    preloadImages() {
-        // Preload current, previous and next images
-        [-1, 0, 1].forEach(offset => {
-            const index = this.gallery.index + offset;
-            if (index >= 0 && index < this.gallery.items.length) {
-                const img = new Image();
-                const item = this.gallery.items[index];
-
-                if (window.innerWidth < 1000) {
-                    img.src = item.large || item.src;
-                } else {
-                    img.src = item.full || item.src;
-                }
-            }
-        });
-    }
-
-    /**
-     * Update display with current image
-     */
-    updateDisplay(index) {
-        const item = this.gallery.items[index];
-        if (!item) return;
-
-        // Get elements
-        const favourite = this.gallery.modal.querySelector('.gallery-favourite');
-        const image = this.gallery.modal.querySelector('.gallery-image');
-        const counter = this.gallery.modal.querySelector('.gallery-counter');
-        const info = this.gallery.modal.querySelector('.item-info');
-
-        // Update image
-        image.src = window.innerWidth < 1000 ?
-            (item.large || item.src) :
-            (item.full || item.src);
-
-        image.alt = item.alt || '';
-
-        // Update favourite button
-        if (favourite && item.fav) {
-            window.removeChildren(favourite);
-            favourite.appendChild(item.fav.cloneNode(true));
-        }
-
-        // Update info
-        if (info && item.info) {
-            window.removeChildren(info);
-            const clone = item.info.cloneNode(true);
-            info.appendChild(clone);
-        }
-
-        // Update counter
-        counter.textContent = `${this.gallery.index + 1} / ${this.gallery.items.length}`;
-
-        // Update navigation buttons
-        this.updateNavigationButtons();
-    }
-
-    /**
-     * Update navigation button visibility
-     */
-    updateNavigationButtons() {
-        const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
-        const nextBtn = this.gallery.modal.querySelector('.gallery-next');
-
-        prevBtn.classList.toggle('end', this.gallery.index > 0 ? '' : 'none');
-        nextBtn.classList.toggle('end', this.gallery.index < this.gallery.items.length - 1 ? '' : 'none');
-    }
-
-    /**
-     * Close the gallery
-     */
-    closeGallery() {
-        this.showBody();
-        // Remove event listeners
-        document.removeEventListener('keydown', this.gallery.keyHandler);
-        this.a11y.announce('Gallery closed.');
-
-        this.gallery.modal.close();
-
-        // Reset state
-        this.gallery.keyHandler = null;
-    }
-
-    /**
-     * Update gallery items
-     * @param {Array} newItems - New gallery items
-     */
-    updateGalleryItems(newItems) {
-        // Store original current index and item
-        const currentItem = this.gallery.items[this.gallery.index];
-
-        // Update items array
-        this.gallery.items = newItems;
-
-        // Try to keep the same item selected
-        if (currentItem) {
-            // Find the same item in the new array by matching source
-            const newIndex = this.gallery.items.findIndex(item =>
-                item.full === currentItem.full ||
-                item.large === currentItem.large
-            );
-
-            if (newIndex !== -1) {
-                this.gallery.index = newIndex;
-            }
-        }
-
-        // Update navigation buttons
-        this.updateNavigationButtons();
-    }
-
-    /**
-     * Ensure gallery is accessible
-     */
-    setupGalleryAccessibility() {
-        // Add ARIA attributes
-        this.gallery.modal.setAttribute('aria-modal', 'true');
-        this.gallery.modal.setAttribute('aria-label', 'Image Gallery');
-    }
+	/**
+	 * Updates the image size according to screen size
+	 */
+	updateImageSizes() {
+		const size = this.getImageSize();
+		const items = this.grid.querySelectorAll('.item');
+		items.forEach(item => {
+			const img = item.querySelector('img');
+			if (img && img.dataset[size] && img.src !== img.dataset[size]) {
+				img.src = img.dataset[size];
+			}
+		});
+	}
+	/**
+	 * Get image size based on screen width
+	 */
+	getImageSize() {
+		const width = window.innerWidth;
+		if (width > 1024) return 'medium';
+		if (width > 500) return 'medium';
+		return 'small';
+	}
 
-    hideBody(){
-        document.body.style.overflow = 'hidden';
-    }
-    showBody(){
-        document.body.style.overflow = '';
-    }
 }
-
-// Initialize feed blocks when DOM is loaded
 document.addEventListener('DOMContentLoaded', () => {
-    document.querySelectorAll('.feed-block').forEach(container => {
-        // Initialize with both the container and overlay
-        window.feedBlock = new FeedBlock(container);
-    });
+	window.feedBlock = new FeedBlock();
 });
-
-
-function isEmptyObject(obj) {
-    return Object.keys(obj).length === 0;
-}
diff --git a/src/gmbreviews/render.php b/src/gmbreviews/render.php
index 47ad2b5..7aa2dcb 100644
--- a/src/gmbreviews/render.php
+++ b/src/gmbreviews/render.php
@@ -108,6 +108,7 @@
 			<?php
 			foreach ($reviews as $review) {
 				$reviewer = $review['reviewer']['displayName'] ?? 'Anonymous';
+				$reviewer = strtok($reviewer, ' ');
 				$profilePhoto = $review['reviewer']['profilePhotoUrl'] ?? '';
 				$rating = $review['starRating'] ?? 0;
 				$rating = match($rating) {
@@ -122,20 +123,27 @@
 				$date = $review['updateTime'] ?? '';
 				?>
 				<li>
-					<article class="review">
-						<header class="row btw">
+					<blockquote class="review">
+						<?php
+						// Review text
+						if (!empty($comment)) { ?>
+							<div class="content review">
+								<?= apply_filters('wpautop', $comment) ?>
+							</div>
+						<?php } ?>
+						<cite class="row start nowrap">
 							<?php if (!empty($profilePhoto)) { ?>
 								<img src="<?=esc_url($profilePhoto)?>"
-									alt="<?=esc_attr($reviewer)?>"
-									'loading="lazy">
+									 alt="<?=esc_attr($reviewer)?>"
+								'loading="lazy">
 							<?php } else { ?>
 								<div class="avatar">
-								<?= jvbIcon('user-circle')?>
+									<?= jvbIcon('user-circle')?>
 								</div>
 							<?php } ?>
 
-							<div class="col end">
-								<h4><?= esc_html($reviewer)?></h4>
+							<div class="row start wrap">
+								<p><?= esc_html($reviewer)?></p>
 								<?php
 								// Date
 								if ($showDate && !empty($date)) {
@@ -154,16 +162,8 @@
 									</div>
 								<?php } ?>
 							</div>
-
-						</header>
-						<?php
-						// Review text
-						if (!empty($comment)) { ?>
-							<div class="review">
-								<?= apply_filters('the_content', $comment) ?>
-							</div>
-						<?php } ?>
-					</article>
+						</cite>
+					</blockquote>
 				</li>
 			<?php
 			}
diff --git a/src/gmbreviews/style.scss b/src/gmbreviews/style.scss
index a2edaf3..339f5dc 100644
--- a/src/gmbreviews/style.scss
+++ b/src/gmbreviews/style.scss
@@ -1,4 +1,5 @@
 .gmb-reviews {
+	max-width: var(--alignWide);
 	> .row.btw {
 		.button {
 			width: 100%;
@@ -9,8 +10,10 @@
 		}
 	}
 	.stars {
-		display: inline-block;
-		vertical-align: middle;
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		flex-wrap: nowrap;
 	}
 	ul {
 		list-style: none;
@@ -19,13 +22,62 @@
 		li {
 			margin: 2rem 0;
 			position:relative;
-			&:nth-of-type(odd) {
-				left: -2rem;
+			@media (min-width: 768px) {
+				&:nth-of-type(odd) {
+					left: -2rem;
+				}
+				&:nth-of-type(even) {
+					right: -2rem;
+				}
 			}
-			&:nth-of-type(even) {
-				right: -2rem;
+
+		}
+	}
+	blockquote {
+		margin:0;
+		padding: 0;
+		.content {
+			border-width: 4px 1px;
+			&::after {
+				border-width: 4px 1px;
+			}
+			&::before {
+				border-width: 8px;
+				bottom: -4px;
 			}
 		}
+		cite {
+			position: relative;
+
+			img {
+				width: 4.5rem;
+				position: absolute;
+				left: -8rem;
+				top: 0;
+			}
+
+			p {
+				margin: 0;
+			}
+
+			.wrap {
+				--wrap: wrap;
+
+				p, time {
+					max-width: 49%;
+				}
+
+				.stars {
+					width: 100%;
+				}
+			}
+		}
+		time {
+			white-space: nowrap;
+		}
+	}
+	.stars .icon {
+		background-color: var(--action-0);
 	}
 	article {
 		padding: 1rem;
diff --git a/src/timeline/block.json b/src/timeline/block.json
new file mode 100644
index 0000000..9e219a1
--- /dev/null
+++ b/src/timeline/block.json
@@ -0,0 +1,23 @@
+{
+	"$schema": "https://schemas.wp.org/trunk/block.json",
+	"apiVersion": 3,
+	"name": "jvb/timeline",
+	"version": "0.1.0",
+	"title": "Timeline",
+	"category": "jvb",
+	"icon": "shortcode",
+	"description": "Outputs a single timeline post in a cool way",
+	"example": {},
+	"supports": {
+		"html": false,
+		"align": ["wide", "full"]
+	},
+	"textdomain": "jvb",
+	"selectors": {
+		"root": ".timeline"
+	},
+	"editorScript": "file:./index.js",
+	"editorStyle": "file:./index.css",
+	"style": "file:./style-index.css",
+	"viewScript": "file:./view.js"
+}
diff --git a/src/timeline/edit.js b/src/timeline/edit.js
new file mode 100644
index 0000000..14f0ce6
--- /dev/null
+++ b/src/timeline/edit.js
@@ -0,0 +1,38 @@
+/**
+ * Retrieves the translation of text.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * React hook that is used to mark the block wrapper element.
+ * It provides all the necessary props like the class name.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * Those files can contain any CSS code that gets applied to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './editor.scss';
+
+/**
+ * The edit function describes the structure of your block in the context of the
+ * editor. This represents what the editor will render when the block is used.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
+ *
+ * @return {Element} Element to render.
+ */
+export default function Edit() {
+	return (
+		<p { ...useBlockProps() }>
+			{ __( 'Will output the timeline', 'jvb' ) }
+		</p>
+	);
+}
diff --git a/src/timeline/editor.scss b/src/timeline/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/timeline/editor.scss
diff --git a/src/timeline/index.js b/src/timeline/index.js
new file mode 100644
index 0000000..d82621b
--- /dev/null
+++ b/src/timeline/index.js
@@ -0,0 +1,33 @@
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+	/**
+	 * @see ./edit.js
+	 */
+	edit: Edit,
+} );
diff --git a/src/timeline/index.php b/src/timeline/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/timeline/index.php
diff --git a/src/timeline/render.php b/src/timeline/render.php
new file mode 100644
index 0000000..52aded4
--- /dev/null
+++ b/src/timeline/render.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
+ */
+?>
+<p <?php echo get_block_wrapper_attributes(); ?>>
+	<?php esc_html_e( 'Menu – hello from a dynamic block!', 'menu' ); ?>
+</p>
diff --git a/src/timeline/style.scss b/src/timeline/style.scss
new file mode 100644
index 0000000..2f6ac8d
--- /dev/null
+++ b/src/timeline/style.scss
@@ -0,0 +1,135 @@
+main {
+	--gap: 0;
+	section:last-of-type {
+		margin-bottom: 0;
+	}
+}
+
+#at-a-glance {
+	max-width: var(--alignWide);
+	--gap: 0;
+	img {
+		width: 100%;
+		height: auto;
+		border: 2px solid var(--action-0);
+	}
+	h3 {
+		font-size: var(--small);
+	}
+	.before {
+		img {
+			border-right-width: 1px;
+			border-left: 0;
+			border-top: 0;
+		}
+	}
+	.after {
+		img {
+			border-left-width: 1px;
+			border-right: 0;
+			border-bottom: 0;
+		}
+	}
+}
+
+.timeline-point.timeline-point {
+	--lineWidth: 1px;
+	--gap: 2rem;
+	padding: 0;
+	margin:0;
+	background-color: var(--base);
+	max-width: 100vw;
+	position: relative;
+	overflow: hidden;
+	.open-gallery {
+		width: 40%;
+		border-radius: 4px;
+		position: sticky;
+		padding: .5rem;
+	}
+	.info {
+		padding: 1rem .5rem .5rem;
+		width: 60%;
+		position: relative;
+		h2 {
+			margin: 0 0 .5rem;
+			font-size: var(--medium);
+			position: relative;
+			.icon {
+				--w: 2.5rem;
+				transform: rotate(-90deg);
+				position: absolute;
+				left: -2.5rem;
+				top: .25rem;
+				background-color: var(--action-100);
+			}
+		}
+
+	}
+	&::before,
+	&::after {
+		content: '';
+		display: block;
+		position: absolute;
+		left: 45%;
+		height: 100%;
+		width: var(--lineWidth);
+		background-color: var(--action-0);
+		//box-shadow: var(--action-shadow);
+	}
+	&::before {
+		height: 1rem;
+	}
+	&::after {
+		top: 4rem;
+	}
+	&#before-treatment::before,
+	&:last-of-type::after {
+		display: none;
+	}
+}
+@media (min-width:768px) {
+	#at-a-glance {
+		h3  {
+			font-size: var(--xlarge);
+		}
+	}
+	.timeline-point.timeline-point {
+		--gap: 4rem;
+		.open-gallery {
+			width: 50%;
+		}
+		.info {
+			width: 50%;
+			padding: 25vh 1rem 1rem;
+			h2 {
+				.icon {
+					--w: 4rem;
+					left: -4.15rem;
+					top: 0;
+				}
+			}
+			a {
+				display: flex;
+				flex-wrap: wrap;
+				align-items: center;
+			}
+
+			time {
+				text-transform: uppercase;
+				font-size: var(--small);
+			}
+		}
+		&::before,
+		&::after {
+			left: calc(50% + 2rem);
+		}
+
+		&::before {
+			height: calc(25vh - 2rem);
+		}
+		&::after {
+			top: calc(25vh + 6rem);
+		}
+	}
+}
diff --git a/src/timeline/view.js b/src/timeline/view.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/timeline/view.js
diff --git a/webpack.jvb.js b/webpack.jvb.js
index df580d7..e2efb38 100644
--- a/webpack.jvb.js
+++ b/webpack.jvb.js
@@ -17,7 +17,8 @@
 		'form':                './assets/js/concise/FormController.js',
 		'favourites':          './assets/js/concise/FrontendFavourites.js',
 		'votes':               './assets/js/concise/FrontendVotes.js',
-		'gallery':             './assets/js/Gallery.js',
+		'gallery':             './assets/js/concise/Gallery.js',
+		'swiper':             './assets/js/concise/Swiper.js',
 		'maps':                './assets/js/dash/GoogleMaps.js',
 		'handleSelection':     './assets/js/concise/HandleSelection.js',
 		'integrations':        './assets/js/dash/Integrations.js',

--
Gitblit v1.10.0