From 0e4b986e81f8132a44e61fa8df18860301cc3468 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 01 Jan 2026 20:31:10 +0000
Subject: [PATCH] =JakeVan preliminary additions

---
 build/list/render.php                   |    2 
 base/_setup.php                         |   11 
 assets/css/icons.css                    |    1 
 src/drawer-menu/index.php               |    0 
 build/drawer-menu/style-index-rtl.css   |    1 
 src/summary/render.php                  |    2 
 assets/js/concise/navigation.js         |    2 
 build/drawer-menu/index-rtl.css         |    1 
 assets/js/min/navigation.min.js         |    2 
 inc/blocks/SummaryBlock.php             |    4 
 build/drawer-menu/index.js              |    1 
 assets/css/dash.min.css                 |    2 
 inc/rest/routes/NotificationsRoutes.php |    1 
 build/drawer-menu/index.css             |    1 
 inc/helpers/ui.php                      |   14 
 inc/forms/TaxonomySelector.php          |    1 
 src/feed/render.php                     |    3 
 wp-config.php                           |  128 
 inc/rest/routes/ContentRoutes.php       |    3 
 assets/js/concise/UtilityFunctions.js   |   20 
 assets/js/min/queue.min.js              |    2 
 inc/blocks/CustomBlocks.php             |   70 
 inc/rest/routes/FeedRoutes.php          |   41 
 assets/js/min/utility.min.js            |    2 
 assets/js/concise/CRUD.js               |   21 
 JVBase.php                              |   34 
 build/summary/render.php                |    2 
 assets/js/concise/UploadManagerC.js     | 2278 ++++++++
 inc/blocks/TimelineBlock.php            |    4 
 src/drawer-menu/index.js                |   10 
 assets/js/concise/Queue.js              |   25 
 inc/ui/CRUDSkeleton.php                 |    3 
 inc/integrations/Integrations.php       |   23 
 src/feed/style.scss                     |   84 
 inc/managers/IconsManager.php           |    1 
 build/drawer-menu/render.php            |   39 
 src/drawer-menu/style.scss              |   88 
 assets/js/min/uploadGroups.min.js       |    1 
 build/feed/style-index-rtl.css          |    2 
 inc/helpers/breadcrumbs.php             |    5 
 inc/ui/Navigation.php                   |  170 
 src/drawer-menu/save.js                 |    3 
 src/forms/view.js                       |   63 
 assets/js/concise/HandleSelection.js    |  551 -
 assets/js/concise/FormController.js     |    4 
 build/feed/style-index.css              |    2 
 assets/js/concise/UploadManagerOld.js   | 3141 ++++++++++++
 inc/managers/DirectoryManager.php       |  335 
 assets/js/min/handleSelection.min.js    |    2 
 inc/registry/CheckCustomTables.php      |  237 
 inc/rest/routes/FormRoutes.php          |    2 
 assets/js/concise/UploadManager.js      | 4628 ++++++-----------
 inc/blocks/FeedBlock.php                |  194 
 inc/managers/SEO/BreadcrumbManager.php  |   63 
 src/feed/view.js                        |  135 
 inc/utility/Validator.php               |    2 
 assets/js/min/crud.min.js               |    2 
 package-lock.json                       | 2288 ++++++--
 inc/helpers/renderFields.php            |   45 
 assets/css/forms.min.css                |    2 
 build/drawer-menu/style-index.css       |    1 
 inc/blocks/_setup.php                   |    3 
 inc/helpers/media.php                   |    1 
 build/forms/view.asset.php              |    2 
 inc/meta/MetaForm.php                   |    5 
 inc/rest/routes/UploadRoutes.php        |   11 
 assets/js/concise/HandleSelectionOld.js |  392 +
 build/drawer-menu/block.json            |   27 
 inc/helpers/directory.php               |   53 
 assets/css/nav.min.css                  |    2 
 build/drawer-menu/index.asset.php       |    1 
 assets/js/min/uploader.min.js           |    2 
 inc/managers/ScriptLoader.php           |    1 
 src/drawer-menu/block.json              |   27 
 src/drawer-menu/view.js                 |    1 
 build/feed/view.asset.php               |    2 
 assets/js/concise/DataStore.js          |   96 
 src/drawer-menu/edit.js                 |   33 
 src/drawer-menu/render.php              |   39 
 src/list/render.php                     |    2 
 assets/js/min/dataStore.min.js          |    2 
 build/feed/view.js                      |    2 
 assets/css/dash.css                     |    1 
 src/drawer-menu/editor.scss             |    0 
 package.json                            |    2 
 build/forms/view.js                     |    2 
 inc/integrations/Helcim.php             |    8 
 assets/css/forms.css                    |    1 
 88 files changed, 10,890 insertions(+), 4,633 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 45fc12e..0b63b08 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -1,13 +1,16 @@
 <?php
 namespace JVBase;
 
+use JVBase\blocks\CustomBlocks;
 use JVBase\integrations\BlueSky;
+use JVBase\managers\CacheManager;
 use JVBase\managers\EmailManager;
 use JVBase\managers\ErrorHandler;
 use JVBase\managers\LoginManager;
 use JVBase\managers\MagicLinkManager;
 use JVBase\managers\OperationQueue;
 use JVBase\managers\DashboardManager;
+use JVBase\managers\DirectoryManager;
 use JVBase\managers\ReferralManager;
 use JVBase\managers\RoleManager;
 //use JVBase\managers\SchemaManager;
@@ -56,6 +59,7 @@
 	protected array $integrations = [];
     protected array $blocks = [];
     protected array $routes = [];
+	protected CustomBlocks $customBlocks;
 
 	protected array $serviceMap = [
 		'maps' => 'JVBase\integrations\GoogleMaps',
@@ -81,6 +85,7 @@
 
     public function __construct()
     {
+		$this->customBlocks = new CustomBlocks();
         $this->managers = [
             'errors'        => new ErrorHandler(),
             'queue'         => new OperationQueue(),
@@ -133,6 +138,10 @@
             $this->routes['term'] = new TermRoutes();
         }
 
+		if (Features::forSite()->has('is_directory')) {
+			$this->managers['directory'] = new DirectoryManager();
+		}
+
 
         if (jvbSiteHasDashboard()) {
             $this->routes['error']  = new ErrorRoutes();
@@ -193,11 +202,15 @@
     {
         return array_merge(array_keys($this->content), array_keys($this->taxonomies));
     }
-    public function dashboard()
+    public function dashboard():DashboardManager|false
     {
-        return $this->managers['dash'];
+        return $this->managers['dash']??false;
     }
-    public function error()
+	public function directories():DirectoryManager|false
+	{
+		return $this->managers['directory']??false;
+	}
+    public function error():ErrorHandler
     {
         return $this->managers['errors'];
     }
@@ -205,11 +218,11 @@
     {
         return $this->managers['file'];
     }
-    public function cache()
+    public function cache():CacheManager
     {
         return $this->managers['cache'];
     }
-    public function queue()
+    public function queue():OperationQueue
     {
         return $this->managers['queue'];
     }
@@ -217,9 +230,9 @@
 //    {
 //        return $this->managers['forms'];
 //    }
-    public function notification()
+    public function notification():NotificationManager|false
     {
-        return $this->managers['notifications'];
+        return $this->managers['notifications']??false;
     }
     public function routes($route):mixed
     {
@@ -228,7 +241,7 @@
         }
         return false;
     }
-    public function roles()
+    public function roles():RoleManager
     {
         return $this->managers['roles'];
     }
@@ -333,4 +346,9 @@
 			<?php
 		}
 	}
+
+	public function blocks():CustomBlocks|bool
+	{
+		return $this->customBlocks??false;
+	}
 }
diff --git a/assets/css/dash.css b/assets/css/dash.css
new file mode 100644
index 0000000..d3d300f
--- /dev/null
+++ b/assets/css/dash.css
@@ -0,0 +1 @@
+.icon-git-merge{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMTE0YTMwLDMwLDAsMCwwLTI5LjIxLDIzLjE5bC00NC02LjI4YTEwLDEwLDAsMCwxLTYuMTgtMy4zOUw5MS4xOCw4My44M0EzMCwzMCwwLDEsMCw3NCw4NS40djg1LjJhMzAsMzAsMCwxLDAsMTIsMFY5Ni4yMmwzMy41MiwzOS4xMWEyMiwyMiwwLDAsMCwxMy42LDcuNDZsNDUuMzUsNi40OEEzMCwzMCwwLDEsMCwyMDgsMTE0Wk02Miw1NkExOCwxOCwwLDEsMSw4MCw3NCwxOCwxOCwwLDAsMSw2Miw1NlpNOTgsMjAwYTE4LDE4LDAsMSwxLTE4LTE4QTE4LDE4LDAsMCwxLDk4LDIwMFptMTEwLTM4YTE4LDE4LDAsMSwxLDE4LTE4QTE4LDE4LDAsMCwxLDIwOCwxNjJaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsNzYuMjRsLTEyOCwxMjhhNiw2LDAsMCwxLTguNDgsMGwtNTYtNTZhNiw2LDAsMCwxLDguNDgtOC40OEw5NiwxOTEuNTEsMjE5Ljc2LDY3Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.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-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01Mi40NCwzNkE2LDYsMCwwLDAsNDMuNTYsNDRMNjQuNDQsNjdjLTM3LjI4LDIxLjktNTMuMjMsNTctNTMuOTIsNTguNTdhNiw2LDAsMCwwLDAsNC44OGMuMzQuNzcsOC42NiwxOS4yMiwyNy4yNCwzNy44QzU1LDE4NS40Nyw4NC42MiwyMDYsMTI4LDIwNmExMjQuOTEsMTI0LjkxLDAsMCwwLDUyLjU3LTExLjI1bDIzLDI1LjI5YTYsNiwwLDAsMCw4Ljg4LTguMDhabTQ4LjYyLDcxLjMyLDQ1LDQ5LjUyYTM0LDM0LDAsMCwxLTQ1LTQ5LjUyWk0xMjgsMTk0Yy0zMS4zOCwwLTU4Ljc4LTExLjQyLTgxLjQ1LTMzLjkzQTEzNC41NywxMzQuNTcsMCwwLDEsMjIuNjksMTI4YzQuMjktOC4yLDIwLjEtMzUuMTgsNTAtNTEuOTFMOTIuODksOTguM2E0Niw0NiwwLDAsMCw2MS4zNSw2Ny40OGwxNy44MSwxOS42QTExMy40NywxMTMuNDcsMCwwLDEsMTI4LDE5NFptNi40LTk5LjRhNiw2LDAsMCwxLDIuMjUtMTEuNzksNDYuMTcsNDYuMTcsMCwwLDEsMzcuMTUsNDAuODcsNiw2LDAsMCwxLTUuNDIsNi41M2wtLjU2LDBhNiw2LDAsMCwxLTYtNS40NUEzNC4xLDM0LjEsMCwwLDAsMTM0LjQsOTQuNlptMTExLjA4LDM1Ljg1Yy0uNDEuOTItMTAuMzcsMjMtMzIuODYsNDMuMTJhNiw2LDAsMSwxLTgtOC45NEExMzQuMDcsMTM0LjA3LDAsMCwwLDIzMy4zMSwxMjhhMTM0LjY3LDEzNC42NywwLDAsMC0yMy44Ni0zMi4wN0MxODYuNzgsNzMuNDIsMTU5LjM4LDYyLDEyOCw2MmExMjAuMTksMTIwLjE5LDAsMCwwLTE5LjY5LDEuNiw2LDYsMCwxLDEtMi0xMS44M0ExMzEuMTIsMTMxLjEyLDAsMCwxLDEyOCw1MGM0My4zOCwwLDczLDIwLjU0LDkwLjI0LDM3Ljc2LDE4LjU4LDE4LjU4LDI2LjksMzcsMjcuMjQsMzcuODFBNiw2LDAsMCwxLDI0NS40OCwxMzAuNDVaIi8+PC9zdmc+');}.icon-columns{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDQsMzRINjRBMTQsMTQsMCwwLDAsNTAsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0aDQwYTE0LDE0LDAsMCwwLDE0LTE0VjQ4QTE0LDE0LDAsMCwwLDEwNCwzNFptMiwxNzRhMiwyLDAsMCwxLTIsMkg2NGEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMlpNMTkyLDM0SDE1MmExNCwxNCwwLDAsMC0xNCwxNFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRoNDBhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMTkyLDM0Wm0yLDE3NGEyLDIsMCwwLDEtMiwySDE1MmEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.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-door{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMjE4SDIwNlY0MGExNCwxNCwwLDAsMC0xNC0xNEg2NEExNCwxNCwwLDAsMCw1MCw0MFYyMThIMjRhNiw2LDAsMCwwLDAsMTJIMjMyYTYsNiwwLDAsMCwwLTEyWk02Miw0MGEyLDIsMCwwLDEsMi0ySDE5MmEyLDIsMCwwLDEsMiwyVjIxOEg2MlptMTA0LDkyYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE2NiwxMzJaIi8+PC9zdmc+');}.icon-book-bookmark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMjZINzJBMzAsMzAsMCwwLDAsNDIsNTZWMjI0YTYsNiwwLDAsMCw2LDZIMTkyYTYsNiwwLDAsMCwwLTEySDU0di0yYTE4LDE4LDAsMCwxLDE4LTE4SDIwOGE2LDYsMCwwLDAsNi02VjMyQTYsNiwwLDAsMCwyMDgsMjZaTTExOCwzOGg1MnY3OEwxNDcuNTksOTkuMmE2LDYsMCwwLDAtNy4yLDBMMTE4LDExNlptODQsMTQ4SDcyYTI5Ljg3LDI5Ljg3LDAsMCwwLTE4LDZWNTZBMTgsMTgsMCwwLDEsNzIsMzhoMzR2OTBhNiw2LDAsMCwwLDkuNiw0LjhMMTQ0LDExMS41bDI4LjQxLDIxLjNBNiw2LDAsMCwwLDE4MiwxMjhWMzhoMjBaIi8+PC9zdmc+');}.icon-faders{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzQsMTIwdjk2YTYsNiwwLDAsMS0xMiwwVjEyMGE2LDYsMCwwLDEsMTIsMFptNjYsNzRhNiw2LDAsMCwwLTYsNnYxNmE2LDYsMCwwLDAsMTIsMFYyMDBBNiw2LDAsMCwwLDIwMCwxOTRabTI0LTMySDIwNlY0MGE2LDYsMCwwLDAtMTIsMFYxNjJIMTc2YTYsNiwwLDAsMCwwLDEyaDQ4YTYsNiwwLDAsMCwwLTEyWk01NiwxNjJhNiw2LDAsMCwwLTYsNnY0OGE2LDYsMCwwLDAsMTIsMFYxNjhBNiw2LDAsMCwwLDU2LDE2MlptMjQtMzJINjJWNDBhNiw2LDAsMCwwLTEyLDB2OTBIMzJhNiw2LDAsMCwwLDAsMTJIODBhNiw2LDAsMCwwLDAtMTJabTcyLTQ4SDEzNFY0MGE2LDYsMCwwLDAtMTIsMFY4MkgxMDRhNiw2LDAsMCwwLDAsMTJoNDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-robot{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDAsNTBIMTM0VjE2YTYsNiwwLDAsMC0xMiwwVjUwSDU2QTMwLDMwLDAsMCwwLDI2LDgwVjE5MmEzMCwzMCwwLDAsMCwzMCwzMEgyMDBhMzAsMzAsMCwwLDAsMzAtMzBWODBBMzAsMzAsMCwwLDAsMjAwLDUwWm0xOCwxNDJhMTgsMTgsMCwwLDEtMTgsMThINTZhMTgsMTgsMCwwLDEtMTgtMThWODBBMTgsMTgsMCwwLDEsNTYsNjJIMjAwYTE4LDE4LDAsMCwxLDE4LDE4Wk03NCwxMDhhMTAsMTAsMCwxLDEsMTAsMTBBMTAsMTAsMCwwLDEsNzQsMTA4Wm04OCwwYTEwLDEwLDAsMSwxLDEwLDEwQTEwLDEwLDAsMCwxLDE2MiwxMDhabTIsMzBIOTJhMjYsMjYsMCwwLDAsMCw1Mmg3MmEyNiwyNiwwLDAsMCwwLTUyWm0tMjIsMTJ2MjhIMTE0VjE1MFpNNzgsMTY0YTE0LDE0LDAsMCwxLDE0LTE0aDEwdjI4SDkyQTE0LDE0LDAsMCwxLDc4LDE2NFptODYsMTRIMTU0VjE1MGgxMGExNCwxNCwwLDAsMSwwLDI4WiIvPjwvc3ZnPg==');}.icon-plugs-connected{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTkuNzZhNiw2LDAsMCwwLTguNDgsMEwxNzMuOTQsNzMuNTdsLTYuNzktNi43OGEzMCwzMCwwLDAsMC00Mi40MiwwTDEwMCw5MS41MWwtNy43Ni03Ljc1YTYsNiwwLDAsMC04LjQ4LDguNDhMOTEuNTEsMTAwLDY2Ljc5LDEyNC43M2EzMCwzMCwwLDAsMCwwLDQyLjQybDYuNzgsNi43OUwxOS43NiwyMjcuNzZhNiw2LDAsMSwwLDguNDgsOC40OGw1My44Mi01My44MSw2Ljc5LDYuNzhhMzAsMzAsMCwwLDAsNDIuNDIsMEwxNTYsMTY0LjQ5bDcuNzYsNy43NWE2LDYsMCwwLDAsOC40OC04LjQ4TDE2NC40OSwxNTZsMjQuNzItMjQuNzNhMzAsMzAsMCwwLDAsMC00Mi40MmwtNi43OC02Ljc5LDUzLjgxLTUzLjgyQTYsNiwwLDAsMCwyMzYuMjQsMTkuNzZabS0xMTMuNDUsMTYxYTE4LDE4LDAsMCwxLTI1LjQ2LDBMNzUuMjcsMTU4LjY3YTE4LDE4LDAsMCwxLDAtMjUuNDZMMTAwLDEwOC40OSwxNDcuNTEsMTU2Wm01Ny45NC01Ny45NEwxNTYsMTQ3LjUxLDEwOC40OSwxMDBsMjQuNzItMjQuNzNhMTgsMTgsMCwwLDEsMjUuNDYsMGwyMi4wNiwyMi4wNmExOCwxOCwwLDAsMSwwLDI1LjQ2Wk05MC40MywzNC4yM2E2LDYsMCwwLDEsMTEuMTQtNC40Nmw4LDIwYTYsNiwwLDEsMS0xMS4xNCw0LjQ2Wm0tNjQsNTkuNTRhNiw2LDAsMCwxLDcuOC0zLjM0bDIwLDhhNiw2LDAsMSwxLTQuNDYsMTEuMTRsLTIwLThBNiw2LDAsMCwxLDI2LjQzLDkzLjc3Wm0yMDMuMTQsNjguNDZhNiw2LDAsMCwxLTcuOCwzLjM0bC0yMC04YTYsNiwwLDAsMSw0LjQ2LTExLjE0bDIwLDhBNiw2LDAsMCwxLDIyOS41NywxNjIuMjNabS02NCw1OS41NGE2LDYsMCwxLDEtMTEuMTQsNC40NmwtOC0yMGE2LDYsMCwwLDEsMTEuMTQtNC40NloiLz48L3N2Zz4=');}.icon-user-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wk03MS40NCwxOThhNjYsNjYsMCwwLDEsMTEzLjEyLDAsODkuOCw4OS44LDAsMCwxLTExMy4xMiwwWk05NCwxMjBhMzQsMzQsMCwxLDEsMzQsMzRBMzQsMzQsMCwwLDEsOTQsMTIwWm05OS41MSw2OS42NGE3Ny41Myw3Ny41MywwLDAsMC00MC0zMS4zOCw0Niw0NiwwLDEsMC01MSwwLDc3LjUzLDc3LjUzLDAsMCwwLTQwLDMxLjM4LDkwLDkwLDAsMSwxLDEzMSwwWiIvPjwvc3ZnPg==');}.icon-password{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw1NlYyMDBhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDBabTk0LjU4LDU2LjQxTDExOCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xNCwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xNC0xOS4yMSwyMi41OC03LjMzYTYsNiwwLDEsMC0zLjcxLTExLjQxWm0xMDMuNTYsMy44NWE2LDYsMCwwLDAtNy41Ni0zLjg1TDIxNCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xMy45NSwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xMy45NS0xOS4yMSwyMi41OC03LjMzQTYsNiwwLDAsMCwyNDQuMTQsMTE2LjI2WiIvPjwvc3ZnPg==');}
\ No newline at end of file
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index ae2a3ee..7435ee0 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(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin:0 auto 0 var(--btn_)!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{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-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(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.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(--action-0);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(--action-0);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:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%}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(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.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(--trans-base),width var(--trans-base),padding var(--trans-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(--trans-base),width var(--trans-base),padding var(--trans-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(--txt-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(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}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(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-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}.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(--radius-outer);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}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:var(--btn_)}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.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{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-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(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.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(--action-0);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(--action-0);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:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%}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(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.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(--trans-base),width var(--trans-base),padding var(--trans-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(--trans-base),width var(--trans-base),padding var(--trans-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(--txt-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(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}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(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-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}.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(--radius-outer);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}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
diff --git a/assets/css/forms.css b/assets/css/forms.css
new file mode 100644
index 0000000..18f0891
--- /dev/null
+++ b/assets/css/forms.css
@@ -0,0 +1 @@
+.icon-copy{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsMzRIODhhNiw2LDAsMCwwLTYsNlY4Mkg0MGE2LDYsMCwwLDAtNiw2VjIxNmE2LDYsMCwwLDAsNiw2SDE2OGE2LDYsMCwwLDAsNi02VjE3NGg0MmE2LDYsMCwwLDAsNi02VjQwQTYsNiwwLDAsMCwyMTYsMzRaTTE2MiwyMTBINDZWOTRIMTYyWm00OC00OEgxNzRWODhhNiw2LDAsMCwwLTYtNkg5NFY0NkgyMTBaIi8+PC9zdmc+');}.icon-paragraph{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsNDJIOTZhNjIsNjIsMCwwLDAsMCwxMjRoNDJ2NDJhNiw2LDAsMCwwLDEyLDBWNTRoMjhWMjA4YTYsNiwwLDAsMCwxMiwwVjU0aDE4YTYsNiwwLDAsMCwwLTEyWk0xMzgsMTU0SDk2QTUwLDUwLDAsMCwxLDk2LDU0aDQyWiIvPjwvc3ZnPg==');}.icon-text-h-one{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEydjk2YTYsNiwwLDAsMS0xMiwwVjEyMy4yMUwyMDMuMzMsMTMzYTYsNiwwLDAsMS02LjY2LTEwbDI0LTE2YTYsNiwwLDAsMSw5LjMzLDVaTTE0NCw1MGE2LDYsMCwwLDAtNiw2djU0SDQ2VjU2YTYsNiwwLDAsMC0xMiwwVjE3NmE2LDYsMCwwLDAsMTIsMFYxMjJoOTJ2NTRhNiw2LDAsMCwwLDEyLDBWNTZBNiw2LDAsMCwwLDE0NCw1MFoiLz48L3N2Zz4=');}.icon-text-h-two{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNTAsNTZWMTc2YTYsNiwwLDAsMS0xMiwwVjEyMkg0NnY1NGE2LDYsMCwwLDEtMTIsMFY1NmE2LDYsMCwwLDEsMTIsMHY1NGg5MlY1NmE2LDYsMCwwLDEsMTIsMFptOTAsMTQ2SDIwNEwyNDAsMTU0LjA1QTMwLDMwLDAsMSwwLDE4Ny43MSwxMjYsNiw2LDAsMSwwLDE5OSwxMzBhMTgsMTgsMCwwLDEsMTQuNDctMTEuODIsMTgsMTgsMCwwLDEsMTYuODcsMjguNjZMMTg3LjIsMjA0LjRBNiw2LDAsMCwwLDE5MiwyMTRoNDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-text-h-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTgwYTM0LDM0LDAsMCwxLTU4LjI5LDIzLjc5LDYsNiwwLDAsMSw4LjU4LTguMzlBMjIsMjIsMCwxLDAsMjEyLDE1OGE2LDYsMCwwLDEtNC45Mi05LjQ0TDIyOC40OCwxMThIMTkyYTYsNiwwLDAsMSwwLTEyaDQ4YTYsNiwwLDAsMSw0LjkxLDkuNDRsLTIyLjUyLDMyLjE4QTM0LjA2LDM0LjA2LDAsMCwxLDI0NiwxODBaTTE0NCw1MGE2LDYsMCwwLDAtNiw2djU0SDQ2VjU2YTYsNiwwLDAsMC0xMiwwVjE3NmE2LDYsMCwwLDAsMTIsMFYxMjJoOTJ2NTRhNiw2LDAsMCwwLDEyLDBWNTZBNiw2LDAsMCwwLDE0NCw1MFoiLz48L3N2Zz4=');}.icon-text-h-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNTAsNTZWMTc2YTYsNiwwLDAsMS0xMiwwVjEyMkg0NnY1NGE2LDYsMCwwLDEtMTIsMFY1NmE2LDYsMCwwLDEsMTIsMHY1NGg5MlY1NmE2LDYsMCwwLDEsMTIsMFpNMjU0LDE4NGE2LDYsMCwwLDEtNiw2SDIzOHYxOGE2LDYsMCwwLDEtMTIsMFYxOTBIMTc2YTYsNiwwLDAsMS00Ljc0LTkuNjhsNTYtNzJBNiw2LDAsMCwxLDIzOCwxMTJ2NjZoMTBBNiw2LDAsMCwxLDI1NCwxODRabS0yOC01NC41MUwxODguMjcsMTc4SDIyNloiLz48L3N2Zz4=');}.icon-text-h-five{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTgwYTM0LDM0LDAsMCwxLTM0LDM0LDMzLjYsMzMuNiwwLDAsMS0yNC4yOS05LjgsNiw2LDAsMCwxLDguNTgtOC40QTIxLjY1LDIxLjY1LDAsMCwwLDIxMiwyMDJhMjIsMjIsMCwwLDAsMC00NCwyMS42NSwyMS42NSwwLDAsMC0xNS43MSw2LjJBNiw2LDAsMCwxLDE4Ni4wOCwxNTlsOC00OGE2LDYsMCwwLDEsNS45Mi01aDQwYTYsNiwwLDAsMSwwLDEySDIwNS4wOGwtNSwzMEEzNiwzNiwwLDAsMSwyMTIsMTQ2LDM0LDM0LDAsMCwxLDI0NiwxODBaTTE0NCw1MGE2LDYsMCwwLDAtNiw2djU0SDQ2VjU2YTYsNiwwLDAsMC0xMiwwVjE3NmE2LDYsMCwwLDAsMTIsMFYxMjJoOTJ2NTRhNiw2LDAsMCwwLDEyLDBWNTZBNiw2LDAsMCwwLDE0NCw1MFoiLz48L3N2Zz4=');}.icon-text-h-six{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIsMTQ2YTM0LjUsMzQuNSwwLDAsMC01LjYuNDdsMTguNzUtMzEuMzlhNiw2LDAsMCwwLTEwLjMtNi4xNmwtMzIuMjUsNTQtLjIyLjQxQTM0LDM0LDAsMSwwLDIxMiwxNDZabTAsNTZhMjIsMjIsMCwxLDEsMjItMjJBMjIsMjIsMCwwLDEsMjEyLDIwMlpNMTUwLDU2VjE3NmE2LDYsMCwwLDEtMTIsMFYxMjJINDZ2NTRhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDB2NTRoOTJWNTZhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-text-italic{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNTZhNiw2LDAsMCwxLTYsNkgxNTYuMzJsLTQ0LDEzMkgxNDRhNiw2LDAsMCwxLDAsMTJINjRhNiw2LDAsMCwxLDAtMTJIOTkuNjhsNDQtMTMySDExMmE2LDYsMCwwLDEsMC0xMmg4MEE2LDYsMCwwLDEsMTk4LDU2WiIvPjwvc3ZnPg==');}.icon-text-underline{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsMjI0YTYsNiwwLDAsMS02LDZINjRhNiw2LDAsMCwxLDAtMTJIMTkyQTYsNiwwLDAsMSwxOTgsMjI0Wm0tNzAtMjZhNjIuMDcsNjIuMDcsMCwwLDAsNjItNjJWNTZhNiw2LDAsMCwwLTEyLDB2ODBhNTAsNTAsMCwwLDEtMTAwLDBWNTZhNiw2LDAsMCwwLTEyLDB2ODBBNjIuMDcsNjIuMDcsMCwwLDAsMTI4LDE5OFoiLz48L3N2Zz4=');}.icon-text-strikethrough{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTY5LjQ1YzExLjI4LDYuOTIsMjAuNTUsMTcuMzgsMjAuNTUsMzQsMCwyNS4zNi0yNy44MSw0Ni02Miw0NnMtNjItMjAuNjQtNjItNDZhNiw2LDAsMCwxLDEyLDBjMCwxOC43NSwyMi40MywzNCw1MCwzNHM1MC0xNS4yNSw1MC0zNGMwLTE4LjIzLTE1LjQ2LTI2LjU5LTQwLjQ3LTM0SDQwYTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNNzYuMzMsMTAyYTYuMiw2LjIsMCwwLDAsMS44OC0uM0E2LDYsMCwwLDAsODIsOTQuMTMsMTkuNzQsMTkuNzQsMCwwLDEsODEuMTEsODhjMC0xOS4zOCwyMC4xNi0zNCw0Ni44OS0zNCwxOS41OCwwLDM1LjU2LDcuODEsNDIuNzQsMjAuODlhNiw2LDAsMCwwLDEwLjUyLTUuNzhDMTcxLjk0LDUyLjEzLDE1Miw0MiwxMjgsNDIsOTQuNDMsNDIsNjkuMTEsNjEuNzcsNjkuMTEsODhhMzEuNjIsMzEuNjIsMCwwLDAsMS41Miw5Ljg3QTYsNiwwLDAsMCw3Ni4zMywxMDJaIi8+PC9zdmc+');}.icon-list-dashes{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik05MCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg5NkE2LDYsMCwwLDEsOTAsNjRabTEyNiw1OEg5NmE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNjRIOTZhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWk01Niw1OEg0MGE2LDYsMCwwLDAsMCwxMkg1NmE2LDYsMCwwLDAsMC0xMlptMCw2NEg0MGE2LDYsMCwwLDAsMCwxMkg1NmE2LDYsMCwwLDAsMC0xMlptMCw2NEg0MGE2LDYsMCwwLDAsMCwxMkg1NmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-list-numbers{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTA0YTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNMTA0LDcwSDIxNmE2LDYsMCwwLDAsMC0xMkgxMDRhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZIMTA0YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlpNNDIuNjgsNTMuMzcsNTAsNDkuNzFWMTA0YTYsNiwwLDAsMCwxMiwwVjQwYTYsNiwwLDAsMC04LjY4LTUuMzdsLTE2LDhhNiw2LDAsMCwwLDUuMzYsMTAuNzRaTTcyLDIwMkg1MmwyMS40OC0yOC43NEEyMS41LDIxLjUsMCwwLDAsNzcuNzksMTU3LDIxLjc1LDIxLjc1LDAsMCwwLDY5LDE0Mi4zOGEyMi44NiwyMi44NiwwLDAsMC0zMS4zNSw0LjMxLDIyLjE4LDIyLjE4LDAsMCwwLTMuMjgsNS45Miw2LDYsMCwwLDAsMTEuMjgsNC4xMSw5Ljg3LDkuODcsMCwwLDEsMS40OC0yLjY3LDEwLjc4LDEwLjc4LDAsMCwxLDE0Ljc4LTIsOS44OSw5Ljg5LDAsMCwxLDQsNi42MSw5LjY0LDkuNjQsMCwwLDEtMiw3LjI4bC0uMDYuMDlMMzUuMiwyMDQuNDFBNiw2LDAsMCwwLDQwLDIxNEg3MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-text-align-left{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRabTYsNDZIMTY4YTYsNiwwLDAsMCwwLTEySDQwYTYsNiwwLDAsMCwwLDEyWm0xNzYsMjhINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWm0tNDgsNDBINDBhNiw2LDAsMCwwLDAsMTJIMTY4YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-text-align-center{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRaTTY0LDk4YTYsNiwwLDAsMCwwLDEySDE5MmE2LDYsMCwwLDAsMC0xMlptMTUyLDQwSDQwYTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlptLTI0LDQwSDY0YTYsNiwwLDAsMCwwLDEySDE5MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-text-align-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRaTTIxNiw5OEg4OGE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNDBINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWm0wLDQwSDg4YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-link{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=');}.icon-file-pdf{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTUyYTYsNiwwLDAsMS02LDZIMTkwdjIwaDE4YTYsNiwwLDAsMSwwLDEySDE5MHYxOGE2LDYsMCwwLDEtMTIsMFYxNTJhNiw2LDAsMCwxLDYtNmgzMkE2LDYsMCwwLDEsMjIyLDE1MlpNOTAsMTcyYTI2LDI2LDAsMCwxLTI2LDI2SDU0djEwYTYsNiwwLDAsMS0xMiwwVjE1MmE2LDYsMCwwLDEsNi02SDY0QTI2LDI2LDAsMCwxLDkwLDE3MlptLTEyLDBhMTQsMTQsMCwwLDAtMTQtMTRINTR2MjhINjRBMTQsMTQsMCwwLDAsNzgsMTcyWm04NCw4YTM0LDM0LDAsMCwxLTM0LDM0SDExMmE2LDYsMCwwLDEtNi02VjE1MmE2LDYsMCwwLDEsNi02aDE2QTM0LDM0LDAsMCwxLDE2MiwxODBabS0xMiwwYTIyLDIyLDAsMCwwLTIyLTIySDExOHY0NGgxMEEyMiwyMiwwLDAsMCwxNTAsMTgwWk00MiwxMTJWNDBBMTQsMTQsMCwwLDEsNTYsMjZoOTZhNiw2LDAsMCwxLDQuMjUsMS43Nmw1Niw1NkE2LDYsMCwwLDEsMjE0LDg4djI0YTYsNiwwLDAsMS0xMiwwVjk0SDE1MmE2LDYsMCwwLDEtNi02VjM4SDU2YTIsMiwwLDAsMC0yLDJ2NzJhNiw2LDAsMCwxLTEyLDBaTTE1OCw4MmgzNS41MkwxNTgsNDYuNDhaIi8+PC9zdmc+');}.icon-file-csv{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00NiwxODBjMCwxMi4xMyw4LjA3LDIyLDE4LDIyYTE2LjI0LDE2LjI0LDAsMCwwLDExLjY3LTUuMjgsNiw2LDAsMCwxLDguNjYsOC4zQTI4LjA2LDI4LjA2LDAsMCwxLDY0LDIxNGMtMTYuNTQsMC0zMC0xNS4yNS0zMC0zNHMxMy40Ni0zNCwzMC0zNGEyOC4wNiwyOC4wNiwwLDAsMSwyMC4zMyw5LDYsNiwwLDAsMS04LjY2LDguM0ExNi4yMywxNi4yMywwLDAsMCw2NCwxNThDNTQuMDcsMTU4LDQ2LDE2Ny44Niw0NiwxODBabTgxLjA1LTYuNzdjLTEwLjg2LTMuMTMtMTMuNDEtNC42OS0xMy03LjkxYTYuNTksNi41OSwwLDAsMSwyLjg4LTUuMDhjNS42LTMuNzksMTcuNjYtMS44MiwyMS40NS0uODRhNiw2LDAsMCwwLDMuMDYtMTEuNmMtMi0uNTMtMjAuMS01LTMxLjIxLDIuNDhhMTguNjEsMTguNjEsMCwwLDAtOC4wOCwxMy41NGMtMS44LDE0LjE5LDEyLjI2LDE4LjI1LDIxLjU3LDIwLjk0LDEyLjEyLDMuNSwxNC43OCw1LjMzLDE0LjIsOS43NmE2Ljg1LDYuODUsMCwwLDEtMyw1LjM0Yy01LjYxLDMuNzMtMTcuNDgsMS42NC0yMS4xOS42MkE2LDYsMCwwLDAsMTEwLjQ4LDIxMmE1OS40MSw1OS40MSwwLDAsMCwxNC42OCwyYzUuNDksMCwxMS41NC0uOTUsMTYuMzYtNC4xNGExOC44OSwxOC44OSwwLDAsMCw4LjMxLTEzLjgxQzE1MS44NCwxODAuMzksMTM2LjkyLDE3Ni4wOCwxMjcuMDUsMTczLjIyWm04My0yNi44OGE2LDYsMCwwLDAtNy42NywzLjYzTDE4OCwxOTAuMTUsMTczLjY1LDE1MGE2LDYsMCwxLDAtMTEuMyw0bDIwLDU2YTYsNiwwLDAsMCwxMS4zLDBsMjAtNTZBNiw2LDAsMCwwLDIxMCwxNDYuMzRaTTIxNCw4OHYyNGE2LDYsMCwxLDEtMTIsMFY5NEgxNTJhNiw2LDAsMCwxLTYtNlYzOEg1NmEyLDIsMCwwLDAtMiwydjcyYTYsNiwwLDEsMS0xMiwwVjQwQTE0LDE0LDAsMCwxLDU2LDI2aDk2YTYsNiwwLDAsMSw0LjI0LDEuNzZsNTYsNTZBNiw2LDAsMCwxLDIxNCw4OFptLTIwLjQ5LTZMMTU4LDQ2LjQ4VjgyWiIvPjwvc3ZnPg==');}.icon-file-doc{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01MiwxNDZIMzZhNiw2LDAsMCwwLTYsNnY1NmE2LDYsMCwwLDAsNiw2SDUyYTM0LDM0LDAsMCwwLDAtNjhabTAsNTZINDJWMTU4SDUyYTIyLDIyLDAsMCwxLDAsNDRabTE2OC4xNS01LjQ2YTYsNiwwLDAsMSwuMTgsOC40OEEyOC4wNiwyOC4wNiwwLDAsMSwyMDAsMjE0Yy0xNi41NCwwLTMwLTE1LjI1LTMwLTM0czEzLjQ2LTM0LDMwLTM0YTI4LjA2LDI4LjA2LDAsMCwxLDIwLjMzLDksNiw2LDAsMCwxLTguNjYsOC4zQTE2LjIzLDE2LjIzLDAsMCwwLDIwMCwxNThjLTkuOTMsMC0xOCw5Ljg3LTE4LDIyczguMDcsMjIsMTgsMjJhMTYuMjMsMTYuMjMsMCwwLDAsMTEuNjctNS4yOEE2LDYsMCwwLDEsMjIwLjE1LDE5Ni41NFpNMTI4LDE0NmMtMTYuNTQsMC0zMCwxNS4yNS0zMCwzNHMxMy40NiwzNCwzMCwzNCwzMC0xNS4yNSwzMC0zNFMxNDQuNTQsMTQ2LDEyOCwxNDZabTAsNTZjLTkuOTMsMC0xOC05Ljg3LTE4LTIyczguMDctMjIsMTgtMjIsMTgsOS44NywxOCwyMlMxMzcuOTMsMjAyLDEyOCwyMDJaTTQ4LDExOGE2LDYsMCwwLDAsNi02VjQwYTIsMiwwLDAsMSwyLTJoOTBWODhhNiw2LDAsMCwwLDYsNmg1MHYxOGE2LDYsMCwwLDAsMTIsMFY4OGE2LDYsMCwwLDAtMS43Ni00LjI0bC01Ni01NkE2LDYsMCwwLDAsMTUyLDI2SDU2QTE0LDE0LDAsMCwwLDQyLDQwdjcyQTYsNiwwLDAsMCw0OCwxMThaTTE1OCw0Ni40OCwxOTMuNTIsODJIMTU4WiIvPjwvc3ZnPg==');}.icon-file-txt{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OCwxMThhNiw2LDAsMCwwLDYtNlY0MGEyLDIsMCwwLDEsMi0yaDkwVjg4YTYsNiwwLDAsMCw2LDZoNTB2MThhNiw2LDAsMCwwLDEyLDBWODhhNiw2LDAsMCwwLTEuNzYtNC4yNGwtNTYtNTZBNiw2LDAsMCwwLDE1MiwyNkg1NkExNCwxNCwwLDAsMCw0Miw0MHY3MkE2LDYsMCwwLDAsNDgsMTE4Wk0xNTgsNDYuNDgsMTkzLjUyLDgySDE1OFptLTUuMTIsMTA5TDEzNS4zNywxODBsMTcuNTEsMjQuNTFhNiw2LDAsMSwxLTkuNzYsN0wxMjgsMTkwLjMybC0xNS4xMiwyMS4xN2E2LDYsMCwwLDEtOS43Ni03TDEyMC42MywxODBsLTE3LjUxLTI0LjUxYTYsNiwwLDEsMSw5Ljc2LTdMMTI4LDE2OS42OGwxNS4xMi0yMS4xN2E2LDYsMCwwLDEsOS43Niw3Wk05MCwxNTJhNiw2LDAsMCwxLTYsNkg3MHY1MGE2LDYsMCwwLDEtMTIsMFYxNThINDRhNiw2LDAsMCwxLDAtMTJIODRBNiw2LDAsMCwxLDkwLDE1MlptMTI4LDBhNiw2LDAsMCwxLTYsNkgxOTh2NTBhNiw2LDAsMCwxLTEyLDBWMTU4SDE3MmE2LDYsMCwwLDEsMC0xMmg0MEE2LDYsMCwwLDEsMjE4LDE1MloiLz48L3N2Zz4=');}.icon-file-xls{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNTQsMjA4YTYsNiwwLDAsMS02LDZIMTIwYTYsNiwwLDAsMS02LTZWMTUyYTYsNiwwLDEsMSwxMiwwdjUwaDIyQTYsNiwwLDAsMSwxNTQsMjA4Wk05MS40OCwxNDcuMTFhNiw2LDAsMCwwLTguMzYsMS4zOUw2OCwxNjkuNjcsNTIuODgsMTQ4LjVhNiw2LDAsMSwwLTkuNzYsN0w2MC42MywxODAsNDMuMTIsMjA0LjVhNiw2LDAsMSwwLDkuNzYsN0w2OCwxOTAuMzFsMTUuMTIsMjEuMTZBNiw2LDAsMCwwLDg4LDIxNGE1LjkxLDUuOTEsMCwwLDAsMy40OC0xLjEyLDYsNiwwLDAsMCwxLjQtOC4zN0w3NS4zNywxODBsMTcuNTEtMjQuNTFBNiw2LDAsMCwwLDkxLjQ4LDE0Ny4xMVpNMTkxLDE3My4yMmMtMTAuODUtMy4xMy0xMy40MS00LjY5LTEzLTcuOTFhNi41OSw2LjU5LDAsMCwxLDIuODgtNS4wOGM1LjYtMy43OSwxNy42NS0xLjgzLDIxLjQ0LS44NGE2LDYsMCwwLDAsMy4wNy0xMS42Yy0yLS41NC0yMC4xLTUtMzEuMjEsMi40OGExOC42NCwxOC42NCwwLDAsMC04LjA4LDEzLjU0Yy0xLjgsMTQuMTksMTIuMjYsMTguMjUsMjEuNTcsMjAuOTQsMTIuMTIsMy41LDE0Ljc3LDUuMzMsMTQuMiw5Ljc2YTYuODUsNi44NSwwLDAsMS0zLDUuMzRjLTUuNjEsMy43My0xNy40OCwxLjY0LTIxLjE5LjYyQTYsNiwwLDAsMCwxNzQuNDcsMjEyYTU5LjQxLDU5LjQxLDAsMCwwLDE0LjY4LDJjNS40OSwwLDExLjU0LS45NSwxNi4zNi00LjE0YTE4Ljg5LDE4Ljg5LDAsMCwwLDguMzEtMTMuODFDMjE1LjgzLDE4MC4zOSwyMDAuOTEsMTc2LjA4LDE5MSwxNzMuMjJaTTQyLDExMlY0MEExNCwxNCwwLDAsMSw1NiwyNmg5NmE2LDYsMCwwLDEsNC4yNCwxLjc2bDU2LDU2QTYsNiwwLDAsMSwyMTQsODh2MjRhNiw2LDAsMSwxLTEyLDBWOTRIMTUyYTYsNiwwLDAsMS02LTZWMzhINTZhMiwyLDAsMCwwLTIsMnY3MmE2LDYsMCwxLDEtMTIsMFpNMTU4LDgySDE5My41TDE1OCw0Ni40OFoiLz48L3N2Zz4=');}.icon-text-b-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjgsMTU2YTIwLDIwLDAsMCwxLTIwLDIwSDk2VjEzNmg1MkEyMCwyMCwwLDAsMSwxNjgsMTU2Wk0yMjQsNDhWMjA4YTE2LDE2LDAsMCwxLTE2LDE2SDQ4YTE2LDE2LDAsMCwxLTE2LTE2VjQ4QTE2LDE2LDAsMCwxLDQ4LDMySDIwOEExNiwxNiwwLDAsMSwyMjQsNDhaTTE4NCwxNTZhMzYsMzYsMCwwLDAtMTgtMzEuMTVBMzYsMzYsMCwwLDAsMTQwLDY0SDg4YTgsOCwwLDAsMC04LDhWMTg0YTgsOCwwLDAsMCw4LDhoNjBBMzYsMzYsMCwwLDAsMTg0LDE1NlptLTI0LTU2YTIwLDIwLDAsMCwwLTIwLTIwSDk2djQwaDQ0QTIwLDIwLDAsMCwwLDE2MCwxMDBaIi8+PC9zdmc+');}
\ No newline at end of file
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index bc9ce27..d108d0a 100644
--- a/assets/css/forms.min.css
+++ b/assets/css/forms.min.css
@@ -1 +1 @@
-input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}.selected-items{--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.selected-item{padding:.25rem .5rem;margin:.125em;background:var(--base-100);border-radius:.25rem;font-size:var(--txt-medium);border:1px solid var(--base-200);position:relative}.remove-item{background:0 0;border:none;padding:.25rem;cursor:pointer;color:#666;border-radius:var(--radius);width:1.5em;height:1.5em}.remove-item .close{width:.5em;height:.5em}.remove-item:hover{color:var(--action-0);background:#fee}.clear-filters{margin-left:auto;border:1px solid var(--base-200)}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:38px;height:38px;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.items-container{margin:0;padding:0;width:100%}.create-new-term{margin-top:1rem;width:100%}.create-new-term .field,.create-new-term[open] summary{margin-bottom:1rem}.create-new-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(1fr,100%))}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}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(--content)}}.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(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));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,.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(--op-1))}.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(--op-4));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(--radius);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(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .group-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:var(--btn_);right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);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(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!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(--z-9);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(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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:rgba(var(--base-rgb),var(--op-45)) var(--shdw);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(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));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(--btn);bottom:var(--btn);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(--op-6));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(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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(--op-6))}.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(--btn);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(--btnbtn));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:rgba(var(--base-rgb),var(--op-45)) var(--shdw);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(--btn_);bottom:var(--btn_);left:1rem;right:1rem;border-radius:var(--radius-outer);padding:1rem;z-index:var(--z-7);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);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:rgba(var(--base-rgb),var(--op-45)) var(--shdw-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(--radius);border-top-right-radius:var(--radius);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(--radius);border-bottom-right-radius:var(--radius);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 rgba(var(--base-rgb),var(--op-6));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(--radius);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(--txt-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(--txt-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(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-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(--txt-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{min-height:0;padding:.5rem}.success-message{color:var(--success);background-color:var(--successBack);border:1px solid var(--success);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(--successBack);border:2px solid var(--success);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success);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)}.error-message{color:var(--error);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--errorBack);border:1px solid var(--error);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success)}.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:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
+input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}.selected-items{--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.selected-item{padding:.25rem .5rem;margin:.125em;background:var(--base-100);border-radius:.25rem;font-size:var(--txt-medium);border:1px solid var(--base-200);position:relative}.remove-item{background:0 0;border:none;padding:.25rem;cursor:pointer;color:#666;border-radius:var(--radius);width:1.5em;height:1.5em}.remove-item .close{width:.5em;height:.5em}.remove-item:hover{color:var(--action-0);background:#fee}.clear-filters{margin-left:auto;border:1px solid var(--base-200)}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:38px;height:38px;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.items-container{margin:0;padding:0;width:100%}.create-new-term{margin-top:1rem;width:100%}.create-new-term .field,.create-new-term[open] summary{margin-bottom:1rem}.create-new-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(1fr,100%))}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}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(--content)}}.empty-group,.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(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.empty-group,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));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}.empty-group p,.file-upload-text{color:var(--contrast);margin:0}.empty-group p strong,.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.groups{grid-template-columns:repeat(1,1fr)}.item-grid.group{margin-bottom:0}.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;padding-left:var(--chipchip)}.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(--op-1))}.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(--op-4));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(--radius);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(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .selection-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:0;right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);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(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!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(--z-9);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(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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:rgba(var(--base-rgb),var(--op-45)) var(--shdw);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(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));transform:scale(1.04)}}.selection-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(--btn);bottom:var(--btn);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(--op-6));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{--wrap:nowrap;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(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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(--op-6))}.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(--btn);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;--wrap:nowrap;max-height:calc(100vh - var(--btnbtn));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:rgba(var(--base-rgb),var(--op-45)) var(--shdw);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}}.item-grid.restore{grid-template-columns:repeat(1,1fr)}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw-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(--radius);border-top-right-radius:var(--radius);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(--radius);border-bottom-right-radius:var(--radius);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 rgba(var(--base-rgb),var(--op-6));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(--radius);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(--txt-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(--txt-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(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-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(--txt-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{min-height:0;padding:.5rem}.success-message{color:var(--success);background-color:var(--successBack);border:1px solid var(--success);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(--successBack);border:2px solid var(--success);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success);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)}.error-message{color:var(--error);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--errorBack);border:1px solid var(--error);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success)}.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:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
diff --git a/assets/css/icons.css b/assets/css/icons.css
index e69de29..22be381 100644
--- a/assets/css/icons.css
+++ b/assets/css/icons.css
@@ -0,0 +1 @@
+.icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMSwxLTIxLjQ5LTU5LjgyLDYsNiwwLDEsMS05LjI1LDcuNjRBODIsODIsMCwxLDAsMjA5Ljc4LDEzNEgxMjhhNiw2LDAsMCwxLDAtMTJoODhBNiw2LDAsMCwxLDIyMiwxMjhaIi8+PC9zdmc+');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTkuNCwxNjcuODRDMjAxLjcxLDE1NS42OSwxOTgsMTM1LjEyLDE5OCwxMjBjMC0xOC40MiwxMy44Ni0zNC4yOSwyMi4xMi00Mi4xMmE2LDYsMCwwLDAsMC04LjcxQzIwOCw1Ny43LDE4Ny4wNyw1MCwxNjgsNTBhNzAuMjMsNzAuMjMsMCwwLDAtNDAsMTIuNTUsNjkuNiw2OS42LDAsMCwwLTg5LjMxLDguMDhBNzIuNjMsNzIuNjMsMCwwLDAsMTgsMTIzLjM1YTEyNS4xMSwxMjUuMTEsMCwwLDAsMzkuNTMsODguMzNBMzcuODUsMzcuODUsMCwwLDAsODMuNiwyMjJoODcuN0EzNy44MywzNy44MywwLDAsMCwxOTksMjEwLjA3YTEyMi42LDEyMi42LDAsMCwwLDE3LjU0LTI0LjJjNi41NS0xMiw1Ljc3LTEzLjc1LDUtMTUuNDhBNi4wNyw2LjA3LDAsMCwwLDIxOS40LDE2Ny44NFptLTI5LjIzLDM0QTI1LjgyLDI1LjgyLDAsMCwxLDE3MS4zLDIxMEg4My42QTI1Ljg1LDI1Ljg1LDAsMCwxLDY1Ljc4LDIwMywxMTMuMjEsMTEzLjIxLDAsMCwxLDMwLDEyM2E2MC41NSw2MC41NSwwLDAsMSwxNy4yMS00NEE1Ni44Miw1Ni44MiwwLDAsMSw4OCw2MmguODFhNTcuMzUsNTcuMzUsMCwwLDEsMzUuNDQsMTIuNzEsNiw2LDAsMCwwLDcuNSwwQTU3LjM5LDU3LjM5LDAsMCwxLDE2OCw2MmMxMy44OSwwLDI4LjgxLDQuNjgsMzkuMTEsMTItOS40NCwxMC4xNC0yMS4xLDI2LjU5LTIxLjEsNDYsMCwyMy43OCw3LjgxLDQyLjYsMjIuNjYsNTQuNzdBMTA3LjMzLDEwNy4zMywwLDAsMSwxOTAuMTcsMjAxLjg5Wm0tNjAtMTcxLjM5QTM4LDM4LDAsMCwxLDE2NywyaDFhNiw2LDAsMCwxLDAsMTJoLTFhMjYsMjYsMCwwLDAtMjUuMTgsMTkuNSw2LDYsMCwxLDEtMTEuNjItM1oiLz48L3N2Zz4=');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=');}.icon-cloud-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01Mi40NCwzNkE2LDYsMCwwLDAsNDMuNTYsNDRsNDAuMTgsNDQuMmMtLjQ1Ljg3LS45LDEuNzUtMS4zMiwyLjY0QTYyLDYyLDAsMSwwLDcyLDIxNGg4OGE4NS4yMyw4NS4yMywwLDAsMCwzMi4zNS02LjNMMjAzLjU2LDIyMGE2LDYsMCwwLDAsOC44OC04LjA4Wk0xNjAsMjAySDcyYTUwLDUwLDAsMSwxLDUuOS05OS42NEE4Ni4yNSw4Ni4yNSwwLDAsMCw3NCwxMjhhNiw2LDAsMCwwLDEyLDAsNzMuOTIsNzMuOTIsMCwwLDEsNi40NC0zMC4ybDkxLjIyLDEwMC4zNEE3My42NSw3My42NSwwLDAsMSwxNjAsMjAyWm04Ni03NGE4NS44NSw4NS44NSwwLDAsMS0yMS44NSw1Ny4yNyw2LDYsMCwwLDEtNC40NywyLDYsNiwwLDAsMS00LjQ3LTEwLDc0LDc0LDAsMCwwLTk5LTEwOC45Miw2LDYsMCwxLDEtNy4xMS05LjY3QTg2LDg2LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-exclamation-mark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTg1LjI3LDg1LjI3LDAsMCwxLTE3LjIsNTEuNiw2LDYsMCwxLDEtOS42LTcuMkE3NCw3NCwwLDEsMCw4NiwxMjhhNiw2LDAsMCwxLTEyLDAsODUuNTQsODUuNTQsMCwwLDEsMy45MS0yNS42NEE1MC42OCw1MC42OCwwLDAsMCw3MiwxMDJhNTAsNTAsMCwwLDAsMCwxMDBIOTZhNiw2LDAsMCwxLDAsMTJINzJBNjIsNjIsMCwxLDEsODIuNDMsOTAuODgsODYsODYsMCwwLDEsMjQ2LDEyOFptLTY2LjI0LDQzLjc2TDE1OCwxOTMuNTFWMTI4YTYsNiwwLDAsMC0xMiwwdjY1LjUxbC0yMS43Ni0yMS43NWE2LDYsMCwwLDAtOC40OCw4LjQ4bDMyLDMyYTYsNiwwLDAsMCw4LjQ4LDBsMzItMzJhNiw2LDAsMCwwLTguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTAwLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OCwwbC04MC04MGE2LDYsMCwwLDEsOC40OC04LjQ4TDEyOCwxNjcuNTFsNzUuNzYtNzUuNzVhNiw2LDAsMCwxLDguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODguMjQsMTY0LjI0YTYsNiwwLDAsMS04LjQ4LDBMMTU4LDE0Mi40OVYyMDhhNiw2LDAsMCwxLTEyLDBWMTQyLjQ5bC0yMS43NiwyMS43NWE2LDYsMCwwLDEtOC40OC04LjQ4bDMyLTMyYTYsNiwwLDAsMSw4LjQ4LDBsMzIsMzJBNiw2LDAsMCwxLDE4OC4yNCwxNjQuMjRaTTE2MCw0MkE4Ni4xLDg2LjEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDQwYTYsNiwwLDAsMCwwLTEySDcyYTUwLDUwLDAsMCwxLDAtMTAwLDUwLjY4LDUwLjY4LDAsMCwxLDUuOTEuMzZBODUuNTQsODUuNTQsMCwwLDAsNzQsMTI4YTYsNiwwLDAsMCwxMiwwLDc0LDc0LDAsMSwxLDEwMy42LDY3Ljg1LDYsNiwwLDAsMCw0LjgsMTFBODYsODYsMCwwLDAsMTYwLDQyWiIvPjwvc3ZnPg==');}.icon-cloud-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMzYuMjQtOTQuMjRhNiw2LDAsMCwxLDAsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxNDQsMTUxLjUxbDQzLjc2LTQzLjc1QTYsNiwwLDAsMSwxOTYuMjQsMTA3Ljc2WiIvPjwvc3ZnPg==');}.icon-cloud-warning{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptLTYtNzRWODhhNiw2LDAsMCwxLDEyLDB2NDBhNiw2LDAsMCwxLTEyLDBabTE2LDM2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE3MCwxNjRaIi8+PC9zdmc+');}.icon-syncing{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iY3VycmVudENvbG9yIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PHBhdGggaWQ9InJlZnJlc2giIGQ9Ik0xNjAuMDQ3IDEyMi44NzVhMzAuNzg0IDMwLjc4NCAwIDAgMC0yMS43NSA4Ljc5N2MtMi44NDIgMy4wMDMtLjQ2NyA0Ljk3MSAxLjMxMiAzLjE1NiAxMS4wNDMtMTAuNzg2IDI4LjcxLTEwLjY4IDM5LjYyNS4yMzRsNy4yMDMgNy4yMDRoLTEyLjg3NWMtMy4zNDcuMDA4LTMuMTY1IDMuODc1IDAgMy44NzVoMTYuMTFjMi4wNjIgMCAyLjU0LTEuNDE4IDIuNTYyLTQuOTdsLjA5NC0xNC45MjFjLjAyLTMuMjktMy40MzctMy4xNjUtMy40MzcgMHYxMi44NmwtNy4yMDMtNy4xODhhMzAuNzY4IDMwLjc2OCAwIDAgMC0yMS42NDEtOS4wNDd6bS0yOS41OTQgMzkuNzk3Yy0yLjA2MiAwLTIuNTI0IDEuNDAyLTIuNTQ3IDQuOTUzbC0uMDk0IDE0LjkyMmMtLjAyIDMuMjkgMy40MjIgMy4xNjQgMy40MjIgMHYtMTIuODZsNy4yMDMgNy4yMDRjMTEuOTU2IDExLjk1NSAzMS4zMTIgMTIuMDY0IDQzLjQwNy4yNSAyLjg0Mi0zLjAwMy40NTEtNC45ODgtMS4zMjgtMy4xNzItMTEuMDQzIDEwLjc4Ni0yOC43MSAxMC42OC0zOS42MjUtLjIzNWwtNy4xODgtNy4yMDNoMTIuODZjMy4zNDctLjAwOCAzLjE2NS0zLjg2IDAtMy44NmgtMTYuMTF6Ii8+PHBhdGggZD0iTTE2MCA0NGE4NC4xMSA4NC4xMSAwIDAgMC03Ni40MSA0OS4xMkE2MC43MSA2MC43MSAwIDAgMCA3MiA5MmE2MCA2MCAwIDAgMCAwIDEyMGg4OGE4NCA4NCAwIDAgMCAwLTE2OFptMCAxNjBINzJhNTIgNTIgMCAxIDEgOC41NS0xMDMuM0E4My42NiA4My42NiAwIDAgMCA3NiAxMjhhNCA0IDAgMCAwIDggMCA3NiA3NiAwIDEgMSA3NiA3NloiLz48L3N2Zz4=');}.icon-cloud-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMjguMjQtODUuNzZMMTY4LjQ4LDEzNmwxOS43NiwxOS43NmE2LDYsMCwxLDEtOC40OCw4LjQ4TDE2MCwxNDQuNDhsLTE5Ljc2LDE5Ljc2YTYsNiwwLDAsMS04LjQ4LTguNDhMMTUxLjUyLDEzNmwtMTkuNzYtMTkuNzZhNiw2LDAsMCwxLDguNDgtOC40OEwxNjAsMTI3LjUybDE5Ljc2LTE5Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=');}.icon-share-fat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTA3Ljc2bC04MC04MEE2LDYsMCwwLDAsMTQ2LDMyVjc0LjJjLTU0LjQ4LDMuNTktMTIwLjM5LDU1LTEyNy45MywxMjAuNjZhMTAsMTAsMCwwLDAsMTcuMjMsOGgwQzQ2LjU2LDE5MC44NSw4NywxNTIuNiwxNDYsMTUwLjEzVjE5MmE2LDYsMCwwLDAsMTAuMjQsNC4yNGw4MC04MEE2LDYsMCwwLDAsMjM2LjI0LDEwNy43NlpNMTU4LDE3Ny41MlYxNDRhNiw2LDAsMCwwLTYtNmMtMjcuNzMsMC01NC43Niw3LjI1LTgwLjMyLDIxLjU1YTE5My4zOCwxOTMuMzgsMCwwLDAtNDAuODEsMzAuNjVjNC43LTI2LjU2LDIwLjE2LTUyLDQ0LTcyLjI3Qzk4LjQ3LDk3Ljk0LDEyNy4yOSw4NiwxNTIsODZhNiw2LDAsMCwwLDYtNlY0Ni40OUwyMjMuNTEsMTEyWiIvPjwvc3ZnPg==');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBIMTc0VjQwYTIyLDIyLDAsMCwwLTIyLTIySDEwNEEyMiwyMiwwLDAsMCw4Miw0MFY1MEg0MGE2LDYsMCwwLDAsMCwxMkg1MFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRIMTkyYTE0LDE0LDAsMCwwLDE0LTE0VjYyaDEwYTYsNiwwLDAsMCwwLTEyWk05NCw0MGExMCwxMCwwLDAsMSwxMC0xMGg0OGExMCwxMCwwLDAsMSwxMCwxMFY1MEg5NFpNMTk0LDIwOGEyLDIsMCwwLDEtMiwySDY0YTIsMiwwLDAsMS0yLTJWNjJIMTk0Wk0xMTAsMTA0djY0YTYsNiwwLDAsMS0xMiwwVjEwNGE2LDYsMCwwLDEsMTIsMFptNDgsMHY2NGE2LDYsMCwwLDEtMTIsMFYxMDRhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-scribble{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjUsMTg4LjI0YTE2LjYzLDE2LjYzLDAsMCwwLDAsMjMuNTIsNiw2LDAsMSwxLTguNDgsOC40OCwyOC42MSwyOC42MSwwLDAsMSwwLTQwLjQ4bDkuMzctOS4zOGExNi42MywxNi42MywwLDAsMC0yMy41Mi0yMy41MWwtNjYuNzUsNjYuNzVhMjguNjMsMjguNjMsMCwwLDEtNDAuNDktNDAuNDlsOTguNzYtOTguNzVhMTYuNjMsMTYuNjMsMCwwLDAtMjMuNTItMjMuNTFMODIuODYsMTE3LjYyQTI4LjYzLDI4LjYzLDAsMCwxLDQyLjM3LDc3LjEzTDgzLjc1LDM1Ljc2YTYsNiwwLDEsMSw4LjQ5LDguNDhMNTAuODYsODUuNjJhMTYuNjMsMTYuNjMsMCwwLDAsMjMuNTIsMjMuNTFsNjYuNzUtNjYuNzVhMjguNjMsMjguNjMsMCwwLDEsNDAuNDksNDAuNDlMODIuODYsMTgxLjYyYTE2LjYzLDE2LjYzLDAsMCwwLDIzLjUyLDIzLjUxbDY2Ljc2LTY2Ljc1YTI4LjYzLDI4LjYzLDAsMCwxLDQwLjQ5LDQwLjQ5WiIvPjwvc3ZnPg==');}.icon-brackets-angle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04NS4wNiw0My4yMiwzMS4xMSwxMjhsNTQsODQuNzhhNiw2LDAsMCwxLTEuODQsOC4yOCw2LDYsMCwwLDEtOC4yOC0xLjg0bC01Ni04OGE2LDYsMCwwLDEsMC02LjQ0bDU2LTg4YTYsNiwwLDAsMSwxMC4xMiw2LjQ0Wm0xNTIsODEuNTYtNTYtODhhNiw2LDAsMSwwLTEwLjEyLDYuNDRMMjI0Ljg5LDEyOGwtNTMuOTUsODQuNzhhNiw2LDAsMCwwLDEuODQsOC4yOCw2LDYsMCwwLDAsOC4yOC0xLjg0bDU2LTg4QTYsNiwwLDAsMCwyMzcuMDYsMTI0Ljc4WiIvPjwvc3ZnPg==');}.icon-brain{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI0YTU0LjEzLDU0LjEzLDAsMCwwLTMyLTQ5LjMzVjcyYTQ2LDQ2LDAsMCwwLTg2LTIyLjY3QTQ2LDQ2LDAsMCwwLDQyLDcydjIuNjdhNTQsNTQsMCwwLDAsMCw5OC42M1YxNzZhNDYsNDYsMCwwLDAsODYsMjIuNjdBNDYsNDYsMCwwLDAsMjE0LDE3NnYtMi43QTU0LjA3LDU0LjA3LDAsMCwwLDI0NiwxMjRaTTg4LDIxMGEzNCwzNCwwLDAsMS0zNC0zMi45NEE1My42Nyw1My42NywwLDAsMCw2NCwxNzhoOGE2LDYsMCwwLDAsMC0xMkg2NEE0Miw0MiwwLDAsMSw1MCw4NC4zOWE2LDYsMCwwLDAsNC01LjY2VjcyYTM0LDM0LDAsMCwxLDY4LDB2NzMuMDVBNDUuODksNDUuODksMCwwLDAsODgsMTMwYTYsNiwwLDAsMCwwLDEyLDM0LDM0LDAsMCwxLDAsNjhabTEwNC00NGgtOGE2LDYsMCwwLDAsMCwxMmg4YTUzLjY3LDUzLjY3LDAsMCwwLDEwLS45NEEzNCwzNCwwLDEsMSwxNjgsMTQyYTYsNiwwLDAsMCwwLTEyLDQ1Ljg5LDQ1Ljg5LDAsMCwwLTM0LDE1LjA1VjcyYTM0LDM0LDAsMCwxLDY4LDB2Ni43M2E2LDYsMCwwLDAsNCw1LjY2QTQyLDQyLDAsMCwxLDE5MiwxNjZabTE0LTU0YTYsNiwwLDAsMS02LDZoLTRhMzQsMzQsMCwwLDEtMzQtMzRWODBhNiw2LDAsMCwxLDEyLDB2NGEyMiwyMiwwLDAsMCwyMiwyMmg0QTYsNiwwLDAsMSwyMDYsMTEyWk02MCwxMThINTZhNiw2LDAsMCwxLDAtMTJoNEEyMiwyMiwwLDAsMCw4Miw4NFY4MGE2LDYsMCwwLDEsMTIsMHY0QTM0LDM0LDAsMCwxLDYwLDExOFoiLz48L3N2Zz4=');}.icon-palette{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTkuMzcsNTUuMzFBMTAxLjMyLDEwMS4zMiwwLDAsMCwxMjgsMjZoLTFBMTAyLDEwMiwwLDAsMCwyNiwxMjhjMCw0Mi4wOSwyNi4wNyw3Ny40NCw2OCw5Mi4yNkEzMC4yMSwzMC4yMSwwLDAsMCwxMDQuMTEsMjIyLDMwLjA2LDMwLjA2LDAsMCwwLDEzNCwxOTJhMTgsMTgsMCwwLDEsMTgtMThoNDYuMjFhMjkuODIsMjkuODIsMCwwLDAsMjkuMjUtMjMuMzFBMTAyLjcxLDEwMi43MSwwLDAsMCwyMzAsMTI3LjExLDEwMS4yNSwxMDEuMjUsMCwwLDAsMTk5LjM3LDU1LjMxWk0yMTUuNzYsMTQ4YTE3Ljg5LDE3Ljg5LDAsMCwxLTE3LjU1LDE0SDE1MmEzMCwzMCwwLDAsMC0zMCwzMCwxOCwxOCwwLDAsMS0yNCwxN0M2MSwxOTUuODYsMzgsMTY0Ljg1LDM4LDEyOGE5MCw5MCwwLDAsMSw4OS4wNy05MEgxMjhhOTAuMzQsOTAuMzQsMCwwLDEsOTAsODkuMjJBOTAuNDYsOTAuNDYsMCwwLDEsMjE1Ljc2LDE0OFpNMTM4LDc2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCw3NlpNOTQsMTAwQTEwLDEwLDAsMSwxLDg0LDkwLDEwLDEwLDAsMCwxLDk0LDEwMFptMCw1NmExMCwxMCwwLDEsMS0xMC0xMEExMCwxMCwwLDAsMSw5NCwxNTZabTg4LTU2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE4MiwxMDBaIi8+PC9zdmc+');}.icon-pen-nib{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsOTIuNjhhMTMuOTQsMTMuOTQsMCwwLDAtNC4xLTkuOUwxNzMuMjEsMTQuMWExNCwxNCwwLDAsMC0xOS44LDBMMTI0LjY4LDQyLjgzLDY2LjIyLDY0Ljc2YTE0LDE0LDAsMCwwLTguOSwxMC44TDM0LjA4LDIxNUE2LDYsMCwwLDAsNDAsMjIyYTYuNjEsNi42MSwwLDAsMCwxLS4wOGwxMzkuNDQtMjMuMjRhMTQsMTQsMCwwLDAsMTAuODEtOC45bDIxLjkyLTU4LjQ2LDI4Ljc0LTI4Ljc0QTEzLjkyLDEzLjkyLDAsMCwwLDI0Niw5Mi42OFptLTY2LDkyLjg5YTIsMiwwLDAsMS0xLjU0LDEuMjdMNTcuNDksMjA3bDUyLjg3LTUyLjg4YTI2LDI2LDAsMSwwLTguNDgtOC40OEw0OSwxOTguNTNsMjAuMTctMTIxQTIsMiwwLDAsMSw3MC40Myw3Nmw1Ni4wNi0yMUwyMDEsMTI5LjUxWk0xMTAsMTMyYTE0LDE0LDAsMSwxLDE0LDE0QTE0LDE0LDAsMCwxLDExMCwxMzJaTTIzMy40MSw5NC4xLDIwOCwxMTkuNTEsMTM2LjQ4LDQ4LDE2MS45LDIyLjU4YTIsMiwwLDAsMSwyLjgzLDBsNjguNjgsNjguNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-question{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-city{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMjEwSDIzMFY4OGE2LDYsMCwwLDAtNi02SDE2MGE2LDYsMCwwLDAtNiw2djQySDEwMlY0MGE2LDYsMCwwLDAtNi02SDMyYTYsNiwwLDAsMC02LDZWMjEwSDE2YTYsNiwwLDAsMCwwLDEySDI0MGE2LDYsMCwwLDAsMC0xMlpNMTY2LDk0aDUyVjIxMEgxNjZabS0xMiw0OHY2OEgxMDJWMTQyWk0zOCw0Nkg5MFYyMTBIMzhaTTcwLDcyVjg4YTYsNiwwLDAsMS0xMiwwVjcyYTYsNiwwLDAsMSwxMiwwWm0wLDQ4djE2YTYsNiwwLDAsMS0xMiwwVjEyMGE2LDYsMCwwLDEsMTIsMFptMCw0OHYxNmE2LDYsMCwwLDEtMTIsMFYxNjhhNiw2LDAsMCwxLDEyLDBabTUyLDE2VjE2OGE2LDYsMCwwLDEsMTIsMHYxNmE2LDYsMCwwLDEtMTIsMFptNjQsMFYxNjhhNiw2LDAsMCwxLDEyLDB2MTZhNiw2LDAsMCwxLTEyLDBabTAtNDhWMTIwYTYsNiwwLDAsMSwxMiwwdjE2YTYsNiwwLDAsMS0xMiwwWiIvPjwvc3ZnPg==');}.icon-folder{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-hash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsOTBIMTczbDguODktNDguOTNhNiw2LDAsMSwwLTExLjgtMi4xNEwxNjAuODEsOTBIMTA5bDguODktNDguOTNhNiw2LDAsMCwwLTExLjgtMi4xNEw5Ni44MSw5MEg0OGE2LDYsMCwwLDAsMCwxMkg5NC42M2wtOS40Niw1MkgzMmE2LDYsMCwwLDAsMCwxMkg4M0w3NC4xLDIxNC45M2E2LDYsMCwwLDAsNC44Myw3QTUuNjQsNS42NCwwLDAsMCw4MCwyMjJhNiw2LDAsMCwwLDUuODktNC45M0w5NS4xOSwxNjZIMTQ3bC04Ljg5LDQ4LjkzYTYsNiwwLDAsMCw0LjgzLDcsNS42NCw1LjY0LDAsMCwwLDEuMDguMSw2LDYsMCwwLDAsNS44OS00LjkzTDE1OS4xOSwxNjZIMjA4YTYsNiwwLDAsMCwwLTEySDE2MS4zN2w5LjQ2LTUySDIyNGE2LDYsMCwwLDAsMC0xMlptLTc0LjgzLDY0SDk3LjM3bDkuNDYtNTJoNTEuOFoiLz48L3N2Zz4=');}.icon-shapes{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik02OS42OSw2Mi4xYTYsNiwwLDAsMC0xMS4zOCwwbC00MCwxMjBBNiw2LDAsMCwwLDI0LDE5MGg4MGE2LDYsMCwwLDAsNS42OS03LjlaTTMyLjMyLDE3OCw2NCw4M2wzMS42OCw5NVpNMjA2LDc2YTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDIwNiw3NlptLTg4LDBhMzgsMzgsMCwxLDEsMzgsMzhBMzgsMzgsMCwwLDEsMTE4LDc2Wm0xMDYsNzBIMTM2YTYsNiwwLDAsMC02LDZ2NTZhNiw2LDAsMCwwLDYsNmg4OGE2LDYsMCwwLDAsNi02VjE1MkE2LDYsMCwwLDAsMjI0LDE0NlptLTYsNTZIMTQyVjE1OGg3NloiLz48L3N2Zz4=');}.icon-diamonds-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjMuNzYsMTA4LjI0YTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBhNiw2LDAsMCwwLDAtOC40OGwtNDAtNDBhNiw2LDAsMCwwLTguNDgsMGwtNDAsNDBhNiw2LDAsMCwwLDAsOC40OFpNMTI4LDMyLjQ5LDE1OS41MSw2NCwxMjgsOTUuNTEsOTYuNDksNjRabTQuMjQsMTE1LjI3YTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MGE2LDYsMCwwLDAsMC04LjQ4Wk0xMjgsMjIzLjUxLDk2LjQ5LDE5MiwxMjgsMTYwLjQ5LDE1OS41MSwxOTJabTEwOC4yNC05OS43NS00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsMCw4LjQ4bDQwLDQwYTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBBNiw2LDAsMCwwLDIzNi4yNCwxMjMuNzZaTTE5MiwxNTkuNTEsMTYwLjQ5LDEyOCwxOTIsOTYuNDksMjIzLjUxLDEyOFptLTgzLjc2LTM1Ljc1LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MEE2LDYsMCwwLDAsMTA4LjI0LDEyMy43NlpNNjQsMTU5LjUxLDMyLjQ5LDEyOCw2NCw5Ni40OSw5NS41MSwxMjhaIi8+PC9zdmc+');}.icon-crosshair-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm02LDE5MS44VjE4NGE2LDYsMCwwLDAtMTIsMHYzMy44QTkwLjE1LDkwLjE1LDAsMCwxLDM4LjIsMTM0SDcyYTYsNiwwLDAsMCwwLTEySDM4LjJBOTAuMTUsOTAuMTUsMCwwLDEsMTIyLDM4LjJWNzJhNiw2LDAsMCwwLDEyLDBWMzguMkE5MC4xNSw5MC4xNSwwLDAsMSwyMTcuOCwxMjJIMTg0YTYsNiwwLDAsMCwwLDEyaDMzLjhBOTAuMTUsOTAuMTUsMCwwLDEsMTM0LDIxNy44WiIvPjwvc3ZnPg==');}.icon-circle-notch{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTI4YTEwMiwxMDIsMCwwLDEtMjA0LDBjMC00MC4xOCwyMy4zNS03Ni44Niw1OS41LTkzLjQ1YTYsNiwwLDAsMSw1LDEwLjlDNTguNjEsNjAuMDksMzgsOTIuNDksMzgsMTI4YTkwLDkwLDAsMCwwLDE4MCwwYzAtMzUuNTEtMjAuNjEtNjcuOTEtNTIuNS04Mi41NWE2LDYsMCwwLDEsNS0xMC45QzIwNi42NSw1MS4xNCwyMzAsODcuODIsMjMwLDEyOFoiLz48L3N2Zz4=');}.icon-cards-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsOTBINDhhMTQsMTQsMCwwLDAtMTQsMTR2OTZhMTQsMTQsMCwwLDAsMTQsMTRIMjA4YTE0LDE0LDAsMCwwLDE0LTE0VjEwNEExNCwxNCwwLDAsMCwyMDgsOTBabTIsMTEwYTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlYxMDRhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMlpNNTAsNjRhNiw2LDAsMCwxLDYtNkgyMDBhNiw2LDAsMCwxLDAsMTJINTZBNiw2LDAsMCwxLDUwLDY0Wk02NiwzMmE2LDYsMCwwLDEsNi02SDE4NGE2LDYsMCwwLDEsMCwxMkg3MkE2LDYsMCwwLDEsNjYsMzJaIi8+PC9zdmc+');}.icon-sun-dim{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMzJhNiw2LDAsMCwxLDEyLDB2OGE2LDYsMCwwLDEtMTIsMFptNjgsODhhNjIsNjIsMCwxLDEtNjItNjJBNjIuMDcsNjIuMDcsMCwwLDEsMTkwLDEyOFptLTEyLDBhNTAsNTAsMCwxLDAtNTAsNTBBNTAuMDYsNTAuMDYsMCwwLDAsMTc4LDEyOFpNNTkuNzYsNjguMjRhNiw2LDAsMSwwLDguNDgtOC40OGwtOC04YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwxLDAtOC40OC04LjQ4Wm0xMzYtMTM2LTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwwLDAtOC40OC04LjQ4Wm0uNDgsMTM2YTYsNiwwLDAsMC04LjQ4LDguNDhsOCw4YTYsNiwwLDAsMCw4LjQ4LTguNDhaTTQwLDEyMkgzMmE2LDYsMCwwLDAsMCwxMmg4YTYsNiwwLDAsMCwwLTEyWm04OCw4OGE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LThBNiw2LDAsMCwwLDEyOCwyMTBabTk2LTg4aC04YTYsNiwwLDAsMCwwLDEyaDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-moon{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==');}.icon-plus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkgxMzR2MzRhNiw2LDAsMCwxLTEyLDBWMTM0SDg4YTYsNiwwLDAsMSwwLTEyaDM0Vjg4YTYsNiwwLDAsMSwxMiwwdjM0aDM0QTYsNiwwLDAsMSwxNzQsMTI4WiIvPjwvc3ZnPg==');}.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-floppy-disk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSw3My40MiwxODIuNTgsMzguMWExMy45LDEzLjksMCwwLDAtOS44OS00LjFINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY4My4zMUExMy45LDEzLjksMCwwLDAsMjE3LjksNzMuNDJaTTE3MCwyMTBIODZWMTUyYTIsMiwwLDAsMSwyLTJoODBhMiwyLDAsMCwxLDIsMlptNDAtMmEyLDIsMCwwLDEtMiwySDE4MlYxNTJhMTQsMTQsMCwwLDAtMTQtMTRIODhhMTQsMTQsMCwwLDAtMTQsMTR2NThINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDE3Mi42OWEyLDIsMCwwLDEsMS40MS41OEwyMDkuNDIsODEuOWEyLDIsMCwwLDEsLjU4LDEuNDFaTTE1OCw3MmE2LDYsMCwwLDEtNiw2SDk2YTYsNiwwLDAsMSwwLTEyaDU2QTYsNiwwLDAsMSwxNTgsNzJaIi8+PC9zdmc+');}.icon-x-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-minus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkg4OGE2LDYsMCwwLDEsMC0xMmg4MEE2LDYsMCwwLDEsMTc0LDEyOFoiLz48L3N2Zz4=');}.icon-pencil-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjUuOSw3NC43OCwxODEuMjEsMzAuMDlhMTQsMTQsMCwwLDAtMTkuOCwwTDM4LjEsMTUzLjQxYTEzLjk0LDEzLjk0LDAsMCwwLTQuMSw5LjlWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDkyLjY5YTEzLjk0LDEzLjk0LDAsMCwwLDkuOS00LjFMMjI1LjksOTQuNThhMTQsMTQsMCwwLDAsMC0xOS44Wk05NC4xLDIwOS40MWEyLDIsMCwwLDEtMS40MS41OUg0OGEyLDIsMCwwLDEtMi0yVjE2My4zMWEyLDIsMCwwLDEsLjU5LTEuNDFMMTM2LDcyLjQ4LDE4My41MSwxMjBaTTIxNy40MSw4Ni4xLDE5MiwxMTEuNTEsMTQ0LjQ5LDY0LDE2OS45LDM4LjU4YTIsMiwwLDAsMSwyLjgzLDBsNDQuNjgsNDQuNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-dots-six-vertical{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDIsNjBBMTAsMTAsMCwxLDEsOTIsNTAsMTAsMTAsMCwwLDEsMTAyLDYwWm02MiwxMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxNjQsNzBaTTkyLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw5MiwxMThabTcyLDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTY0LDExOFpNOTIsMTg2YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDkyLDE4NlptNzIsMGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCwxNjQsMTg2WiIvPjwvc3ZnPg==');}.icon-list{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-loading{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNSIgICAgaWQ9InN2ZzEwIiAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzICAgIGlkPSJkZWZzMTAiIC8+PHBhdGggICAgaWQ9InBhdGgxMSIgICAgc3R5bGU9ImJhc2VsaW5lLXNoaWZ0OmJhc2VsaW5lO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmVjdG9yLWVmZmVjdDpub25lO2ZpbGw6IzIyMjIyMjtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgZD0ibSAxNi42MjEwOTQsMS4xNDI1NzgxIGMgLTguMjY2MzIzMiwwIC0xNi4yMjA4NjczOCw2LjQ0MjgwOTUgLTE1LjU4NTkzNzgsMTQuNjg1NTQ2OSAwLjYwMTM0NTUsNy44MDczMDggNy40MzQxMjY0LDE0LjEyNjk4IDE0LjkzMzU5MzgsMTQuOTQzMzU5IDguODM5ODQ1LDAuOTYyMjgzIDE1LjUwNTQ2OSwtNi4zNzY5MTkgMTUuMDA1ODU5LC0xNC45ODYzMjggQyAzMC40OTU5LDcuNTM2MjY4NCAyNC44ODMzOTcsMS4xNDI1NzgxIDE2LjYyMTA5NCwxLjE0MjU3ODEgWiBtIDAsMC42NTAzOTA3IEMgMjYuNDg4Nzg2LDEuODAzODY0NSAyOS43MTQ1MTgsOS41OTM1ODMzIDMwLjMwMjczNCwxNS44MDQ2ODggMzEuMTQxOTgyLDI0LjY2NjM2NSAyMi4xNjA0NTksMzEuMTY4MDc3IDE2LjAzOTA2MiwzMC4xMjUgOC44OTUxMzI3LDI4LjkwNzY4MSAyLjI2MTMxNDIsMjMuMjc5Mzc2IDEuNjgzNTkzOCwxNS43NzkyOTcgMS4wNzY5MzM4LDcuOTAzMjc1NCA4LjcyMjU0NTEsMS43ODQyNjk5IDE2LjYyMTA5NCwxLjc5Mjk2ODggWiBtIC0wLjA2NDQ1LDEuMjE4NzUgYyAtMy42MTAwODMsMCAtNy4xNTQ3OTk1LDEuNDAxMDY4NyAtOS43MzA0NjkxLDMuNzAzMTI1IEMgNC4yNTA1MDIzLDkuMDE2OTAwMiAyLjY0MjAzNzIsMTIuMjI2Mjk1IDIuOTE5OTIxOSwxNS44MzM5ODQgMy40NDY5MzUsMjIuNjc1NzEyIDkuNDI4OTY0OSwyOC4xOTg5ODUgMTUuOTk4MDQ3LDI4LjkxNDA2MiAyMy43MTQyNTYsMjkuNzU0MDIzIDI5LjUzMTYwMywyMy4zMzE3IDI5LjA5NTcwMywxNS44MjAzMTIgMjguNjc3OTQ4LDguNjIxMzk1MyAyMy43NzY2ODYsMy4wMTE3MTg4IDE2LjU1NjY0MSwzLjAxMTcxODggWiBtIDAsMC4xOTUzMTI0IGMgNy4xMTkxMzQsMCAxMS45MzI3MSw1LjUwODEzNzMgMTIuMzQ1NzAzLDEyLjYyNDk5OTggQyAyOS4zMzIwNjIsMjMuMjM2ODk2IDIzLjYxODk1OCwyOS41NDU5OTggMTYuMDE5NTMxLDI4LjcxODc1IDkuNTQ1NDMyMSwyOC4wMTQwMTIgMy42MzQxNjM3LDIyLjU1NTE0MyAzLjExNTIzNDQsMTUuODE4MzU5IDIuODQyNDU2MywxMi4yNzY5NjcgNC40MTg0MTA5LDkuMTI4MzE2OSA2Ljk1NzAzMTIsNi44NTkzNzUgOS40OTU2NTE2LDQuNTkwNDMzMSAxMi45OTcwOTMsMy4yMDcwMzEyIDE2LjU1NjY0MSwzLjIwNzAzMTIgWiBtIC0wLjA3MDMxLDEuNDE2MDE1NyBjIC0zLjE2MTk3MywwIC02LjI2MzUwOSwxLjIyNTgxMzkgLTguNTE5NTMxMSwzLjI0MjE4NzUgQyA1LjcxMDc2OTEsOS44ODE2MDggNC4zMDE0NTQyLDEyLjY5NDU4OSA0LjU0NDkyMTksMTUuODU1NDY5IDUuMDA2NTYyNCwyMS44NDg1NTQgMTAuMjQ0MTc4LDI2LjY4NjE1OSAxNS45OTgwNDcsMjcuMzEyNSAyMi43NTcwMTMsMjguMDQ4MjYxIDI3Ljg1NDQ1MSwyMi40MjA5MzYgMjcuNDcyNjU2LDE1Ljg0MTc5NyAyNy4xMDY4MjQsOS41Mzc2MDI1IDIyLjgxMDE2LDQuNjIzMDQ2OSAxNi40ODYzMjgsNC42MjMwNDY5IFogbSAwLDAuMTk1MzEyNSBjIDYuMjIyOTIsMCAxMC40Mjk5NDYsNC44MTMwMTM4IDEwLjc5MTAxNiwxMS4wMzUxNTY2IDAuMzc1NjEzLDYuNDcyNjE1IC00LjYxNzU4NCwxMS45ODY3MiAtMTEuMjU5NzY2LDExLjI2MzY3MiBDIDEwLjM1ODY4NSwyNi41MDExODYgNS4xOTE4MzgxLDIxLjcyNzk4NSA0LjczODI4MTIsMTUuODM5ODQ0IDQuNDk5OTIwMSwxMi43NDUyNjIgNS44NzY3MzE1LDkuOTk0OTc3OCA4LjA5NTcwMzEsOC4wMTE3MTg4IDEwLjMxNDY3NSw2LjAyODQ1OTUgMTMuMzc0ODksNC44MTgzNTk0IDE2LjQ4NjMyOCw0LjgxODM1OTQgWiBtIC0wLjA2ODM2LDEuNDE2MDE1NiBjIC0yLjcxMzg3NywwIC01LjM3NjExOCwxLjA1MjUxNjQgLTcuMzEyNTAwMiwyLjc4MzIwMzEgLTEuOTM2MzgyOCwxLjczMDY4NjkgLTMuMTQ2NTUxNyw0LjE0NTMxMTkgLTIuOTM3NSw2Ljg1OTM3NDkgMC4zOTYyNjk5LDUuMTQ0NDMgNC44ODk0NDQyLDkuMjk0NDI5IDkuODI4MTI1Miw5LjgzMjAzMSA1LjgwMTc0OSwwLjYzMTU2MiAxMC4xNzkyNTcsLTQuMTk4ODI4IDkuODUxNTYyLC05Ljg0NTcwMyBDIDI1LjUzMzc1LDEwLjQ1MzgyMiAyMS44NDU2MTYsNi4yMzQzNzUgMTYuNDE3OTc0LDYuMjM0Mzc1IFogbSAwLDAuMTk1MzEyNSBjIDUuMzI2NzMsMCA4LjkyNTIyNiw0LjExNzkwNTUgOS4yMzQzNzUsOS40NDUzMTI1IDAuMzIxNTEzLDUuNTQwMzUxIC0zLjk0OTgwMSwxMC4yNTk0NzQgLTkuNjM0NzY2LDkuNjQwNjI1IEMgMTEuMTczODc1LDI0Ljk4ODM2MiA2Ljc0OTUxNDMsMjAuOTAwODE0IDYuMzYxMzI4MSwxNS44NjEzMjggNi4xNTczODMxLDEzLjIxMzU2MyA3LjMzNTA0MzEsMTAuODU5NjgyIDkuMjM0Mzc1LDkuMTYyMTA5NCAxMS4xMzM3MDcsNy40NjQ1MzcyIDEzLjc1NDYyOCw2LjQyOTY4NzUgMTYuNDE3OTY5LDYuNDI5Njg3NSBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTIuMjY1Nzc1LDAgLTQuNDg4NzI5LDAuODc5MjE5NiAtNi4xMDU0NjgsMi4zMjQyMTg5IC0xLjYxNjc0MDgsMS40NDQ5OTkgLTIuNjI3NzYwNywzLjQ2MTI2OSAtMi40NTMxMjU0LDUuNzI4NTE2IDAuMzMwODk4Niw0LjI5NTc2OCA0LjA4MTU5NjQsNy43NjAxMiA4LjIwNTA3ODQsOC4yMDg5ODQgNC44NDQ1MjUsMC41MjczNiA4LjUwMDE1NiwtMy41MDYwOTcgOC4yMjY1NjIsLTguMjIwNzAzIEMgMjMuOTYwNjcyLDExLjM3MTk5NiAyMC44ODEwNiw3Ljg0NTcwMzEgMTYuMzQ5NjE0LDcuODQ1NzAzMSBaIG0gMCwwLjE5NTMxMjUgYyA0LjQzMDUzNCwwIDcuNDIyNDYxLDMuNDIyNzk5NCA3LjY3OTY4OCw3Ljg1NTQ2ODQgMC4yNjc0MTIsNC42MDgwODIgLTMuMjgzOTc4LDguNTMyMjI2IC04LjAxMTcxOSw4LjAxNzU3OCBDIDExLjk4OTA3NSwyMy40NzU1MzggOC4zMDcxODk5LDIwLjA3NTU5MyA3Ljk4NDM3NSwxNS44ODQ3NjYgNy44MTQ4NDYzLDEzLjY4MzgxOSA4Ljc5NTMxMDUsMTEuNzI2MzM4IDEwLjM3NSwxMC4zMTQ0NTMgMTEuOTU0Njg5LDguOTAyNTY4OSAxNC4xMzQzNyw4LjA0MTAxNTYgMTYuMzQ5NjA5LDguMDQxMDE1NiBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTEuODE3NjcyLDAgLTMuNjAxMzQyLDAuNzAzOTY4OCAtNC44OTg0MzgsMS44NjMyODA4IC0xLjI5NzA5NSwxLjE1OTMxIC0yLjEwODk2ODMsMi43NzkxODUgLTEuOTY4NzQ5NSw0LjU5OTYxIDAuMjY1NTI2OSwzLjQ0NzExMSAzLjI3Mzc1MDUsNi4yMjU4MTMgNi41ODIwMzE1LDYuNTg1OTM3IDMuODg3Mjk1LDAuNDIzMTYgNi44MjMwMDgsLTIuODE1MzE4IDYuNjAzNTE1LC02LjU5NzY1NiBDIDIyLjM4OTU0MSwxMi4yODgyMjIgMTkuOTE2NDk1LDkuNDU3MDMxMSAxNi4yODEyNSw5LjQ1NzAzMTIgWiBtIDAsMC4xOTUzMTI2IGMgMy41MzQzMzMsMCA1LjkxNzc0MiwyLjcyNzY5NjIgNi4xMjMwNDcsNi4yNjU2MjUyIDAuMjEzMzExLDMuNjc1ODE0IC0yLjYxNjIwOCw2LjgwMzAyNSAtNi4zODY3MTksNi4zOTI1NzggLTMuMjEzMjk4LC0wLjM0OTc4NSAtNi4xNTA3NTk3LC0zLjA2MjEzIC02LjQwODIwMywtNi40MDQyOTcgLTAuMTM1MTEyMiwtMS43NTQxMjcgMC42NDQyNTIsLTMuMzEzMjU3IDEuOTA0Mjk3LC00LjQzOTQ1MyAxLjI2MDA0NSwtMS4xMjYxOTYgMy4wMDA0NDEsLTEuODE0NDUzMyA0Ljc2NzU3OCwtMS44MTQ0NTMyIHogbSAtMC4wNzAzMSwxLjQxNjAxNTIgYyAtMS4zNjk1NzIsMCAtMi43MTIsMC41MzA2NzUgLTMuNjg5NDU0LDEuNDA0Mjk3IC0wLjk3NzQ1MywwLjg3MzYyMiAtMS41OTAxNzcsMi4wOTUxNDUgLTEuNDg0Mzc1LDMuNDY4NzUgMC4yMDAxNTYsMi41OTg0NTIgMi40NjU5LDQuNjg5NTUxIDQuOTU4OTg1LDQuOTYwOTM4IDIuOTMwMDcsMC4zMTg5NTggNS4xNDM5MDgsLTIuMTIyNTg3IDQuOTc4NTE1LC00Ljk3MjY1NiAtMC4xNTgxNDUsLTIuNzI1MjQ0IC0yLjAyNDYyMiwtNC44NjEzMjkgLTQuNzYzNjcxLC00Ljg2MTMyOSB6IG0gMCwwLjE5NTMxMyBjIDIuNjM4MTM1LDAgNC40MTQ5NzUsMi4wMzQ1NDQgNC41NjgzNTksNC42Nzc3MzQgMC4xNTkyMTEsMi43NDM1NDYgLTEuOTUwMzg2LDUuMDczODI0IC00Ljc2MzY3Miw0Ljc2NzU3OCAtMi4zOTgxMDIsLTAuMjYxMDQ3IC00LjU5MTEzMSwtMi4yODc3NDEgLTQuNzgzMjAzLC00Ljc4MTI1IC0wLjEwMDY5NiwtMS4zMDczMDggMC40Nzk1MTksLTIuNDcwMDM5IDEuNDE5OTIyLC0zLjMxMDU0NiAwLjk0MDQwMywtMC44NDA1MDggMi4yMzk1NTcsLTEuMzUzNTE2IDMuNTU4NTk0LC0xLjM1MzUxNiB6IG0gLTAuMDY4MzYsMS40MTYwMTYgYyAtMC45MjE0NzIsMCAtMS44MjI2NTcsMC4zNTU0MjUgLTIuNDgwNDY5LDAuOTQzMzU5IC0wLjY1NzgxMSwwLjU4NzkzNCAtMS4wNzMzMzksMS40MTUwMSAtMS4wMDE5NTMsMi4zNDE3OTcgMC4xMzQ3ODUsMS43NDk3OTIgMS42NTYwOTUsMy4xNTMyOTEgMy4zMzM5ODUsMy4zMzU5MzcgMS45NzI4NDYsMC4yMTQ3NTkgMy40NjY3NiwtMS40MzE4MDkgMy4zNTU0NjgsLTMuMzQ5NjA5IC0wLjEwNjIyNCwtMS44MzA1MDMgLTEuMzY0MTc3LC0zLjI3MTQ4NyAtMy4yMDcwMzEsLTMuMjcxNDg0IHogbSAwLDAuMTk1MzEyIGMgMS43NDE5NDIsMCAyLjkxMjIwOSwxLjMzOTQ0IDMuMDEzNjcyLDMuMDg3ODkxIDAuMTA1MTEsMS44MTEyNzYgLTEuMjg0NTYyLDMuMzQ2NTc3IC0zLjE0MDYyNSwzLjE0NDUzMSAtMS41ODI5MDcsLTAuMTcyMzA3IC0zLjAzMzQ1NSwtMS41MTMzNTUgLTMuMTYwMTU2LC0zLjE1ODIwMyAtMC4wNjYyOCwtMC44NjA0OSAwLjMxNDc4NSwtMS42MjQ4NjggMC45MzU1NDcsLTIuMTc5Njg4IDAuNjIwNzQ5LC0wLjU1NDgxOSAxLjQ4MDYyLC0wLjg5NDUzMSAyLjM1MTU1NiwtMC44OTQ1MzEgeiBtIC0wLjA2ODM2LDEuNDE2MDE2IGMgLTAuNDczMzY5LDAgLTAuOTM1MjcxLDAuMTgyMTI5IC0xLjI3MzQzOCwwLjQ4NDM3NSAtMC4zMzgxNjcsMC4zMDIyNDYgLTAuNTU0NTQ2LDAuNzMwOTY5IC0wLjUxNzU3OCwxLjIxMDkzNyAwLjA2OTQxLDAuOTAxMTMzIDAuODQ4MjQ5LDEuNjE4OTgxIDEuNzEwOTM4LDEuNzEyODkxIDEuMDE1NjE2LDAuMTEwNTU3IDEuNzg5NjE0LC0wLjc0MTAzMSAxLjczMjQyMSwtMS43MjY1NjMgLTAuMDU0MywtMC45MzU3NjYgLTAuNzA1NjkxLC0xLjY4MTY0IC0xLjY1MjM0MywtMS42ODE2NCB6IG0gMCwwLjE5NTMxMiBjIDAuODQ1NzQsMCAxLjQwNzQ5LDAuNjQ0MzMzIDEuNDU3MDMxLDEuNDk4MDQ3IDAuMDUxMDEsMC44NzkwMDggLTAuNjE2NzkzLDEuNjE5MzI5IC0xLjUxNTYyNSwxLjUyMTQ4NCAtMC43Njc3MDYsLTAuMDgzNTcgLTEuNDc1NzgsLTAuNzM4OTY3IC0xLjUzNzEwOSwtMS41MzUxNTYgLTAuMDMxODYsLTAuNDEzNjcxIDAuMTUwMDU1LC0wLjc3OTY5NyAwLjQ1MTE3MiwtMS4wNDg4MjggMC4zMDExMTYsLTAuMjY5MTMxIDAuNzIxNjk4LC0wLjQzNTU0NyAxLjE0NDUzMSwtMC40MzU1NDcgeiIgLz48L3N2Zz4=');}.icon-magnifying-glass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMjE5Ljc2bC01MS4zOC01MS4zOGE4Ni4xNSw4Ni4xNSwwLDEsMC04LjQ4LDguNDhsNTEuMzgsNTEuMzhhNiw2LDAsMCwwLDguNDgtOC40OFpNMzgsMTEyYTc0LDc0LDAsMSwxLDc0LDc0QTc0LjA5LDc0LjA5LDAsMCwxLDM4LDExMloiLz48L3N2Zz4=');}.icon-infinity{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTU0LDU0LDAsMCwxLTkyLjE4LDM4LjE4LDMuMDcsMy4wNywwLDAsMS0uMjUtLjI2bC02MC02Ny43NGE0Miw0MiwwLDEsMCwwLDU5LjY0bDguNTctOS42N2E2LDYsMCwxLDEsOSw4bC04LjY5LDkuODFhMy4wNywzLjA3LDAsMCwxLS4yNS4yNiw1NCw1NCwwLDEsMSwwLTc2LjM2LDMuMDcsMy4wNywwLDAsMSwuMjUuMjZsNjAsNjcuNzRhNDIsNDIsMCwxLDAsMC01OS42NGwtOC41Nyw5LjY3YTYsNiwwLDEsMS05LThsOC42OS05LjgxYTMuMDcsMy4wNywwLDAsMSwuMjUtLjI2QTU0LDU0LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-clock{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=');}.icon-house{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNmg2NGE2LDYsMCwwLDAsNi02VjE1OGgzNnY1OGE2LDYsMCwwLDAsNiw2aDY0YTYsNiwwLDAsMCw2LTZWMTIwQTEzLjkyLDEzLjkyLDAsMCwwLDIxNy45LDExMC4xWk0yMTAsMjEwSDE1OFYxNTJhNiw2LDAsMCwwLTYtNkgxMDRhNiw2LDAsMCwwLTYsNnY1OEg0NlYxMjBhMiwyLDAsMCwxLC41OC0xLjQybDgwLTgwYTIsMiwwLDAsMSwyLjg0LDBsODAsODBBMiwyLDAsMCwxLDIxMCwxMjBaIi8+PC9zdmc+');}.icon-logo{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlO3N0cm9rZS1saW5lam9pbjpiZXZlbDtzdHJva2UtbWl0ZXJsaW1pdDoxLjUiICAgIGlkPSJzdmcxMiIgICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcyAgICBpZD0iZGVmczEyIiAvPjxwYXRoICAgIGQ9Ik0gMTYsMi4wOTE3OTY5IEMgOC4yODYyNTM4LDIuMDkxNzk2OSAyLjAyNTM5MDcsOC4zMjE0NzYzIDIuMDI1MzkwNiwxNiAyLjAyNTM5MDYsMjMuNjc4NTI0IDguMjg2MjUzOCwyOS45MDgyMDMgMTYsMjkuOTA4MjAzIDIzLjcxMzc0NiwyOS45MDgyMDMgMjkuOTc0NjA5LDIzLjY3ODUyNCAyOS45NzQ2MDksMTYgMjkuOTc0NjA5LDguMzIxNDc2MyAyMy43MTM3NDYsMi4wOTE3OTY5IDE2LDIuMDkxNzk2OSBaIG0gMCwwLjYwNTQ2ODcgYyAwLjIwMzk4LDAgMC40MDU2ODMsMC4wMDY2NCAwLjYwNzQyMiwwLjAxNTYyNSAwLjE5NjUxNiwwLjAwODc1IDAuMzkxNzI4LDAuMDIxOTcyIDAuNTg1OTM3LDAuMDM5MDYzIC0wLjE5NDE2NiwtMC4wMTcwODEgLTAuMzg5NDY3LC0wLjAzMDMxNiAtMC41ODU5MzcsLTAuMDM5MDYzIFYgMjkuMjg3MTA5IGMgMC4xOTY0NywtMC4wMDg3IDAuMzkxNzcxLC0wLjAyMTk4IDAuNTg1OTM3LC0wLjAzOTA2IC0wLjE5NDIwOSwwLjAxNzA5IC0wLjM4OTQyMSwwLjAzMDMxIC0wLjU4NTkzNywwLjAzOTA2IEMgMTYuNDA1NjgzLDI5LjI5NjA5IDE2LjIwMzk4LDI5LjMwMjczNCAxNiwyOS4zMDI3MzQgOC42MTIxNjcsMjkuMzAyNzM0IDIuNjMwODU5NCwyMy4zNDk0MDggMi42MzA4NTk0LDE2IDIuNjMwODU5NCw4LjY1MDU5MTggOC42MTIxNjcxLDIuNjk3MjY1NiAxNiwyLjY5NzI2NTYgWiBtIDEuMjgxMjUsMC4wNjI1IGMgMC4yMTQ5ODYsMC4wMjAzMjkgMC40Mjg3NCwwLjA0NTg0NSAwLjY0MDYyNSwwLjA3NjE3MiAwLjIxMTQxMiwwLjAzMDI1OSAwLjQyMDgyMywwLjA2NTQ5NyAwLjYyODkwNiwwLjEwNTQ2ODggQyAxOC4zNDI4MiwyLjkwMTQ3NDYgMTguMTMzMTU5LDIuODY2MTcxOCAxNy45MjE4NzUsMi44MzU5Mzc1IDE3LjcwOTkxNSwyLjgwNTYwNjYgMTcuNDk2MzEsMi43ODAwOTU1IDE3LjI4MTI1LDIuNzU5NzY1NiBaIG0gMS4yODcxMDksMC4xODM1OTM4IGMgMC4yMTExMDcsMC4wNDA4NDEgMC40MTk1MzMsMC4wODgwODEgMC42MjY5NTMsMC4xMzg2NzE5IDAuMTc2MDc3LDAuMDQyOTQ2IDAuMzUwMTg2LDAuMDkwODIgMC41MjM0MzgsMC4xNDA2MjUgLTAuMTczMDk1LC0wLjA0OTczMSAtMC4zNDc1MjUsLTAuMDk3NzM4IC0wLjUyMzQzOCwtMC4xNDA2MjUgLTAuMjA3NDI4LC0wLjA1MDU3MSAtMC40MTU4NCwtMC4wOTc4NDcgLTAuNjI2OTUzLC0wLjEzODY3MTkgeiBtIDEuMjg3MTEsMC4zMTgzNTk0IGMgMC4xNDQ0MTEsMC4wNDMxODUgMC4yODczODQsMC4wODg4NTMgMC40Mjk2ODcsMC4xMzY3MTg3IC0wLjE0MjM0OCwtMC4wNDc4NTcgLTAuMjg1MjMxLC0wLjA5MzU0MyAtMC40Mjk2ODcsLTAuMTM2NzE4NyB6IG0gMC42MTcxODcsMC4yMDExNzE4IGMgMC4xNjc2NzIsMC4wNTkxOTEgMC4zMzM1MzcsMC4xMjE5MTcxIDAuNDk4MDQ3LDAuMTg3NSAtMC4xNjQ1NjEsLTAuMDY1NTc0IC0wLjMzMDMyNCwtMC4xMjgzMTg3IC0wLjQ5ODA0NywtMC4xODc1IHogbSAwLjU2MjUsMC4yMTI4OTA3IGMgMC4xNzYxNzgsMC4wNzEyOTcgMC4zNTExMDcsMC4xNDYxNDY0IDAuNTIzNDM4LDAuMjI0NjA5MyAtMC4xNzIyMTgsLTAuMDc4MzcxIC0wLjM0NzM4LC0wLjE1MzM5MTYgLTAuNTIzNDM4LC0wLjIyNDYwOTMgeiBtIC01LjUyNzM0MywwLjExOTE0MDYgLTAuMTIxMDk0LDAuMDA1ODYgQyA4Ljg5ODA2NTIsNC4xMTkzODMgMy43MjY1NjI1LDkuNDY0MjE4NSAzLjcyNjU2MjUsMTYgYyAwLDYuNTM1ODE3IDUuMTcxNTAyNSwxMS44ODA2NTIgMTEuNjYwMTU2NSwxMi4xOTkyMTkgbCAwLjEyMTA5NCwwLjAwNTkgMC4wMTE3MiwtMC4yNDIxODcgLTAuMTIxMDkzLC0wLjAwNTkgQyA5LjAzNjA5MjgsMjcuNjQ0NjY0IDMuOTY4NzUsMjIuNDA3OTY1IDMuOTY4NzUsMTYgMy45Njg3NSw5LjU5MjA3MjEgOS4wMzYwOTY0LDQuMzU1MzY5OCAxNS4zOTg0MzgsNC4wNDI5Njg3IGwgMC4xMjEwOTMsLTAuMDA1ODYgeiBtIDYuMTA5Mzc0LDAuMTMwODU5NCBjIDAuMTY0NjM1LDAuMDc1OTExIDAuMzI3MzY0LDAuMTU1OTgyOCAwLjQ4ODI4MiwwLjIzODI4MTIgLTAuMTYxNTE3LC0wLjA4MjU5NCAtMC4zMjMwMjEsLTAuMTYyMTIwMyAtMC40ODgyODIsLTAuMjM4MjgxMiB6IG0gMC41ODM5ODUsMC4yODcxMDkzIGMgMC4xNTIwMjYsMC4wNzkzIDAuMzAwNzE1LDAuMTYzMTY1MyAwLjQ0OTIxOSwwLjI0ODA0NjkgLTAuMTQ4MjE1LC0wLjA4NDY5MiAtMC4yOTc0OTcsLTAuMTY4OTEzIC0wLjQ0OTIxOSwtMC4yNDgwNDY5IHogbSAwLjU1NjY0LDAuMzA4NTkzOCBjIDAuMTM4NDkzLDAuMDgwODY2IDAuMjc0OTEzLDAuMTY0MzcwOCAwLjQxMDE1NywwLjI1IC0wLjEzNTA3OSwtMC4wODU0ODYgLTAuMjcxODM3LC0wLjE2OTI2MjkgLTAuNDEwMTU3LC0wLjI1IHogbSAwLjU0ODgyOSwwLjMzOTg0MzcgYyAwLjEyMzIwMSwwLjA4MDE1MSAwLjI0NDkwMywwLjE2MjA0OTcgMC4zNjUyMzQsMC4yNDYwOTM4IC0wLjEyMDM0OSwtMC4wODQwMiAtMC4yNDIwMTUsLTAuMTY1OTY2MyAtMC4zNjUyMzQsLTAuMjQ2MDkzOCB6IG0gMC41NDY4NzUsMC4zNzUgYyAwLjEwMzM0OCwwLjA3NDc3MSAwLjIwNTU2LDAuMTUwODk4NiAwLjMwNjY0LDAuMjI4NTE1NiAtMC4xMDEwNzEsLTAuMDc3NTc1IC0wLjIwMzMwMiwtMC4xNTM3ODUgLTAuMzA2NjQsLTAuMjI4NTE1NiB6IG0gLTguMzQ3NjU3LDAuMDcwMzEyIC0wLjEyMTA5MywwLjAwNzgxIEMgOS43MzUzODExLDUuNjMxODMwOCA1LjI0NDE0MDYsMTAuMjk4OTk4IDUuMjQ0MTQwNiwxNiBjIDAsNS43MDEwMzMgNC40OTExNDg1LDEwLjM2ODE3NCAxMC4xNDA2MjU0LDEwLjY4NTU0NyBsIDAuMTIxMDkzLDAuMDA3OCAwLjAxMzY3LC0wLjI0MjE4NyAtMC4xMjEwOTMsLTAuMDA3OCBDIDkuODc0NjI3MiwyNi4xMzMwNDQgNS40ODgyODE0LDIxLjU3Mzc1NyA1LjQ4ODI4MTIsMTYgYyAwLC01LjU3MzcyNCA0LjM4NjM0MiwtMTAuMTMzMDQ2MSA5LjkxMDE1NjgsLTEwLjQ0MzM1OTQgbCAwLjEyMTA5MywtMC4wMDc4MSB6IG0gOC44NDk2MSwwLjMxMDU0NjkgYyAwLjEwMDk1NiwwLjA4MDUyNyAwLjIwMDMwNCwwLjE2Mjc0MTEgMC4yOTg4MjgsMC4yNDYwOTM3IC0wLjA5ODcsLTAuMDgzNDc1IC0wLjE5NzY5MiwtMC4xNjU0NTM1IC0wLjI5ODgyOCwtMC4yNDYwOTM3IHogbSAwLjQ5MDIzNCwwLjQxMDE1NjIgYyAwLjA5NDk3LDAuMDgzNDYyIDAuMTg4NzEzLDAuMTY3ODI3MiAwLjI4MTI1LDAuMjUzOTA2MyAtMC4wOTIzLC0wLjA4NTgxMyAtMC4xODY1MzUsLTAuMTcwNjk0OSAtMC4yODEyNSwtMC4yNTM5MDYzIHogbSAwLjQ0MzM1OSwwLjQwODIwMzIgQyAyNS4zOTM4NzcsNi41MzYzNjA5IDI1LjQ5ODE0Nyw2LjYzODAzNCAyNS41OTk2MDksNi43NDIxODcgMjUuNDk3OTc5LDYuNjM3OTAyIDI1LjM5NDA1NCw2LjUzNjQ4MjggMjUuMjg5MDYyLDYuNDM1NTQ2NCBaIE0gMTUuNTA1ODU5LDYuODIwMzEyIDE1LjM4NDc2Niw2LjgyODEyMiBDIDEwLjU3NDc5Niw3LjE0Mzc4OTUgNi43NjM2NzE5LDExLjEzMzk2MyA2Ljc2MzY3MTksMTYgYyAwLDQuODY2MDM4IDMuODExMDMyMSw4Ljg1NjE4OCA4LjYyMTA5NDEsOS4xNzE4NzUgbCAwLjEyMTA5MywwLjAwNzggMC4wMTU2MywtMC4yNDIxODcgLTAuMTIxMDkzLC0wLjAwNzggQyAxMC43MTUxLDI0LjYyMjE4OCA3LjAwNTg1OTQsMjAuNzM5NTMgNy4wMDU4NTk0LDE2IGMgMCwtNC43Mzk1MjkgMy43MDkyNTE2LC04LjYyMjIxNjggOC4zOTQ1MzE2LC04LjkyOTY4NzUgTCAxNS41MjE0ODQsNy4wNjI1IFogbSAxMC4yMTg3NSwwLjA1MjczNCBjIDAuMDkzNzcsMC4wOTg4ODYgMC4xODY2MjgsMC4xOTkwNjk0IDAuMjc3MzQ0LDAuMzAwNzgxMiBDIDI1LjkxMTE0Myw3LjA3MjAzNDUgMjUuODE4NDc5LDYuOTcyMDA3OSAyNS43MjQ2MDksNi44NzMwNDYgWiBtIDAuNDM5NDUzLDAuNDg2MzI4MSBjIDAuMDk0MzksMC4xMDk4MzY1IDAuMTg2NTM0LDAuMjIxMDkyNCAwLjI3NzM0NCwwLjMzMzk4NDQgLTAuMDkwNjksLTAuMTEyNzA0NCAtMC4xODMwODEsLTAuMjI0MzI0OSAtMC4yNzczNDQsLTAuMzMzOTg0NCB6IG0gMC4zNTM1MTYsMC40Mjk2ODc1IGMgMC4xMDE5NDcsMC4xMjkxNDA4IDAuMjAxNjQ3LDAuMjU5NjQ5NiAwLjI5ODgyOCwwLjM5MjU3ODEgQyAyNi43MTkwODUsOC4wNDg1NTI4IDI2LjYxOTY3OSw3LjkxODM1MiAyNi41MTc1NzgsNy43ODkwNjE2IFogTSAyNi45MTAxNTYsOC4zMTI1IGMgMC4wODU5NywwLjEyMDYyOTIgMC4xNzE5MTgsMC4yNDE2NzMzIDAuMjUzOTA2LDAuMzY1MjM0NCBDIDI3LjA4MjE2MSw4LjU1NDM0MzYgMjYuOTk2MDM1LDguNDMyOTY1NSAyNi45MTAxNTYsOC4zMTI1IFogbSAtMTEuNDA2MjUsMC4wMjM0MzcgLTAuMTIxMDkzLDAuMDA3ODEgQyAxMS40MTI3OTgsOC42NTcxMTA1IDguMjgzMjAzMSwxMS45NjkxODcgOC4yODMyMDMxLDE2IGMgMCw0LjAzMDgzNSAzLjEyOTQwNzksNy4zNDI5MTggNy4wOTk2MDk5LDcuNjU2MjUgbCAwLjEyMTA5MywwLjAwNzggMC4wMTk1MywtMC4yNDAyMzQgLTAuMTIxMDk0LC0wLjAwOTggQyAxMS41NTU2NjUsMjMuMTEwNTcyIDguNTI1MzkwNiwxOS45MDU0MDkgOC41MjUzOTA2LDE2IGMgMCwtMy45MDUzODcgMy4wMzAyNzA0LC03LjExMDQ1NTggNi44NzY5NTM0LC03LjQxNDA2MjUgbCAwLjEyMTA5NCwtMC4wMDk3NyB6IG0gMTEuNzg3MTEsMC41NDI5Njg4IGMgMC4wNjkwMywwLjEwODE3MDQgMC4xMzkxMzMsMC4yMTU5MTA0IDAuMjA1MDc4LDAuMzI2MTcxOCAtMC4wNjU5LC0wLjExMDE1MTMgLTAuMTM2MDk5LC0wLjIxODEwODQgLTAuMjA1MDc4LC0wLjMyNjE3MTggeiBtIDAuMzQ1NzAzLDAuNTcyMjY1NiBjIDAuMDUzMjcsMC4wOTM1NTkgMC4xMDcxNjIsMC4xODYyOTU0IDAuMTU4MjAzLDAuMjgxMjUgLTAuMDUxMTIsLTAuMDk1MDgyIC0wLjEwNDg0NCwtMC4xODc1Njg1IC0wLjE1ODIwMywtMC4yODEyNSB6IG0gLTEyLjEzNDc2NiwwLjM5ODQzOCAtMC4xMjEwOTQsMC4wMTM2NzIgQyAxMi4yNTE1MjcsMTAuMTczMDgxIDkuODAyNzM0NCwxMi44MDQ4NzMgOS44MDI3MzQ0LDE2IGMgMCwzLjE5NTE0NCAyLjQ0ODUxMjYsNS44MjY5MiA1LjU3ODEyNDYsNi4xMzY3MTkgbCAwLjEyMTA5NCwwLjAxMzY3IDAuMDIzNDQsLTAuMjQyMTg4IC0wLjEyMTA5NCwtMC4wMTE3MiBDIDEyLjM5NjI0NiwyMS41OTg3MjUgMTAuMDQ0OTIyLDE5LjA3MTM0OCAxMC4wNDQ5MjIsMTYgYyAwLC0zLjA3MTMyOSAyLjM1MTMyOCwtNS41OTg3MTkgNS4zNTkzNzUsLTUuODk2NDg0IGwgMC4xMjEwOTQsLTAuMDExNzIgeiBtIDEyLjQ3NDYwOSwwLjI0MDIzNDYgYyAwLjAzMDk1LDAuMDYyMDIgMC4wNjM3NSwwLjEyMjk4NiAwLjA5Mzc1LDAuMTg1NTQ3IC0wLjAyOTk1LC0wLjA2MjQzIC0wLjA2Mjg1LC0wLjEyMzY2NCAtMC4wOTM3NSwtMC4xODU1NDcgeiBtIDAuMjgzMjA0LDAuNTk5NjA5IGMgMC4wMjU0LDAuMDU4MDIgMC4wNTE1OCwwLjExNTM4NCAwLjA3NjE3LDAuMTczODI4IC0wLjAyNDU4LC0wLjA1ODQxIC0wLjA1MDc4LC0wLjExNTg0OSAtMC4wNzYxNywtMC4xNzM4MjggeiBtIDAuMjM0Mzc1LDAuNTc0MjE5IGMgMC4wMjY0MiwwLjA2OTAyIDAuMDU0NzgsMC4xMzc0NyAwLjA4MDA4LDAuMjA3MDMxIC0wLjAyNTM2LC0wLjA2OTcyIC0wLjA1MzU5LC0wLjEzNzg2MSAtMC4wODAwOCwtMC4yMDcwMzEgeiBtIC0xMi45OTgwNDcsMC4xMDU0NjkgLTAuMTE5MTQxLDAuMDE1NjMgQyAxMy4wODk4MDUsMTEuNjg4NjA0IDExLjMyMjI2NiwxMy42NDE0OTMgMTEuMzIyMjY2LDE2IGMgMCwyLjM1ODUyMSAxLjc2NzE3Nyw0LjMxMTM4NyA0LjA1NDY4Nyw0LjYxNTIzNCBsIDAuMTE5MTQxLDAuMDE1NjMgMC4wMzMyLC0wLjI0MDIzNCBMIDE1LjQwODIsMjAuMzc1IEMgMTMuMjM4ODQ1LDIwLjA4Njg0NyAxMS41NjQ0NSwxOC4yMzc0NjMgMTEuNTY0NDUsMTYgYyAwLC0yLjIzNzQ0NyAxLjY3NDM5NSwtNC4wODY4NDYgMy44NDM3NSwtNC4zNzUgbCAwLjEyMTA5NCwtMC4wMTU2MyB6IE0gMjguODc1LDEyLjQxNjAxNiBjIDAuMDMwOCwwLjEwOTg3NSAwLjA2Mzc4LDAuMjE5MDczIDAuMDkxOCwwLjMzMDA3OCAtMC4wMjc5OSwtMC4xMTA4NDYgLTAuMDYxMDMsLTAuMjIwMzYgLTAuMDkxOCwtMC4zMzAwNzggeiBtIC0xMy4zODg2NzIsMC40Nzg1MTUgLTAuMTE3MTg3LDAuMDIzNDQgQyAxMy45MjgwNzYsMTMuMjA5NjMyIDEyLjgzOTg0NCwxNC40ODA2NTEgMTIuODM5ODQ0LDE2IGMgMCwxLjUxOTM1IDEuMDg3ODAyLDIuNzkwMzY1IDIuNTI5Mjk3LDMuMDgyMDMxIGwgMC4xMTcxODcsMC4wMjM0NCAwLjA0ODgzLC0wLjIzODI4MiAtMC4xMTkxNCwtMC4wMjM0NCBDIDE0LjA4NTM0MiwxOC41NzQ1MSAxMy4wODM5ODQsMTcuNDAzODUyIDEzLjA4Mzk4NCwxNiBjIDAsLTEuNDAzODUxIDEuMDAxMzYyLC0yLjU3NDUwNyAyLjMzMjAzMiwtMi44NDM3NSBsIDAuMTE5MTQsLTAuMDIzNDQgeiBNIDI5LDEyLjg5NjQ4NCBDIDI5LjAzODA2LDEzLjA1NTA3IDI5LjA3NTEyLDEzLjIxNDI2MiAyOS4xMDc0MjIsMTMuMzc1IDI5LjA3NTA2NywxMy4yMTQwNTQgMjkuMDM4MTMyLDEzLjA1NTI3MSAyOSwxMi44OTY0ODQgWiBtIDAuMTM4NjcyLDAuNjQ2NDg1IGMgMC4wMjk1NywwLjE1NzcwOCAwLjA1ODEsMC4zMTQ5OTEgMC4wODIwMywwLjQ3NDYwOSAtMC4wMjM5MSwtMC4xNTkyNjIgLTAuMDUyNTEsLTAuMzE3MjUgLTAuMDgyMDMsLTAuNDc0NjA5IHogbSAtMTMuNjgzNTk0LDAuOTEyMTA5IC0wLjExMTMyOCwwLjA0ODgzIEMgMTQuNzY0OTMyLDE0Ljc1NjA0MyAxNC4zNTkzNzUsMTUuMzMxNjcyIDE0LjM1OTM3NSwxNiBjIDAsMC42NjgzMjkgMC40MDUxMjcsMS4yNDM5NTIgMC45ODQzNzUsMS40OTYwOTQgbCAwLjExMTMyOCwwLjA0ODgzIDAuMDk3NjYsLTAuMjIyNjU2IC0wLjExMTMyOCwtMC4wNDY4NyBDIDE0Ljk0ODAwMywxNy4wNjA2MDYgMTQuNjAxNTYsMTYuNTcwNDM5IDE0LjYwMTU2MywxNiBjIDAsLTAuNTcwNDM4IDAuMzQ2NDQ3LC0xLjA2MDYyIDAuODM5ODQzLC0xLjI3NTM5MSBsIDAuMTExMzI4LC0wLjA0Njg3IHogbSAxMy43NjU2MjUsMy41MjczNDQgYyAtMC4wMjM5MywwLjE1OTYxOCAtMC4wNTI0NiwwLjMxNjkwMSAtMC4wODIwMywwLjQ3NDYwOSAwLjAyOTUyLC0wLjE1NzM1OSAwLjA1ODEzLC0wLjMxNTM0NyAwLjA4MjAzLC0wLjQ3NDYwOSB6IE0gMjkuMTA3NDIyLDE4LjYyNSBjIC0wLjAzMjMsMC4xNjA3MzggLTAuMDY5MzYsMC4zMTk5MyAtMC4xMDc0MjIsMC40Nzg1MTYgMC4wMzgxMywtMC4xNTg3ODcgMC4wNzUwNywtMC4zMTc1NyAwLjEwNzQyMiwtMC40Nzg1MTYgeiBtIC0wLjE0MDYyNSwwLjYyODkwNiBjIC0wLjAyODAyLDAuMTExMDA1IC0wLjA2MSwwLjIyMDIwMyAtMC4wOTE4LDAuMzMwMDc4IDAuMDMwNzcsLTAuMTA5NzE4IDAuMDYzOCwtMC4yMTkyMzIgMC4wOTE4LC0wLjMzMDA3OCB6IG0gLTAuMzkyNTc4LDEuMjc1MzkxIGMgLTAuMDI1MywwLjA2OTU2IC0wLjA1MzY2LDAuMTM4MDE2IC0wLjA4MDA4LDAuMjA3MDMxIDAuMDI2NDksLTAuMDY5MTcgMC4wNTQ3MiwtMC4xMzczMTIgMC4wODAwOCwtMC4yMDcwMzEgeiBtIC0wLjIzODI4MiwwLjYwNzQyMiBjIC0wLjAyNDU5LDAuMDU4NDQgLTAuMDUwNzcsMC4xMTU4MTEgLTAuMDc2MTcsMC4xNzM4MjggMC4wMjUzOSwtMC4wNTc5OCAwLjA1MTU5LC0wLjExNTQyMiAwLjA3NjE3LC0wLjE3MzgyOCB6IG0gLTAuMjY1NjI1LDAuNTg3ODkgYyAtMC4wMywwLjA2MjU2IC0wLjA2MjgsMC4xMjM1MzEgLTAuMDkzNzUsMC4xODU1NDcgMC4wMzA5LC0wLjA2MTg4IDAuMDYzOCwtMC4xMjMxMiAwLjA5Mzc1LC0wLjE4NTU0NyB6IG0gLTAuMjc1MzksMC41NDI5NjkgYyAtMC4wNTEwNCwwLjA5NDk1IC0wLjEwNDkzMiwwLjE4NzY5MSAtMC4xNTgyMDMsMC4yODEyNSAwLjA1MzM2LC0wLjA5MzY4IDAuMTA3MDgxLC0wLjE4NjE2OCAwLjE1ODIwMywtMC4yODEyNSB6IG0gLTAuMjk4ODI4LDAuNTI3MzQ0IGMgLTAuMDY1OTQsMC4xMTAyNjEgLTAuMTM2MDUxLDAuMjE4MDAxIC0wLjIwNTA3OCwwLjMyNjE3MiAwLjA2ODk4LC0wLjEwODA2NCAwLjEzOTE3NiwtMC4yMTYwMjEgMC4yMDUwNzgsLTAuMzI2MTcyIHogbSAtMC4zMzIwMzIsMC41MjczNDQgYyAtMC4wODE5OSwwLjEyMzU2MSAtMC4xNjc5MzEsMC4yNDQ2MDUgLTAuMjUzOTA2LDAuMzY1MjM0IDAuMDg1ODgsLTAuMTIwNDY2IDAuMTcyMDA1LC0wLjI0MTg0NCAwLjI1MzkwNiwtMC4zNjUyMzQgeiBtIC0wLjM0NzY1NiwwLjQ5NjA5MyBjIC0wLjA5NzE4LDAuMTMyOTI5IC0wLjE5Njg4MSwwLjI2MzQzOCAtMC4yOTg4MjgsMC4zOTI1NzggMC4xMDIxMDEsLTAuMTI5Mjg5IDAuMjAxNTA3LC0wLjI1OTQ5IDAuMjk4ODI4LC0wLjM5MjU3OCB6IG0gLTAuMzc1LDAuNDg4MjgyIGMgLTAuMDkwODEsMC4xMTI4OTIgLTAuMTgyOTU0LDAuMjI0MTQ4IC0wLjI3NzM0NCwwLjMzMzk4NCAwLjA5NDI2LC0wLjEwOTY1OSAwLjE4NjY1MSwtMC4yMjEyNzkgMC4yNzczNDQsLTAuMzMzOTg0IHogbSAtMC40Mzk0NTMsMC41MTk1MzEgYyAtMC4wOTA3MiwwLjEwMTcxMiAtMC4xODM1NzUsMC4yMDE4OTUgLTAuMjc3MzQ0LDAuMzAwNzgxIDAuMDkzODcsLTAuMDk4OTYgMC4xODY1MzMsLTAuMTk4OTg4IDAuMjc3MzQ0LC0wLjMwMDc4MSB6IG0gLTAuNDAyMzQ0LDAuNDMxNjQgYyAtMC4xMDE0NjIsMC4xMDQxNTMgLTAuMjA1NzMyLDAuMjA1ODI3IC0wLjMxMDU0NywwLjMwNjY0MSAwLjEwNDk5MiwtMC4xMDA5MzYgMC4yMDg5MTcsLTAuMjAyMzU1IDAuMzEwNTQ3LC0wLjMwNjY0MSB6IG0gLTAuNDcyNjU2LDAuNDYwOTM4IGMgLTAuMDkyNTQsMC4wODYwOCAtMC4xODYyNzksMC4xNzA0NDQgLTAuMjgxMjUsMC4yNTM5MDYgMC4wOTQ3MiwtMC4wODMyMSAwLjE4ODk1NSwtMC4xNjgwOTMgMC4yODEyNSwtMC4yNTM5MDYgeiBtIC0wLjQ3MjY1NiwwLjQxNzk2OSBjIC0wLjA5ODUyLDAuMDgzMzUgLTAuMTk3ODcyLDAuMTY1NTY2IC0wLjI5ODgyOCwwLjI0NjA5MyAwLjEwMTEzNiwtMC4wODA2NCAwLjIwMDEzMiwtMC4xNjI2MTkgMC4yOTg4MjgsLTAuMjQ2MDkzIHogbSAtMC40OTQxNDEsMC4zOTg0MzcgYyAtMC4xMDEwOCwwLjA3NzYyIC0wLjIwMzI5MiwwLjE1Mzc0NSAtMC4zMDY2NCwwLjIyODUxNiAwLjEwMzMzOCwtMC4wNzQ3MyAwLjIwNTU2OSwtMC4xNTA5NCAwLjMwNjY0LC0wLjIyODUxNiB6IG0gLTAuNDg4MjgxLDAuMzU3NDIyIGMgLTAuMTIwMzMxLDAuMDg0MDQgLTAuMjQyMDMzLDAuMTY1OTQzIC0wLjM2NTIzNCwwLjI0NjA5NCAwLjEyMzIxOSwtMC4wODAxMyAwLjI0NDg4NSwtMC4xNjIwNzQgMC4zNjUyMzQsLTAuMjQ2MDk0IHogbSAtMC41MDM5MDYsMC4zMzU5MzggYyAtMC4xMzUyNDQsMC4wODU2MyAtMC4yNzE2NjQsMC4xNjkxMzMgLTAuNDEwMTU3LDAuMjUgMC4xMzgzMiwtMC4wODA3NCAwLjI3NTA3OCwtMC4xNjQ1MTQgMC40MTAxNTcsLTAuMjUgeiBtIC0wLjUxNzU3OCwwLjMxMDU0NiBjIC0wLjE0ODUwNCwwLjA4NDg4IC0wLjI5NzE5MywwLjE2ODc0NyAtMC40NDkyMTksMC4yNDgwNDcgMC4xNTE3MjIsLTAuMDc5MTMgMC4zMDEwMDQsLTAuMTYzMzU1IDAuNDQ5MjE5LC0wLjI0ODA0NyB6IG0gLTAuNTQ0OTIyLDAuMjk2ODc1IGMgLTAuMTYwOTE4LDAuMDgyMyAtMC4zMjM2NDcsMC4xNjIzNyAtMC40ODgyODIsMC4yMzgyODIgMC4xNjUyNjEsLTAuMDc2MTYgMC4zMjY3NjUsLTAuMTU1Njg4IDAuNDg4MjgyLC0wLjIzODI4MiB6IG0gLTAuNTQ2ODc1LDAuMjYzNjcyIGMgLTAuMTcyMzMxLDAuMDc4NDYgLTAuMzQ3MjYsMC4xNTMzMTIgLTAuNTIzNDM4LDAuMjI0NjEgMC4xNzYwNTgsLTAuMDcxMjIgMC4zNTEyMiwtMC4xNDYyMzkgMC41MjM0MzgsLTAuMjI0NjEgeiBtIC0wLjU4Nzg5MSwwLjI1IGMgLTAuMTY0NTEsMC4wNjU1OCAtMC4zMzAzNzUsMC4xMjgzMDkgLTAuNDk4MDQ3LDAuMTg3NSAwLjE2NzcyMywtMC4wNTkxOCAwLjMzMzQ4NiwtMC4xMjE5MjUgMC40OTgwNDcsLTAuMTg3NSB6IG0gLTAuNjg1NTQ3LDAuMjUxOTUzIGMgLTAuMTQyMzAzLDAuMDQ3ODcgLTAuMjg1Mjc2LDAuMDkzNTMgLTAuNDI5Njg3LDAuMTM2NzE5IDAuMTQ0NDU2LC0wLjA0MzE4IDAuMjg3MzM5LC0wLjA4ODg2IDAuNDI5Njg3LC0wLjEzNjcxOSB6IG0gLTAuNTY2NDA2LDAuMTc1NzgyIGMgLTAuMTczMjUyLDAuMDQ5OCAtMC4zNDczNjEsMC4wOTc2OCAtMC41MjM0MzgsMC4xNDA2MjUgLTAuMjA3NDIsMC4wNTA1OSAtMC40MTU4NDYsMC4wOTc4MyAtMC42MjY5NTMsMC4xMzg2NzIgMC4yMTExMTMsLTAuMDQwODIgMC40MTk1MjUsLTAuMDg4MSAwLjYyNjk1MywtMC4xMzg2NzIgMC4xNzU5MTMsLTAuMDQyODkgMC4zNTAzNDMsLTAuMDkwODkgMC41MjM0MzgsLTAuMTQwNjI1IHogbSAtMS4xNjc5NjksMC4yODEyNSBjIC0wLjIwODA4MywwLjAzOTk3IC0wLjQxNzQ5NCwwLjA3NTIxIC0wLjYyODkwNiwwLjEwNTQ2OCAtMC4yMTE4ODUsMC4wMzAzMyAtMC40MjU2MzksMC4wNTU4NCAtMC42NDA2MjUsMC4wNzYxNyAwLjIxNTA2LC0wLjAyMDMzIDAuNDI4NjY1LC0wLjA0NTg0IDAuNjQwNjI1LC0wLjA3NjE3IDAuMjExMjg0LC0wLjAzMDIzIDAuNDIwOTQ1LC0wLjA2NTU0IDAuNjI4OTA2LC0wLjEwNTQ2OCB6IiAgICBzdHlsZT0iYmFzZWxpbmUtc2hpZnQ6YmFzZWxpbmU7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2ZWN0b3ItZWZmZWN0Om5vbmU7ZmlsbDojMjIyMjIyO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgaWQ9InBhdGgxNCIgLz48L3N2Zz4=');}.icon-jakevan{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJjbGlwLXJ1bGU6ZXZlbm9kZDtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MiIgdmlld0JveD0iMCAwIDMyIDMyIj48cGF0aCBkPSJNMTcuODggMTQuNjhIMTIuOWwtLjQzLTEuNjNIOS41OGwtLjQ1IDEuNjNINi41bDIuODktOC43NGgzLjJsMi44OSA4LjY0di04LjZoMi40djMuNzhjLjEtLjIuMjItLjM4LjM1LS41Ny4xMy0uMi4yNi0uMzcuMzktLjU0bDEuODYtMi42N2g3Ljh2MS44OUgyNS40djEuMzdoMi42NXYxLjg4SDI1LjR2MS42NWgyLjg2djEuOTFoLTcuOTNsLTEuNzUtMy4zMi0uNy40MXptNS4xMy04LjU5LTIuNyAzLjc5IDIuNyA0Ljc0em0tMTEuMDUgNS4wMy0uMzgtMS40M2ExMzYuODYgMTM2Ljg2IDAgMCAwLS40LTEuNTVMMTEgNy4zOGExNy43NiAxNy43NiAwIDAgMS0uMzYgMS42bC0uMTguNzEtLjM5IDEuNDN6bS04LjU4IDYuM2E1Ljc0IDUuNzQgMCAwIDEtMS4yNC0uMTN2LTEuODNsLjQxLjA4Yy4xNS4wMy4zLjA1LjQ3LjA1LjMgMCAuNTEtLjA2LjY3LS4xN2EuOTIuOTIgMCAwIDAgLjM0LS41MmMuMDYtLjIzLjEtLjUyLjEtLjg2VjUuOThoMi40djcuODVjMCAuODgtLjEzIDEuNTctLjQgMi4xLS4yNi41Mi0uNjMuOS0xLjEgMS4xNC0uNDguMjMtMS4wMy4zNS0xLjY1LjM1WiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE4NDM5MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0yMi44MiAyMi4yN2gtNC4wNmwtLjM3LTEuNEgxNS45bC0uMzkgMS40aC0yLjI2bDIuNDktNy41M2gyLjc1bDIuNDkgNy40NHYtNy40MWgyLjdsMi43NyA1LjIxaC4wM2E0MS4xIDQxLjEgMCAwIDEtLjA3LTEuODJ2LTMuMzloMS44M3Y3LjVoLTIuN2wtMi43OS01LjI4aC0uMDRhMTIuODMgMTIuODMgMCAwIDEgLjA4IDEuMjZsLjAyLjY0em0tNC44Ni0zLjA3LS4zMy0xLjIzYTg5LjA3IDg5LjA3IDAgMCAwLS4zNS0xLjM0bC0uMTQtLjY1YTE1LjA0IDE1LjA0IDAgMCAxLS4zMSAxLjM3bC0uMTYuNjItLjMzIDEuMjN6bS0zLjg1LTQuNDMtMi41IDcuNUg5LjJsLTIuNS03LjVoMi4zMmwxLjA0IDMuOGExNS4wMyAxNS4wMyAwIDAgMSAuMzYgMS43NiA3LjYxIDcuNjEgMCAwIDEgLjItMS4ybC4xNC0uNTQgMS4wNi0zLjgyeiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE1OTg4NCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0xMS45IDI0LjIxYzAgLjQtLjA3LjcyLS4yLjk5LS4xNS4yNi0uMzYuNDYtLjYzLjYtLjI4LjEzLS42Mi4yLTEuMDMuMkg5LjJ2LTMuNWguOTdjLjM4IDAgLjcuMDYuOTYuMTkuMjUuMTMuNDUuMzIuNTguNTguMTQuMjUuMi41Ny4yLjk0em0tLjI2LjAxYzAtLjMzLS4wNS0uNjEtLjE2LS44M2ExLjEgMS4xIDAgMCAwLS41MS0uNTEgMS45NSAxLjk1IDAgMCAwLS44Ny0uMTdoLS42NnYzLjA3aC42Yy41MyAwIC45My0uMTMgMS4yLS4zOS4yNy0uMjYuNC0uNjUuNC0xLjE3ek0xNC4yNyAyNmgtMS45NXYtMy41aDEuOTV2LjIyaC0xLjd2MS4zMmgxLjZ2LjIzaC0xLjZ2MS41aDEuN3ptMS4yOC0zLjVjLjI4IDAgLjUyLjAyLjcuMDhhLjguOCAwIDAgMSAuNDQuM2MuMS4xNC4xNC4zMy4xNC41N2EuOS45IDAgMCAxLS4xLjQ1Ljg3Ljg3IDAgMCAxLS4yNy4zMmMtLjEyLjA4LS4yNS4xNC0uNC4xOGwuOTggMS42aC0uM2wtLjkyLTEuNTNoLS44OVYyNmgtLjI1di0zLjV6bS0uMDMuMjFoLS41OXYxLjU1aC43MWMuMyAwIC41Mi0uMDcuNjktLjIxLjE2LS4xNC4yNC0uMzQuMjQtLjYgMC0uMjgtLjA5LS40Ny0uMjYtLjU4LS4xNy0uMS0uNDMtLjE2LS43OS0uMTZ6bTUuNTctLjIyTDIwLjEyIDI2aC0uMjVsLS43Ni0yLjY1LS4wNS0uMTYtLjA0LS4xNGExOC44IDE4LjggMCAwIDEtLjA2LS4yNCAyMC42IDIwLjYgMCAwIDEtLjExLjQ4TDE4LjA5IDI2aC0uMjVsLS45Ni0zLjVoLjI2bC42NyAyLjQ3YTI3LjM2IDI3LjM2IDAgMCAxIC4wOS4zNWwuMDQuMTcuMDMuMTUuMDMtLjE2YTQuODMgNC44MyAwIDAgMSAuMTQtLjUzbC43LTIuNDZoLjI1bC43MyAyLjQ4YTExLjk4IDExLjk4IDAgMCAxIC4xMy41M2wuMDQuMTVhMTEuMDIgMTEuMDIgMCAwIDEgLjE1LS42OGwuNjktMi40OHpNMjMuMjYgMjZoLTEuOTV2LTMuNWgxLjk1di4yMmgtMS43djEuMzJoMS42di4yM2gtMS42djEuNWgxLjd6bTEuMjgtMy41Yy4yOCAwIC41Mi4wMi43MS4wOGEuOC44IDAgMCAxIC40My4zYy4xLjE0LjE0LjMzLjE0LjU3YS45LjkgMCAwIDEtLjEuNDUuODcuODcgMCAwIDEtLjI3LjMyYy0uMTEuMDgtLjI1LjE0LS40LjE4bC45OCAxLjZoLS4zbC0uOTItMS41M2gtLjg4VjI2aC0uMjZ2LTMuNXptLS4wMi4yMWgtLjZ2MS41NWguNzJjLjI5IDAgLjUxLS4wNy42OC0uMjEuMTYtLjE0LjI0LS4zNC4yNC0uNiAwLS4yOC0uMDgtLjQ3LS4yNi0uNTgtLjE3LS4xLS40My0uMTYtLjc4LS4xNnpNMjYuNSAyNmgtLjI1di0zLjVoMS45NXYuMjJoLTEuN3YxLjQ5aDEuNnYuMjJoLTEuNnoiIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvcjtzdHJva2Utd2lkdGg6LjAxMDEwNjgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuNCAuNjYpIHNjYWxlKC45NjA1MDEzNCkiLz48L3N2Zz4=');}.icon-user-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRaTTk0LDEyMGEzNCwzNCwwLDEsMSwzNCwzNEEzNCwzNCwwLDAsMSw5NCwxMjBaTTY1Ljc3LDIxMGE2Ni40Myw2Ni40MywwLDAsMSwyMC43Ny0yOS4zNiw2Niw2NiwwLDAsMSw4Mi45MiwwQTY2LjQzLDY2LjQzLDAsMCwxLDE5MC4yMywyMTBaTTIxMCwyMDhhMiwyLDAsMCwxLTIsMmgtNS4xN2E3Ny44NSw3Ny44NSwwLDAsMC00OS4zOC01MS43MSw0Niw0NiwwLDEsMC01MC45LDBBNzcuODUsNzcuODUsMCwwLDAsNTMuMTcsMjEwSDQ4YTIsMiwwLDAsMS0yLTJWNDhhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-chat-teardrop{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzIsMjZhOTguMTEsOTguMTEsMCwwLDAtOTgsOTh2ODRhMTQsMTQsMCwwLDAsMTQsMTRoODRhOTgsOTgsMCwwLDAsMC0xOTZabTAsMTg0SDQ4YTIsMiwwLDAsMS0yLTJWMTI0YTg2LDg2LDAsMSwxLDg2LDg2WiIvPjwvc3ZnPg==');}.icon-house-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNkgyMTZhNiw2LDAsMCwwLDYtNlYxMjBBMTMuOTIsMTMuOTIsMCwwLDAsMjE3LjksMTEwLjFaTTIxMCwyMTBINDZWMTIwYTIsMiwwLDAsMSwuNTgtMS40Mmw4MC04MGEyLDIsMCwwLDEsMi44NCwwbDgwLDgwQTIsMiwwLDAsMSwyMTAsMTIwWiIvPjwvc3ZnPg==');}.icon-caret-left{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMjAzLjc2YTYsNiwwLDEsMS04LjQ4LDguNDhsLTgwLTgwYTYsNiwwLDAsMSwwLTguNDhsODAtODBhNiw2LDAsMCwxLDguNDgsOC40OEw4OC40OSwxMjhaIi8+PC9zdmc+');}.icon-chat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-envelope{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+');}.icon-caret-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODAuMjQsMTMyLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OC04LjQ4TDE2Ny41MSwxMjgsOTEuNzYsNTIuMjRhNiw2LDAsMCwxLDguNDgtOC40OGw4MCw4MEE2LDYsMCwwLDEsMTgwLjI0LDEzMi4yNFoiLz48L3N2Zz4=');}.icon-calendar{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==');}.icon-shuffle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTc5Ljc2YTYsNiwwLDAsMSwwLDguNDhsLTI0LDI0YTYsNiwwLDAsMS04LjQ4LTguNDhMMjE3LjUyLDE5MEgyMDAuOTRhNzAuMTYsNzAuMTYsMCwwLDEtNTctMjkuMzFsLTQxLjcxLTU4LjRBNTguMTEsNTguMTEsMCwwLDAsNTUuMDYsNzhIMzJhNiw2LDAsMCwxLDAtMTJINTUuMDZhNzAuMTYsNzAuMTYsMCwwLDEsNTcsMjkuMzFsNDEuNzEsNTguNEE1OC4xMSw1OC4xMSwwLDAsMCwyMDAuOTQsMTc4aDE2LjU4bC0xMy43Ni0xMy43NmE2LDYsMCwwLDEsOC40OC04LjQ4Wm0tOTIuMDYtNzQuNDFhNS45MSw1LjkxLDAsMCwwLDMuNDgsMS4xMiw2LDYsMCwwLDAsNC44OS0yLjUxbDEuMTktMS42N0E1OC4xMSw1OC4xMSwwLDAsMSwyMDAuOTQsNzhoMTYuNThMMjAzLjc2LDkxLjc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMjQtMjRhNiw2LDAsMCwwLDAtOC40OGwtMjQtMjRhNiw2LDAsMCwwLTguNDgsOC40OEwyMTcuNTIsNjZIMjAwLjk0YTcwLjE2LDcwLjE2LDAsMCwwLTU3LDI5LjMxTDE0Mi43OCw5N0E2LDYsMCwwLDAsMTQ0LjE4LDEwNS4zNVptLTMyLjM2LDQ1LjNhNiw2LDAsMCwwLTguMzcsMS4zOWwtMS4xOSwxLjY3QTU4LjExLDU4LjExLDAsMCwxLDU1LjA2LDE3OEgzMmE2LDYsMCwwLDAsMCwxMkg1NS4wNmE3MC4xNiw3MC4xNiwwLDAsMCw1Ny0yOS4zMWwxLjE5LTEuNjdBNiw2LDAsMCwwLDExMS44MiwxNTAuNjVaIi8+PC9zdmc+');}.icon-sort-descending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00MiwxMjhhNiw2LDAsMCwxLDYtNmg3MmE2LDYsMCwwLDEsMCwxMkg0OEE2LDYsMCwwLDEsNDIsMTI4Wm02LTU4aDU2YTYsNiwwLDAsMCwwLTEySDQ4YTYsNiwwLDAsMCwwLDEyWk0xODQsMTg2SDQ4YTYsNiwwLDAsMCwwLDEySDE4NGE2LDYsMCwwLDAsMC0xMlpNMjI4LjI0LDgzLjc2bC00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsOC40OCw4LjQ4TDE3OCw2Mi40OVYxNDRhNiw2LDAsMCwwLDEyLDBWNjIuNDlsMjkuNzYsMjkuNzVhNiw2LDAsMCwwLDguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-sort-ascending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjYsMTI4YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLDAtMTJoNzJBNiw2LDAsMCwxLDEyNiwxMjhaTTQ4LDcwSDE4NGE2LDYsMCwwLDAsMC0xMkg0OGE2LDYsMCwwLDAsMCwxMlptNTYsMTE2SDQ4YTYsNiwwLDAsMCwwLDEyaDU2YTYsNiwwLDAsMCwwLTEyWm0xMjQuMjQtMjIuMjRhNiw2LDAsMCwwLTguNDgsMEwxOTAsMTkzLjUxVjExMmE2LDYsMCwwLDAtMTIsMHY4MS41MWwtMjkuNzYtMjkuNzVhNiw2LDAsMCwwLTguNDgsOC40OGw0MCw0MGE2LDYsMCwwLDAsOC40OCwwbDQwLTQwQTYsNiwwLDAsMCwyMjguMjQsMTYzLjc2WiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsNzJhNiw2LDAsMCwxLTYsNkg5NFYyMDEuNTFsMzcuNzYtMzcuNzVhNiw2LDAsMCwxLDguNDgsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtNDgtNDhhNiw2LDAsMCwxLDguNDgtOC40OEw4MiwyMDEuNTFWNzJhNiw2LDAsMCwxLDYtNkgyMzJBNiw2LDAsMCwxLDIzOCw3MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTY0LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OCwwbC00OC00OGE2LDYsMCwxLDEsOC40OC04LjQ4TDE3MCwxOTMuNTFWNzBIMzJhNiw2LDAsMCwxLDAtMTJIMTc2YTYsNiwwLDAsMSw2LDZWMTkzLjUxbDM3Ljc2LTM3Ljc1YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-heart{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+');}.icon-dots-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTI4YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxMjhaTTYwLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw2MCwxMThabTEzNiwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDE5NiwxMThaIi8+PC9zdmc+');}.icon-star-half-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMCwyNC41LDE3LjgyTDEyOCwxOTguNDlsNTAuNTMsMzEuMDhBMTYuNCwxNi40LDAsMCwwLDIwMywyMTEuNzVsLTEzLjc2LTU4LjA3LDQ1LTM4LjgzQTE2LjQzLDE2LjQzLDAsMCwwLDIzOS4xOCw5Ny4yNlptLTE1LjM0LDUuNDctNDguNyw0MmE4LDgsMCwwLDAtMi41Niw3LjkxbDE0Ljg4LDYyLjhhLjM3LjM3LDAsMCwxLS4xNy40OGMtLjE4LjE0LS4yMy4xMS0uMzgsMGwtNTQuNzItMzMuNjVBOCw4LDAsMCwwLDEyOCwxODEuMVYzMmMuMjQsMCwuMjcuMDguMzUuMjZMMTUzLDkxLjg2YTgsOCwwLDAsMCw2Ljc1LDQuOTJsNjMuOTEsNS4xNmMuMTYsMCwuMjUsMCwuMzQuMjlTMjI0LDEwMi42MywyMjMuODQsMTAyLjczWiIvPjwvc3ZnPg==');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');}.icon-heart-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==');}
\ No newline at end of file
diff --git a/assets/css/nav.min.css b/assets/css/nav.min.css
index e9f2124..a11598a 100644
--- a/assets/css/nav.min.css
+++ b/assets/css/nav.min.css
@@ -1 +1 @@
-nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn_)}.on-this-page ul{width:100%}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.letters li{flex:1;max-width:calc(7.69% - 2px)}@media (min-width:768px){nav.letters li{flex:0 1 auto;max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:flex-start;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:max-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--trans-base)}.index li.active,.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}
\ No newline at end of file
+nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}.on-this-page ul{width:100%}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:flex-start;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:max-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--trans-base)}.index li.active,.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}
\ No newline at end of file
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index f6eee56..2471e91 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -91,7 +91,7 @@
 		});
 
 		this.queue.subscribe((event, data) => {
-			if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'content') return;
+			if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
 			if (event === 'operation-completed') {
 				this.handleQueueSuccess(event, data);
 			} else if (event === 'operation-failed-permanent') {
@@ -207,7 +207,6 @@
 	}
 	async handleQueueSuccess(event, data) {
 		this.store.clearCache();
-		this.store.clearHttpHeaders();
 		this.store.fetch();
 	}
 	handleQueueFailure(event, data) {
@@ -234,6 +233,20 @@
 			uploader: 'details.uploader'
 		};
 		this.ui = window.uiFromSelectors(this.elements);
+		if (this.ui.uploader) {
+			window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
+
+			window.jvbUploads.subscribe((event, data) => {
+				if (event === 'sent-to-queue') {
+					console.log(data);
+					if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
+						window.debouncer.schedule('crud-complete', ()=> {
+							this.store.clearHttpHeaders();
+						});
+					}
+				}
+			});
+		}
 		this.isTimeline = !!document.querySelector('[data-timeline]');
 	}
 	init() {
@@ -316,7 +329,7 @@
 						break;
 
 					case 'create':
-						this.modals.create.dataset.itemID = 'new';
+						this.modals.create.dataset.itemId = 'new';
 						this.modals.create.dataset.content = this.content;
 						this.modals.create.handleOpen();
 						break;
@@ -646,7 +659,7 @@
 
 		let item = this.store.get(parseInt(itemID));
 		if (item) {
-			this.ui.modals.edit.dataset.itemID = itemID;
+			this.ui.modals.edit.dataset.itemId = itemID;
 			this.ui.modals.edit.dataset.content = this.content;
 
 			let form = this.ui.modals.edit.querySelector('form');
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index d4cb8bc..95027e3 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -6,6 +6,7 @@
  *   this.store = window.jvbStore.register('feed', { config });
  */
 class DataStore {
+
 	constructor() {
 		// Singleton pattern
 		if (DataStore.instance) {
@@ -140,6 +141,8 @@
 			delete: (id) => this.delete(name, id),
 			get: (id) => this.get(name, id),
 			getAll: () => this.getAll(name),
+			getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
+			filterByIndex: (criteria) => this.filterByIndex(name, criteria),
 			getFiltered: () => this.getFiltered(name),
 			clear: () => this.clear(name),
 
@@ -747,30 +750,33 @@
 
 		// Reject functions
 		if (type === 'function') {
-			return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
+			if (validate) return { valid: false, error: `Function at ${path}` };
+			console.debug(`[DataStore] Stripped function at ${path}`);
+			return { valid: true, data: undefined };
 		}
 
 		// DOM elements
 		if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
-			return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
+			if (validate) return { valid: false, error: `DOM element at ${path}` };
+			console.debug(`[DataStore] Stripped DOM element at ${path}`);
+			return { valid: true, data: undefined };
 		}
 
 		// FormData - convert and continue
 		if (obj instanceof FormData) {
-			return validate
-				? { valid: false, error: `FormData at ${path}` }
-				: { valid: true, data: this.formDataToObject(obj) };
+			if (validate) return { valid: false, error: `FormData at ${path}` };
+			console.debug(`[DataStore] Converted FormData at ${path}`);
+			return { valid: true, data: this.formDataToObject(obj) };
 		}
 
 		// Preserve safe types
-		if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+		if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj) || obj instanceof Blob) {
 			return { valid: true, data: obj };
 		}
 
 		// Convert Sets to Arrays
 		if (obj instanceof Set) {
-			const arr = Array.from(obj);
-			return this.processForStorage(arr, validate, path);
+			return this.processForStorage(Array.from(obj), validate, path);
 		}
 
 		// Convert Maps to Objects
@@ -784,7 +790,7 @@
 			for (let i = 0; i < obj.length; i++) {
 				const result = this.processForStorage(obj[i], validate, `${path}[${i}]`);
 				if (!result.valid) return result;
-				if (result.data !== null) processed.push(result.data);
+				if (result.data !== undefined) processed.push(result.data);
 			}
 			return { valid: true, data: processed };
 		}
@@ -795,14 +801,14 @@
 			for (const [key, value] of Object.entries(obj)) {
 				const result = this.processForStorage(value, validate, `${path}.${key}`);
 				if (!result.valid) return result;
-				if (result.data !== null) processed[key] = result.data;
+				if (result.data !== undefined) processed[key] = result.data;
 			}
 			return { valid: true, data: processed };
 		}
 
-		return validate
-			? { valid: false, error: `Unknown type at ${path}` }
-			: { valid: true, data: null };
+		if (validate) return { valid: false, error: `Unknown type at ${path}` };
+		console.debug(`[DataStore] Stripped unknown type at ${path}`);
+		return { valid: true, data: undefined };
 	}
 
 	/***********************************************************************
@@ -829,6 +835,64 @@
 		const store = this.stores.get(name);
 		return Array.from(store.data.values());
 	}
+	/**
+	 * Filter in-memory data by multiple index/value pairs
+	 * @param {string} name - Store name
+	 * @param {Object} criteria - Object of { indexName: acceptedValue(s) }
+	 * @returns {Array} - Items matching ALL criteria
+	 *
+	 * @example
+	 * filterByIndex(name, { field: 'upload_123', status: ['queued', 'uploading'] })
+	 */
+	filterByIndex(name, criteria) {
+		const store = this.stores.get(name);
+		if (!store) return [];
+
+		return Array.from(store.data.values()).filter(item => {
+			return Object.entries(criteria).every(([key, value]) => {
+				const accepted = Array.isArray(value) ? value : [value];
+				return accepted.includes(item[key]);
+			});
+		});
+	}
+	/**
+	 * Get all items matching an index value
+	 * @param {string} name - Store name
+	 * @param {string} indexName - Name of the index to query
+	 * @param {*} value - Value to match
+	 * @returns {Promise<Array>} - Matching items
+	 */
+	async getAllByIndex(name, indexName, value) {
+		const store = this.stores.get(name);
+		const values = Array.isArray(value) ? value : [value];
+
+		// Try IndexedDB index query first (more efficient for large datasets)
+		if (store.db && store.db.objectStoreNames.contains(store.config.storeName)) {
+			try {
+				const tx = store.db.transaction([store.config.storeName], 'readonly');
+				const objectStore = tx.objectStore(store.config.storeName);
+
+				if (objectStore.indexNames.contains(indexName)) {
+					const index = objectStore.index(indexName);
+
+					const results = await Promise.all(
+						values.map(v => new Promise((resolve, reject) => {
+							const request = index.getAll(v);
+							request.onsuccess = () => resolve(request.result || []);
+							request.onerror = () => reject(request.error);
+						}))
+					);
+
+					return results.flat();
+				}
+			} catch (error) {
+				console.warn(`Index query failed for "${indexName}", falling back to filter:`, error);
+			}
+		}
+
+		// Fallback: filter in-memory data
+		return Array.from(store.data.values()).filter(item => values.includes(item[indexName]));
+	}
 
 	getFiltered(name) {
 		const store = this.stores.get(name);
@@ -859,12 +923,8 @@
 	}
 
 	/***********************************************************************
-	 * FILTER OPERATIONS (UNIFIED)
+	 * FILTER OPERATIONS
 	 ***********************************************************************/
-
-	/**
-	 * Unified filter update - handles all filter operations
-	 */
 	async updateFilters(name, updates, clearAll = false) {
 		const store = this.stores.get(name);
 		const oldFilters = { ...store.filters };
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 8e5fbdc..a0388e2 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -1,7 +1,3 @@
-/**
- * Enhanced FormController - Manages forms with special fields, caching, and queue integration
- * Works with DataStore for CRUD operations and standalone for front-end forms
- */
 class FormController {
 	constructor(config = {}) {
 		this.config = {
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
index 16ad1bf..7981602 100644
--- a/assets/js/concise/HandleSelection.js
+++ b/assets/js/concise/HandleSelection.js
@@ -1,391 +1,302 @@
-/**
- * HandleSelection - Reusable selection management for items in grids
- *
- * Handles selection logic including:
- * - Individual item selection/deselection
- * - Select all / Clear all
- * - Range selection (shift+click)
- * - Selection count updates
- * - Bulk action visibility
- *
- * @class HandleSelection
- */
 class HandleSelection {
-	/**
-	 * @param {Object} config - Configuration object
-	 * @param {HTMLElement} config.container - Container element holding items
-	 * @param {Object} config.ui - UI element references
-	 * @param {HTMLElement} config.ui.selectAll - Select all checkbox
-	 * @param {HTMLElement} config.ui.bulkControls - Bulk actions container
-	 * @param {HTMLElement} config.ui.count - Selection count display
-	 * @param {string} config.itemSelector - Selector for individual items
-	 * @param {string} config.checkboxSelector - Selector for item checkboxes
-	 * @param {Function} config.onSelectionChange - Optional callback when selection changes
-	 */
-	constructor(config) {
-		this.container = config.container;
-		this.ui = config.ui || {};
-		this.itemSelector = config.itemSelector || '.item';
-		this.checkboxSelector = config.checkboxSelector || '[name*="select-item"]';
+	constructor(options) {
+		this.container = options.container; // An actual element, not class name
+		this.selectors = {
+			item: options.item || '.item',
+			count: options.count || '.selection-count',
+			bulkControls: options.bulkControls || '.selection-actions',
+			checkbox: options.checkbox || '[name*="select-item"]',
+			selectAll: options.selectAll || '[data-select-all]',
+			wrapper: options.wrapper || ':has(.item-grid)'
+		};
+
+		this.ui = window.uiFromSelectors(this.selectors, this.container)
 
 		this.selectedItems = new Set();
-		this.lastSelected = null;
+		this.lastSelected = null; // For shift+click range selection
+		this.lastSelectedWrapper = null; //Tracks which wrapper we're in
+		this.lastClicked = null;
 		this.subscribers = new Set();
 
-		this.init();
+		this.initListeners();
 	}
 
-	init() {
-		// Bind event handlers
+	initListeners() {
 		this.clickHandler = this.handleClick.bind(this);
 		this.changeHandler = this.handleChange.bind(this);
 		this.keyHandler = this.handleKeys.bind(this);
 
-		// Attach listeners
-		this.container.addEventListener('click', this.clickHandler);
 		this.container.addEventListener('change', this.changeHandler);
+		this.container.addEventListener('click', this.clickHandler);
 		this.container.addEventListener('keydown', this.keyHandler);
 	}
 
+	handleChange(e) {
+		// Select all
+		if (e.target.matches(this.selectors.selectAll)) {
+			this.handleSelectAll(e.target);
+			return;
+		}
+
+		// Individual checkbox
+		if (e.target.matches(this.selectors.checkbox)) {
+			const item = e.target.closest(this.selectors.item);
+			if (!item) return;
+
+			// Find the immediate wrapper - check group first, then preview
+			const wrapper = this.getItemWrapper(item);
+			const id = this.getItemId(item);
+
+			// Clear selection if clicking in different wrapper without shift
+			if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
+				this.clearSelection();
+			}
+
+			if (e.target.checked) {
+				this.select(id, false);
+			} else {
+				this.deselect(id, false);
+			}
+
+			this.lastSelected = id;
+			this.lastSelectedWrapper = wrapper;
+		}
+	}
+
+	handleClick(e) {
+		const item = e.target.closest(this.selectors.item);
+		const wrapper = item ? this.getItemWrapper(item) : null;
+
+		if (wrapper) {
+			this.lastClicked = wrapper;
+		}
+
+		// Handle non-checkbox clicks on items
+		if (item && !e.target.matches(this.selectors.checkbox)) {
+			if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
+				this.clearSelection();
+				this.lastSelectedWrapper = wrapper;
+			}
+		}
+
+		// Shift+click for range selection
+		if (!e.shiftKey) return;
+
+		const checkbox = e.target.closest(this.selectors.checkbox);
+		if (!checkbox || !this.lastSelected || !this.lastSelectedWrapper) return;
+
+		if (!item || !wrapper) return;
+
+		// Range selection only works within the same wrapper
+		if (wrapper !== this.lastSelectedWrapper) return;
+
+		const items = Array.from(wrapper.querySelectorAll(this.selectors.item));
+		const currentId = this.getItemId(item);
+
+		const lastIndex = items.findIndex(el => this.getItemId(el) === this.lastSelected);
+		const currentIndex = items.findIndex(el => this.getItemId(el) === currentId);
+
+		if (lastIndex === -1 || currentIndex === -1) return;
+
+		const [start, end] = [Math.min(lastIndex, currentIndex), Math.max(lastIndex, currentIndex)];
+		const rangeItems = items.slice(start, end + 1);
+
+		rangeItems.forEach(rangeItem => {
+			this.select(this.getItemId(rangeItem));
+		});
+
+		this.notify('range-selected', {
+			selectedItems: new Set(this.selectedItems),
+			wrapper: wrapper
+		});
+	}
+
+	getItemWrapper(item) {
+		if (!item) return null;
+
+		// Split the compound selector and check each one
+		const wrapperSelectors = this.selectors.wrapper.split(',').map(s => s.trim());
+
+		for (const selector of wrapperSelectors) {
+			const wrapper = item.closest(selector);
+			if (wrapper) return wrapper;
+		}
+
+		return null;
+	}
+
 	handleKeys(e) {
 		// Ctrl/Cmd + A: Select all
 		if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
 			e.preventDefault();
 
-			if (this.ui.selectAll) {
-				this.ui.selectAll.checked = true;
-				this.selectAll(true);
-				if (window.jvbA11y) {
-					window.jvbA11y.announce('All items selected');
+			if (this.lastClicked) {
+				let check = this.lastClicked.querySelector(this.selectors.selectAll);
+				if (check) {
+					check.checked = true;
 				}
 			}
 		}
 
 		// Escape: Deselect all
 		if (e.key === 'Escape' && this.selectedItems.size > 0) {
-			this.selectAll(false);
+			this.clearSelection();
 			if (window.jvbA11y) {
 				window.jvbA11y.announce('Selection cleared');
 			}
 		}
+	}
 
-		// Delete/Backspace: Remove selected items
-		if ((e.key === 'Delete' || e.key === 'Backspace') &&
-			!e.target.matches('input, textarea')
-			&& this.selectedItems > 0) {
-			e.preventDefault();
-			if (confirm(`Remove ${this.selectedItems.size} selected item${this.selectedItems.size !== 1 ? 's' : ''}?`)) {
-				this.deselect(this.selectedItems);
+	handleSelectAll(trigger) {
+		const wrapper = this.getItemWrapper(trigger) || trigger.closest(this.selectors.wrapper);
+		if (!wrapper) return;
+
+		// Clear any existing selection from other wrappers first
+		if (this.lastSelectedWrapper && wrapper !== this.lastSelectedWrapper) {
+			this.clearSelection();
+		}
+
+		const items = wrapper.querySelectorAll(this.selectors.item);
+		const ids = Array.from(items).map(item => this.getItemId(item));
+
+		if (trigger.checked) {
+			ids.forEach(id => this.select(id, true, false));
+			this.lastSelectedWrapper = wrapper;
+		} else {
+			ids.forEach(id => this.deselect(id, true, false));
+			if (this.selectedItems.size === 0) {
+				this.lastSelectedWrapper = null;
 			}
 		}
-	}
 
-	handleClick(e) {
-		const checkbox = e.target.closest(`${this.checkboxSelector}, label[for]`);
-		if (!checkbox) return;
-
-		// Get the actual checkbox element
-		const input = checkbox.tagName === 'LABEL'
-			? document.getElementById(checkbox.getAttribute('for'))
-			: checkbox;
-
-		if (!input) return;
-
-		// Handle shift+click for range selection
-		if (e.shiftKey && this.lastSelected) {
-			e.preventDefault();
-			this.handleRangeSelection(input);
-		} else {
-			// Store last clicked for range selection
-			const item = input.closest(this.itemSelector);
-			if (item) {
-				this.lastSelected = item;
-			}
+		let label = trigger.nextElementSibling || trigger.previousElementSibling;
+		if (label && label.tagName === 'LABEL') {
+			label.textContent = (trigger.checked && items.length > 0) ? 'Clear Selection' : 'Select All';
 		}
-	}
-
-	handleChange(e) {
-		if (this.ui.selectAll && e.target === this.ui.selectAll) {
-			this.selectAll(e.target.checked);
-		} else {
-			const checkbox = e.target.closest(this.checkboxSelector);
-			if (!checkbox) return;
-
-			this.toggleSelection(this.getItemId(checkbox));
-		}
-	}
-
-	/**
-	 * Toggle selection state of an item
-	 */
-	toggleSelection(id) {
-		if (!id) return;
-
-		let selected = true;
-		if (this.selectedItems.has(id)) {
-			selected = false;
-			this.selectedItems.delete(id);
-		} else {
-			this.selectedItems.add(id);
-		}
-
-		if (selected) {
-			this.notify('item-selected', {
-				selectedItem: id,
-				selectedItems: this.selectedItems,
-				container: this.container
-			});
-		} else {
-			this.notify('item-deselected', {
-				selectedItem: id,
-				selectedItems: this.selectedItems,
-				container: this.container
-			});
-		}
-
-
 		this.updateSelectionUI();
-	}
-
-	/**
-	 * Select or deselect all items
-	 */
-	selectAll(checked) {
-		const items = this.container.querySelectorAll(this.itemSelector);
-
-		if (!checked) {
-			this.selectedItems.clear();
-			if (this.ui.selectAll) {
-				this.ui.selectAll.checked = false;
-			}
-		}
-
-		items.forEach(item => {
-			const id = this.getItemId(item);
-			const checkbox = item.querySelector(this.checkboxSelector);
-
-			if (checkbox) {
-				checkbox.checked = checked;
-			}
-
-			if (checked && id) {
-				this.selectedItems.add(id);
-			}
-		});
 
 		this.notify('select-all', {
-			container: this.container,
-			selected: checked,
-			items: items
-		})
-
-		this.updateSelectionUI();
-	}
-
-	/**
-	 * Clear all selections
-	 */
-	clearSelection() {
-		this.selectAll(false);
-	}
-
-	/**
-	 * Handle shift+click range selection
-	 */
-	handleRangeSelection(currentCheckbox) {
-		if (!this.lastSelected) {
-			this.lastSelected = currentCheckbox.closest(this.itemSelector);
-			return;
-		}
-
-		const currentItem = currentCheckbox.closest(this.itemSelector);
-		if (!currentItem) return;
-
-		// Get all items
-		const allItems = Array.from(this.container.querySelectorAll(this.itemSelector));
-
-		// Find indices
-		const lastIndex = allItems.indexOf(this.lastSelected);
-		const currentIndex = allItems.indexOf(currentItem);
-
-		if (lastIndex === -1 || currentIndex === -1) return;
-
-		// Determine range (handle both directions)
-		const startIndex = Math.min(lastIndex, currentIndex);
-		const endIndex = Math.max(lastIndex, currentIndex);
-
-		let isChecked = !currentCheckbox.checked;
-		// Select all items in range
-		for (let i = startIndex; i <= endIndex; i++) {
-			const item = allItems[i];
-			const checkbox = item.querySelector(this.checkboxSelector);
-			const id = this.getItemId(item);
-
-			if (checkbox && id) {
-				checkbox.checked = isChecked;
-				this.selectedItems.add(id);
-			}
-		}
-
-		// Update last selected to current
-		this.lastSelected = currentItem;
-
-		this.updateSelectionUI();
-
-		this.notify('range-selected', {
-			selectedItems: this.selectedItems,
-			container: this.container
+			wrapper: wrapper,
+			checked: trigger.checked,
+			ids: ids,
+			selectedItems: new Set(this.selectedItems)
 		});
-
-		// Announce for accessibility
-		const selectedCount = endIndex - startIndex + 1;
-		if (window.jvbA11y) {
-			window.jvbA11y.announce(`Selected ${selectedCount} items in range`);
-		}
 	}
 
-	/**
-	 * Update selection UI elements
-	 */
-	updateSelectionUI() {
-		const count = this.selectedItems.size;
-		const totalItems = this.container.querySelectorAll(this.itemSelector).length;
+	getItemId(item) {
+		return item.dataset.uploadId;
+	}
 
-		// Update bulk controls visibility
-		if (this.ui.bulkControls) {
-			this.ui.bulkControls.hidden = count === 0;
+	/*******************************************************************
+	 * PUBLIC API
+	 *******************************************************************/
+	select(id, updateCheckbox = true, updateUI = true) {
+		if (this.selectedItems.has(id)) return;
+
+		this.selectedItems.add(id);
+		if (updateCheckbox) this.setCheckboxState(id, true);
+		if (updateUI) {
+			this.updateSelectionUI();
 		}
+		this.notify('item-selected', { id, selectedItems: new Set(this.selectedItems) });
+	}
 
-		// Update count display
-		if (this.ui.count) {
-			const itemText = count === 1 ? 'item' : 'items';
-			this.ui.count.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
-			this.ui.count.hidden = count === 0;
+	deselect(id, updateCheckbox = true, updateUI = true) {
+		if (!this.selectedItems.has(id)) return;
+
+		this.selectedItems.delete(id);
+		if (updateCheckbox) this.setCheckboxState(id, false);
+		if (updateUI) {
+			this.updateSelectionUI();
 		}
+		this.notify('item-deselected', { id, selectedItems: new Set(this.selectedItems) });
+	}
 
-		// Update select all checkbox state
-		if (this.ui.selectAll) {
-			this.ui.selectAll.checked = totalItems > 0 && count === totalItems;
-			this.ui.selectAll.indeterminate = count > 0 && count < totalItems;
+	toggle(id) {
+		this.selectedItems.has(id) ? this.deselect(id) : this.select(id);
+		this.updateSelectionUI();
+	}
 
-			// Update label text if available
-			const label = this.ui.selectAll.nextElementSibling ||
-				this.ui.selectAll.previousElementSibling;
-			if (label && label.tagName === 'LABEL') {
-				label.textContent = (totalItems > 0 && count === totalItems)
-					? 'Clear Selection'
-					: 'Select All';
+	clearSelection() {
+		this.selectedItems.forEach(id => this.setCheckboxState(id, false));
+		this.selectedItems.clear();
+		this.lastSelected = null;
+		this.lastSelectedWrapper = null;
+
+		// Uncheck all select-all triggers
+		this.container.querySelectorAll(this.selectors.selectAll).forEach(trigger => {
+			trigger.checked = false;
+			const label = trigger.nextElementSibling || trigger.previousElementSibling;
+			if (label?.tagName === 'LABEL') {
+				label.textContent = 'Select All';
 			}
-		}
+		});
+		this.updateSelectionUI();
+		this.notify('selection-cleared', { selectedItems: new Set() });
 	}
 
-	/**
-	 * Get item ID from element
-	 */
-	getItemId(element) {
-		const item = element.closest(this.itemSelector);
-		if (!item) return null;
-
-		// Try common ID attributes in order
-		return item.dataset.id ||
-			item.dataset.itemId ||
-			item.dataset.uploadId ||
-			item.id;
-	}
-
-	/**
-	 * Check if an item is selected
-	 */
 	isSelected(id) {
 		return this.selectedItems.has(id);
 	}
 
-	/**
-	 * Programmatically select specific items
-	 */
-	select(ids) {
-		const idArray = Array.isArray(ids) ? ids : [ids];
-
-		idArray.forEach(id => {
-			this.selectedItems.add(id);
-
-			// Update checkbox if element exists
-			const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
-			if (item) {
-				const checkbox = item.querySelector(this.checkboxSelector);
-				if (checkbox) checkbox.checked = true;
-			}
-		});
-
-		this.updateSelectionUI();
-
-		this.notify('item-selected', {
-			selectedItem: id,
-			selectedItems: this.selectedItems,
-			container: this.container
-		});
+	getSelection() {
+		return new Set(this.selectedItems);
 	}
 
-	/**
-	 * Programmatically deselect specific items
-	 */
-	deselect(ids) {
-		const idArray = Array.isArray(ids) ? ids : [ids];
-
-		idArray.forEach(id => {
-			this.selectedItems.delete(id);
-
-			// Update checkbox if element exists
-			const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
-			if (item) {
-				const checkbox = item.querySelector(this.checkboxSelector);
-				if (checkbox) checkbox.checked = false;
-			}
-		});
-
-		this.updateSelectionUI();
-
-		this.notify('item-deselected', {
-			selectedItem: id,
-			selectedItems: this.selectedItems,
-			container: this.container
-		});
+	/*******************************************************************
+	 * DOM HELPERS
+	 *******************************************************************/
+	setCheckboxState(id, checked) {
+		const item = this.container.querySelector(`[data-upload-id="${id}"]`);
+		const checkbox = item?.querySelector(this.selectors.checkbox);
+		if (checkbox && checkbox.checked !== checked) {
+			checkbox.checked = checked;
+		}
 	}
-	/**
-	 * Event system
-	 */
+	updateSelectionUI() {
+		if (!this.lastClicked || !this.ui.count) return;
+
+		const count = this.selectedItems.size;
+
+		// Update bulk controls visibility
+		let controls = this.lastClicked.querySelector(this.selectors.bulkControls);
+		if (controls) {
+			controls.hidden = count === 0;
+		}
+
+		// Update count display
+		let countEl = this.lastClicked.querySelector(this.selectors.count);
+		if (countEl) {
+			const itemText = count === 1 ? 'item' : 'items';
+			countEl.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+			countEl.hidden = count === 0;
+		}
+	}
+	/*******************************************************************
+	 * EVENT SYSTEM
+	 *******************************************************************/
 	subscribe(callback) {
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
 	}
 
 	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
+		this.subscribers.forEach(cb => {
+			try {
+				cb(event, data);
+			} catch (e) {
+				console.error('HandleSelection subscriber error:', e);
+			}
+		});
 	}
 
-	/**
-	 * Clean up event listeners
-	 */
 	destroy() {
-		// Remove event listeners
-		if (this.container) {
-			this.container.removeEventListener('click', this.clickHandler);
-			this.container.removeEventListener('change', this.changeHandler);
-			this.container.removeEventListener('keydown', this.keyHandler);
-		}
-
-		// Clear selections
-		this.clearSelection();
-
-		// Clear subscribers
+		this.container.removeEventListener('change', this.changeHandler);
+		this.container.removeEventListener('click', this.clickHandler);
+		this.container.removeEventListener('keydown', this.keyHandler);
 		this.subscribers.clear();
-
-		// Clear references
-		this.container = null;
-		this.ui = null;
-		this.lastSelected = null;
+		this.selectedItems.clear();
 	}
 }
 
-// Export for use in other modules
 window.jvbHandleSelection = HandleSelection;
diff --git a/assets/js/concise/HandleSelectionOld.js b/assets/js/concise/HandleSelectionOld.js
new file mode 100644
index 0000000..e2d619c
--- /dev/null
+++ b/assets/js/concise/HandleSelectionOld.js
@@ -0,0 +1,392 @@
+/**
+ * HandleSelection - Reusable selection management for items in grids
+ *
+ * Handles selection logic including:
+ * - Individual item selection/deselection
+ * - Select all / Clear all
+ * - Range selection (shift+click)
+ * - Selection count updates
+ * - Bulk action visibility
+ *
+ * @class HandleSelection
+ */
+class HandleSelection {
+	/**
+	 * @param {Object} config - Configuration object
+	 * @param {HTMLElement} config.container - Container element holding items
+	 * @param {Object} config.ui - UI element references
+	 * @param {HTMLElement} config.ui.selectAll - Select all checkbox
+	 * @param {HTMLElement} config.ui.bulkControls - Bulk actions container
+	 * @param {HTMLElement} config.ui.count - Selection count display
+	 * @param {string} config.itemSelector - Selector for individual items
+	 * @param {string} config.checkboxSelector - Selector for item checkboxes
+	 * @param {Function} config.onSelectionChange - Optional callback when selection changes
+	 */
+	constructor(config) {
+
+		this.container = config.container;
+		this.ui = config.ui || {};
+		this.itemSelector = config.itemSelector || '.item';
+		this.checkboxSelector = config.checkboxSelector || '[name*="select-item"]';
+
+		this.selectedItems = new Set();
+		this.lastSelected = null;
+		this.subscribers = new Set();
+
+		this.init();
+	}
+
+	init() {
+		// Bind event handlers
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.keyHandler = this.handleKeys.bind(this);
+
+		// Attach listeners
+		this.ui.wrapper.addEventListener('click', this.clickHandler);
+		this.ui.wrapper.addEventListener('change', this.changeHandler);
+		this.ui.wrapper.addEventListener('keydown', this.keyHandler);
+	}
+
+	handleKeys(e) {
+		// Ctrl/Cmd + A: Select all
+		if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
+			e.preventDefault();
+
+			if (this.ui.selectAll) {
+				this.ui.selectAll.checked = true;
+				this.selectAll(true);
+				if (window.jvbA11y) {
+					window.jvbA11y.announce('All items selected');
+				}
+			}
+		}
+
+		// Escape: Deselect all
+		if (e.key === 'Escape' && this.selectedItems.size > 0) {
+			this.selectAll(false);
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Selection cleared');
+			}
+		}
+
+		// Delete/Backspace: Remove selected items
+		if ((e.key === 'Delete' || e.key === 'Backspace') &&
+			!e.target.matches('input, textarea')
+			&& this.selectedItems > 0) {
+			e.preventDefault();
+			if (confirm(`Remove ${this.selectedItems.size} selected item${this.selectedItems.size !== 1 ? 's' : ''}?`)) {
+				this.deselect(this.selectedItems);
+			}
+		}
+	}
+
+	handleClick(e) {
+		const checkbox = e.target.closest(`${this.checkboxSelector}, label[for]`);
+		if (!checkbox) return;
+
+		// Get the actual checkbox element
+		const input = checkbox.tagName === 'LABEL'
+			? document.getElementById(checkbox.getAttribute('for'))
+			: checkbox;
+
+		if (!input) return;
+
+		// Handle shift+click for range selection
+		if (e.shiftKey && this.lastSelected) {
+			e.preventDefault();
+			this.handleRangeSelection(input);
+		} else {
+			// Store last clicked for range selection
+			const item = input.closest(this.itemSelector);
+			if (item) {
+				this.lastSelected = item;
+			}
+		}
+	}
+
+	handleChange(e) {
+		if (this.ui.selectAll && e.target === this.ui.selectAll) {
+			this.selectAll(e.target.checked);
+		} else {
+			const checkbox = e.target.closest(this.checkboxSelector);
+			if (!checkbox) return;
+
+			this.toggleSelection(this.getItemId(checkbox));
+		}
+	}
+
+	/**
+	 * Toggle selection state of an item
+	 */
+	toggleSelection(id) {
+		if (!id) return;
+
+		let selected = true;
+		if (this.selectedItems.has(id)) {
+			selected = false;
+			this.selectedItems.delete(id);
+		} else {
+			this.selectedItems.add(id);
+		}
+
+		if (selected) {
+			this.notify('item-selected', {
+				selectedItem: id,
+				selectedItems: this.selectedItems,
+				container: this.container
+			});
+		} else {
+			this.notify('item-deselected', {
+				selectedItem: id,
+				selectedItems: this.selectedItems,
+				container: this.container
+			});
+		}
+
+
+		this.updateSelectionUI();
+	}
+
+	/**
+	 * Select or deselect all items
+	 */
+	selectAll(checked) {
+		const items = this.container.querySelectorAll(this.itemSelector);
+
+		if (!checked) {
+			this.selectedItems.clear();
+			if (this.ui.selectAll) {
+				this.ui.selectAll.checked = false;
+			}
+		}
+
+		items.forEach(item => {
+			const id = this.getItemId(item);
+			const checkbox = item.querySelector(this.checkboxSelector);
+
+			if (checkbox) {
+				checkbox.checked = checked;
+			}
+
+			if (checked && id) {
+				this.selectedItems.add(id);
+			}
+		});
+
+		this.notify('select-all', {
+			container: this.container,
+			selected: checked,
+			items: items
+		})
+
+		this.updateSelectionUI();
+	}
+
+	/**
+	 * Clear all selections
+	 */
+	clearSelection() {
+		this.selectAll(false);
+	}
+
+	/**
+	 * Handle shift+click range selection
+	 */
+	handleRangeSelection(currentCheckbox) {
+		if (!this.lastSelected) {
+			this.lastSelected = currentCheckbox.closest(this.itemSelector);
+			return;
+		}
+
+		const currentItem = currentCheckbox.closest(this.itemSelector);
+		if (!currentItem) return;
+
+		// Get all items
+		const allItems = Array.from(this.container.querySelectorAll(this.itemSelector));
+
+		// Find indices
+		const lastIndex = allItems.indexOf(this.lastSelected);
+		const currentIndex = allItems.indexOf(currentItem);
+
+		if (lastIndex === -1 || currentIndex === -1) return;
+
+		// Determine range (handle both directions)
+		const startIndex = Math.min(lastIndex, currentIndex);
+		const endIndex = Math.max(lastIndex, currentIndex);
+
+		let isChecked = !currentCheckbox.checked;
+		// Select all items in range
+		for (let i = startIndex; i <= endIndex; i++) {
+			const item = allItems[i];
+			const checkbox = item.querySelector(this.checkboxSelector);
+			const id = this.getItemId(item);
+
+			if (checkbox && id) {
+				checkbox.checked = isChecked;
+				this.selectedItems.add(id);
+			}
+		}
+
+		// Update last selected to current
+		this.lastSelected = currentItem;
+
+		this.updateSelectionUI();
+
+		this.notify('range-selected', {
+			selectedItems: this.selectedItems,
+			container: this.container
+		});
+
+		// Announce for accessibility
+		const selectedCount = endIndex - startIndex + 1;
+		if (window.jvbA11y) {
+			window.jvbA11y.announce(`Selected ${selectedCount} items in range`);
+		}
+	}
+
+	/**
+	 * Update selection UI elements
+	 */
+	updateSelectionUI() {
+		const count = this.selectedItems.size;
+		const totalItems = this.container.querySelectorAll(this.itemSelector).length;
+
+		// Update bulk controls visibility
+		if (this.ui.bulkControls) {
+			this.ui.bulkControls.hidden = count === 0;
+		}
+
+		// Update count display
+		if (this.ui.count) {
+			const itemText = count === 1 ? 'item' : 'items';
+			this.ui.count.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+			this.ui.count.hidden = count === 0;
+		}
+
+		// Update select all checkbox state
+		if (this.ui.selectAll) {
+			this.ui.selectAll.checked = totalItems > 0 && count === totalItems;
+			this.ui.selectAll.indeterminate = count > 0 && count < totalItems;
+
+			// Update label text if available
+			const label = this.ui.selectAll.nextElementSibling ||
+				this.ui.selectAll.previousElementSibling;
+			if (label && label.tagName === 'LABEL') {
+				label.textContent = (totalItems > 0 && count === totalItems)
+					? 'Clear Selection'
+					: 'Select All';
+			}
+		}
+	}
+
+	/**
+	 * Get item ID from element
+	 */
+	getItemId(element) {
+		const item = element.closest(this.itemSelector);
+		if (!item) return null;
+
+		// Try common ID attributes in order
+		return item.dataset.id ||
+			item.dataset.itemId ||
+			item.dataset.uploadId ||
+			item.id;
+	}
+
+	/**
+	 * Check if an item is selected
+	 */
+	isSelected(id) {
+		return this.selectedItems.has(id);
+	}
+
+	/**
+	 * Programmatically select specific items
+	 */
+	select(ids) {
+		const idArray = Array.isArray(ids) ? ids : [ids];
+
+		idArray.forEach(id => {
+			this.selectedItems.add(id);
+
+			// Update checkbox if element exists
+			const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
+			if (item) {
+				const checkbox = item.querySelector(this.checkboxSelector);
+				if (checkbox) checkbox.checked = true;
+			}
+		});
+
+		this.updateSelectionUI();
+
+		this.notify('item-selected', {
+			selectedItem: id,
+			selectedItems: this.selectedItems,
+			container: this.container
+		});
+	}
+
+	/**
+	 * Programmatically deselect specific items
+	 */
+	deselect(ids) {
+		const idArray = Array.isArray(ids) ? ids : [ids];
+
+		idArray.forEach(id => {
+			this.selectedItems.delete(id);
+
+			// Update checkbox if element exists
+			const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
+			if (item) {
+				const checkbox = item.querySelector(this.checkboxSelector);
+				if (checkbox) checkbox.checked = false;
+			}
+		});
+
+		this.updateSelectionUI();
+
+		this.notify('item-deselected', {
+			selectedItem: id,
+			selectedItems: this.selectedItems,
+			container: this.container
+		});
+	}
+	/**
+	 * Event system
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data) {
+		this.subscribers.forEach(cb => cb(event, data));
+	}
+
+	/**
+	 * Clean up event listeners
+	 */
+	destroy() {
+		// Remove event listeners
+		if (this.container) {
+			this.container.removeEventListener('click', this.clickHandler);
+			this.container.removeEventListener('change', this.changeHandler);
+			this.container.removeEventListener('keydown', this.keyHandler);
+		}
+
+		// Clear selections
+		this.clearSelection();
+
+		// Clear subscribers
+		this.subscribers.clear();
+
+		// Clear references
+		this.container = null;
+		this.ui = null;
+		this.lastSelected = null;
+	}
+}
+
+// Export for use in other modules
+window.jvbHandleSelection = HandleSelection;
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index d0fb19b..7e3033c 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -163,6 +163,7 @@
 			method: 'POST',
 			headers: {},
 			data: {},
+			sendNow: false,				// true = process immediately
 			canMerge: true,
 			popup: 'Saving changes...',
 			title: 'Operation',
@@ -183,6 +184,14 @@
 			return null;
 		}
 
+		if (item.sendNow) {
+			this.processOperation(item).then(()=> {});
+			this.store.clearCache();
+			window.debouncer.schedule('fastQueue', this.startPolling.bind(this), 200);
+			this.showQueue();
+			return item.id;
+		}
+
 		const existingOps = Array.from(this.store.data.values()).filter(op=>
 			op.status === 'queued' &&
 			op.endpoint === item.endpoint &&
@@ -201,7 +210,6 @@
 			return existing.id;
 		}
 
-		console.log('Added to Queue: ', item);
 		this.store.clearCache();
 
 		//Add new operation to DataStore
@@ -313,12 +321,14 @@
 		this.maybeStartPolling() ? this.showQueue() : this.hideQueue();
 	}
 
-	async processOperation(operation) {
+	async processOperation(operation, skip = false) {
 		try {
-			this.updateOperationStatus(operation.id, 'uploading');
+			if (!skip) {
+				this.updateOperationStatus(operation.id, 'uploading');
 
-			if (operation.data?._isFormData) {
-				operation.data = await this.store.objectToFormData(operation.data);
+				if (operation.data?._isFormData) {
+					operation.data = await this.store.objectToFormData(operation.data);
+				}
 			}
 
 			const url = `${this.config.apiBase}${operation.endpoint}`;
@@ -336,7 +346,6 @@
 				});
 				operation.headers['Content-Type'] = 'application/json';
 			}
-
 			const response = await fetch(url, {
 				method: operation.method,
 				headers: operation.headers,
@@ -344,7 +353,9 @@
 			});
 
 			const result = await response.json();
-
+			if (skip) {
+				operation.data = {};
+			}
 			if (response.ok && result.success !== false) {
 				// Handle server-side merge
 				if (result.id && operation.id !== result.id) {
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index c8f39f7..b48d38c 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -1,102 +1,1074 @@
-/**
- * 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
-		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
+		this.queue = window.jvbQueue;
 		this.error = window.jvbError;
-		this.fieldStoreReady = false;
-		this.uploadStoreReady = false;
-		this.hasCheckedForUploads = false;
-		const {fields, uploads} = window.jvbStore.register(
+
+		this.subscribers = new Set();
+
+		this.initStores();
+		this.initWorker();
+
+		//Maps for DOM references
+		this.fields = new Map();
+		this.uploads = new Map();
+		this.groups = new Map();
+
+		this.selected = new Map();
+		this.selectionHandlers = new Map();
+		this.sortables = new Map();
+
+		this.previewUrls = new Set();
+		this.initElements();
+		this.initListeners();
+	}
+
+	initStores() {
+		const {uploads, groups} = window.jvbStore.register(
 			'uploads',
 			[
 				{
-					storeName: 'fields',
-					keyPath: 'id',
-					indexes: [
-						{ name: 'fieldId', keyPath: 'fieldId' },
-						{ name: 'timestamp', keyPath: 'timestamp' },
-						{ name: 'content', keyPath: 'content' },
-						{ name: 'itemId', keyPath: 'itemId' },
-						{ name: 'status', keyPath: 'status' }
-					],
-					TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
-					delayFetch: true
-				},
-				{
 					storeName: 'uploads',
 					keyPath: 'id',
-					storeBlobs: true,
 					indexes: [
-						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'field', keyPath: 'field' },
 						{ name: 'status', keyPath: 'status' },
-						{ name: 'groupId', keyPath: 'groupId' },
-						{ name: 'attachmentId', keyPath: 'attachmentId' }
+						{ name: 'group', keyPath: 'group' },
+						{ name: 'src', keyPath: 'src' }
 					],
-					delayFetch: true
+				},
+				{
+					storeName: 'groups',
+					keyPath: 'id',
+					indexes: [
+						{ name: 'field', keyPath: 'field' },
+						{ name: 'src', keyPath: 'src' }
+					]
 				}
 			]
 		);
-		this.fieldStore = fields;
-		this.uploadStore = uploads;
 
-		window.jvbUploadBlobs = this.uploadStore;
+		this.stores = {
+			uploads: uploads,
+			groups: groups,
+			ready: []
+		};
 
-		// Subscribe to store events
-		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
-		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+		this.stores.uploads.subscribe(this.handleStores.bind(this, 'uploads'));
+		this.stores.groups.subscribe(this.handleStores.bind(this, 'groups'));
+		this.queue.subscribe((event, operation) => {
+			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
+				return;
+			}
 
-		// 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 }
 
-		// Selection and UI state
-		this.selected = new Map();
-		this.selectionHandlers = new Map();
-		this.previewUrls = new Set();
-		this.sortableInstances = new Map();
+			const fieldId = operation.data instanceof FormData
+				? operation.data.get('fieldId')
+				: operation.data?.fieldId;
+			if (!fieldId) {
+				return;
+			}
+			switch (event) {
+				case 'cancel-operation':
+					this.handleOperationCancelled(fieldId).then(()=>{});
+					break;
+				case 'operation-status':
+					this.handleFieldStatus(fieldId, operation).then(()=>{});
+					break;
+				case 'operation-completed':
+					this.handleOperationComplete(operation, fieldId).then(()=>{});
+					break;
+				case 'operation-failed':
+				case 'operation-failed-permanent':
+					this.handleOperationFailed(operation, fieldId).then(()=>{});
+					break;
+			}
+		});
+	}
 
-		// Worker for image processing
-		this.initWorker();
+	storesReady() {
+		return this.stores.ready.length === 2;
+	}
 
-		// Notification subscribers
-		this.subscribers = new Set();
+	handleStores(storeName, event) {
+		if (event === 'data-ready') {
+			this.stores.ready.push(storeName);
+			if (this.storesReady()) {
+				this.checkRecovery();
+			}
+		}
+	}
 
-		// Selectors
+	initWorker() {
+		this.worker = null;
+		this.workerState = {
+			worker: null,
+			tasks: new Map(),
+			restart: { count: 0, max: 3 },
+			settings: {
+				timeout: 3000,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
+			}
+		};
+	}
+
+	initElements() {
 		this.selectors = {
-			field: {
+			fields: {
 				field: '[data-upload-field]',
 				input: 'input[type="file"]',
 				dropZone: '.file-upload-container',
-				preview: '.item-grid.preview',
-				progress: '.image-progress'
+				preview: '.preview-wrap',
+				grid: '.item-grid.preview',
+				progress: {
+					progress: '.file-upload-container .progress',
+					fill: '.file-upload-container .progress .fill',
+					details: '.file-upload-container .progress .details',
+					icon: '.file-upload-container .progress .icon'
+				},
+				selectAll: '[name="select-all-uploads"]',
+				actions: '.selection-actions',
+				count: '.selection-count',
+				hidden: 'input[type="hidden"]'
 			},
+			// groups = selectors that affect groups as a whole
 			groups: {
-				container: '.upload-group',
-				grid: '.item-grid.group',
-				header: '.group-header',
+				container: '.group-display',
+				grid: '.item-grid.groups',
+				empty: '.empty-group',
+				header: '.sidebar .header',
+			},
+			// group = selectors that affect individual groups
+			group: {
+				item: '.upload-group',
+				actions: '.selection-actions',
 				selectAll: '[name="select-all-group"]',
-				actions: '.group-actions',
-				count: '.selection-controls .info'
+				count: '.group-header .info',
+				fields: 'details .fields',
+				grid: '.item-grid.group',
+				total: '.group-content .group-count'
 			},
 			items: {
 				item: '[data-upload-id]',
 				checkbox: '[name*="select-item"]',
 				featured: '[name="featured"]',
-				details: 'details'
+				image: 'img',
+				details: 'details',
+				progress: {
+					progress: '.progress',
+					fill: '.fill',
+					details: '.details',
+					icon: '.icon'
+				}
+			}
+		};
+	}
+
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.dragEnterHandler = this.handleDragEnter.bind(this);
+		this.dragLeaveHandler = this.handleDragLeave.bind(this);
+		this.dragOverHandler = this.handleDragOver.bind(this);
+		this.dropHandler = this.handleDrop.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('drop', this.dropHandler);
+
+		window.addEventListener('beforeunload', () => {
+			this.cleanupAllPreviewUrls();
+		});
+	}
+
+	async setUpload(uploadId, data) {
+		const defaults = {
+			id: uploadId,
+			attachment: null,
+			group: null,
+			field: null,
+			src: window.location.href,
+			blob: null,
+			status: 'local_processing',
+			operationId: null,
+			fields: {}
+		};
+
+		const upload = { ...defaults, ...data };
+		Object.preventExtensions(upload);
+		await this.stores.uploads.save(upload);
+		return upload;
+	}
+
+	/*********************************************************************
+	 UTILITY
+	*********************************************************************/
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		this.previewUrls.add(url);
+		return url;
+	}
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls.delete(url);
+		}
+	}
+
+	formatFile(upload) {
+		if (!upload.blob) return null;
+		return new File([upload.blob], upload.fields.originalName || 'file', {
+			type: upload.fields.type || upload.blob.type,
+			lastModified: upload.fields.lastModified || Date.now()
+		});
+	}
+	/*********************************************************************
+	 LISTENERS
+	*********************************************************************/
+	handleClick(e) {
+		//Open the file input if it's a dropzone
+		let dropZone = window.targetCheck(e, this.selectors.fields.dropZone);
+		if (dropZone && !e.target.matches('input, button, a')){
+			dropZone.querySelector(this.selectors.fields.input)?.click();
+		}
+
+		//Handle action buttons
+		const button = window.targetCheck(e, '[data-action]');
+		if (button) this.handleAction(button);
+	}
+		handleAction(button) {
+			const action = button.dataset.action;
+			const fieldId = this.getFieldIdFromElement(button);
+
+			switch (action) {
+				case 'add-to-group':
+					this.handleAddToGroup(fieldId).then(()=>{});
+					break;
+				case 'delete-group':
+					this.handleDeleteGroup(button);
+					break;
+				case 'delete-upload':
+				case 'remove-from-group':
+					this.handleRemoveItem(button).then(()=>{});
+					break;
+				case 'upload':
+					this.queueUploads('uploads/groups',fieldId).then(()=>{});
+					break;
+				case 'restore':
+					this.handleRestoreSelected().then(()=>{});
+					break;
+				case 'restore-all':
+					this.handleRestoreAll().then(()=>{});
+					break;
+				case 'clear-cache':
+					this.handleClearCache().then(()=>{});
+					break;
+			}
+		}
+	handleChange(e) {
+		let fieldId = this.getFieldIdFromElement(e.target);
+		if (!fieldId) return;
+
+		if (e.target.matches(this.selectors.fields.input)) {
+			const files = Array.from(e.target.files);
+			if (files.length > 0) this.processFiles(fieldId, files).then(()=>{});
+			return;
+		}
+
+		// Skip selection-related inputs
+		if (e.target.matches(this.selectors.items.checkbox) ||
+			e.target.matches(this.selectors.items.featured) ||
+			e.target.matches('[name*="select-"]')) {
+			return;
+		}
+
+		let field = this.fields.get(fieldId);
+		if (!field || !field.config.autoUpload) return;
+
+		if (field.config.destination === 'post_group') {
+			this.handleGroupMetaChange(e.target);
+		} else {
+			this.queueUploadMeta(e).then(()=>{});
+		}
+	}
+		handleGroupMetaChange(input) {
+			const element = input.closest(this.selectors.group.fields);
+			if (!element) return;
+
+			const groupId = element.dataset.groupId;
+			const group = this.stores.groups.get(groupId);  // Changed from this.groups
+			if (!group) return;
+
+			window.debouncer.schedule(`group-meta-${groupId}`, async (input, groupId) => {
+				let name = input.name
+					.replace(`${groupId}_`, '')
+					.replace(`${groupId}[`, '')
+					.replace(']', '');
+				group.fields[name] = input.value;
+				await this.setGroup(groupId, group);
+			}, 300);
+		}
+	handleDragEnter(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
+	}
+	handleDragLeave(e) {
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (dropZone && !dropZone.contains(e.relatedTarget)) {
+			dropZone.classList.remove('dragover');
+		}
+	}
+	handleDragOver(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'copy';
+		}
+	}
+	handleDrop(e) {
+		const dropZone = e.target.closest(this.selectors.fields.dropZone);
+		if (!dropZone) return;
+
+		e.preventDefault();
+		dropZone.classList.remove('dragover');
+		dropZone.classList.add('uploading');
+
+
+		const files = Array.from(e.dataTransfer.files);
+		if (files.length === 0) return;
+
+		const fieldId = this.getFieldIdFromElement(dropZone);
+		if (fieldId) {
+			this.processFiles(fieldId, files).then(()=>{});
+			this.a11y.announce(`${files.length} file(s) dropped for upload`);
+		}
+	}
+
+	async queueUploads(endpoint, fieldId) {
+		let data = new FormData();
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		if (uploads.length === 0) return;
+
+		const [ isUpload, isGroups] =
+			[ endpoint === 'uploads', endpoint === 'uploads/groups'];
+
+		data.append('fieldId', field.id);
+		data.append('content', field.config.content);
+
+		if (isUpload) {
+			data.append('mode', field.config.mode);
+			data.append('field_name', field.config.name);
+			data.append('fieldId', field.id);
+			data.append('field_type', field.config.type);
+			data.append('subtype', field.config.subtype);
+			data.append('item_id', field.config.itemID);
+			data.append('destination', field.config.destination);
+		}
+
+		let posts, uploadMap, files;
+		if (isGroups) {
+			({posts, uploadMap, files} = this.collectGroups(fieldId));
+		} else if (isUpload) {
+			({uploadMap, files} = this.collectUploads(fieldId));
+		}
+
+		if (isGroups) {
+			data.append('posts', JSON.stringify(posts));
+		}
+		files.forEach(file => {
+			data.append('files[]', file);
+		});
+		data.append('upload_ids', JSON.stringify(uploadMap));
+
+		let title, popup;
+		if (isUpload) {
+			title = `Uploading ${uploads.length} file${uploads.length>1?'s':''} to server...`;
+			popup = `Uploading ${uploads.length} file${uploads.length>1?'s':''}...`;
+		} else if (isGroups) {
+			title = `Creating ${posts.length} ${field.config.content}${posts.length > 1 ? 's' : ''} from uploads...`;
+			popup = `Creating ${posts.length} post${posts.length>1?'s':''}...`;
+		}
+		await this.setBulkUpload(uploads, 'status', 'queued');
+		let operationId = this.sendToQueue(endpoint, data, title, popup);
+
+		if (endpoint === 'uploads/groups') {
+			let details = field.element.closest('details');
+			if (details) {
+				details.open = false;
+			}
+		}
+		if (operationId) {
+			field.operationId = operationId;
+			await this.setBulkUpload(uploads, 'operationId', operationId);
+			await this.setBulkUpload(uploads, 'status', 'uploading');
+			await this.setBulkGroup(fieldId, 'operationId', operationId);
+			this.fields.set(field.id, field);
+		} else {
+			await this.setBulkUpload(uploads, 'status', 'failed');
+		}
+		this.notify('sent-to-queue', fieldId);
+		return operationId;
+	}
+
+	async sendToQueue(endpoint, data, title = '', popup = '', mergable = false) {
+		if (popup === '') {
+			popup = title;
+		}
+		const operation = {
+			endpoint: endpoint,
+			method: 'POST',
+			data: data,
+			title: title,
+			popup: popup,
+			canMerge: mergable,
+			sendNow: endpoint === 'uploads/groups',
+			headers: {
+				'action_nonce': window.auth.getNonce('dash')
+			},
+			append: '_upload'
+		}
+		try {
+			return await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sentToQueue'
+			});
+			return false;
+		}
+	}
+
+	collectGroups(fieldId) {
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		let groups = this.stores.groups.filterByIndex({field: fieldId});
+
+		let posts = [];
+		let uploadMap = [];
+		let files = [];
+
+		for (const group of groups) {
+			const post = {
+				images: [],
+				fields: group.fields??{}
+			};
+
+			const groupUploads = uploads.filter(u => u.group === group.id);
+			for (const upload of groupUploads) {
+				const file = this.formatFile(upload);
+				if (file) {
+					files.push(file);
+					const imageData = {
+						upload_id: upload.id,
+						index: uploadMap.length
+					};
+					let uploadEl = this.uploads.get(upload.id);
+					if (uploadEl.ui?.featured?.checked) {
+						post.fields.featured = upload.id;
+					}
+					post.images.push(imageData);
+					uploadMap.push(upload.id);
+				}
+			}
+			posts.push(post);
+		}
+
+		const remaining = uploads.filter(u => !u.group);
+
+		for (const upload of remaining) {
+			const post = {
+				images: [],
+				fields: {}
+			};
+
+			const file = this.formatFile(upload);
+			if (file) {
+				files.push(file);
+
+				const imageData = {
+					upload_id: upload.id,
+					index: uploadMap.length
+				};
+				post.images.push(imageData);
+				uploadMap.push(upload.id);
+			}
+			posts.push(post);
+		}
+		return {posts, uploadMap, files};
+	}
+
+	collectUploads(fieldId) {
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		if (uploads.length === 0) return;
+
+		let uploadMap = [];
+		let files = [];
+
+		for (const upload of uploads) {
+			const file = this.formatFile(upload);
+			if (file) {
+				files.push(file);
+				uploadMap.push(upload.id);
+			}
+		}
+		return { uploadMap, files };
+	}
+
+	async queueUploadMeta(e) {
+		const uploadId = e.target.closest(this.selectors.items.item)?.dataset.uploadId;
+		const upload = this.stores.uploads.get(uploadId);
+		if (!uploadId || !upload) return;
+
+		const field = this.fields.get(upload.field);
+		if (!field) return;
+
+		let data = {};
+		data[e.target.name] = e.target.value;
+
+		upload.fields = { ...upload.fields, ...data };
+		await this.setUpload(upload.id, upload);
+
+		let queueData = {};
+		queueData[upload.attachmentId ?? upload.id] = upload.fields;
+		return await this.sendToQueue('uploads/meta', queueData, 'Uploading Meta', '', true);
+	}
+
+	async handleOperationComplete(operation, fieldId) {
+		const response = operation.response;
+
+		// Handle direct upload results (from uploads endpoint)
+		if (response?.data) {
+			const results = Array.isArray(response.data) ? response.data : Object.values(response.data);
+			for (const result of results) {
+				if (result.upload_id && result.attachment_id) {
+					const upload = this.stores.uploads.get(result.upload_id);
+					if (upload) {
+						upload.attachmentId = result.attachment_id;
+						upload.status = 'completed';
+						await this.stores.uploads.save(upload);
+					}
+				}
+			}
+		}
+
+		// Clear completed uploads and groups
+		const uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		const groups = this.stores.groups.filterByIndex({field: fieldId});
+
+		await Promise.all([
+			...uploads
+				.filter(upload => upload.status === 'completed')
+				.map(upload => this.clearUpload(upload.id)),
+			...groups.map(group => this.stores.groups.delete(group.id))
+		]);
+
+		this.notify('uploads-complete', { fieldId, response });
+	}
+	/*********************************************************************
+	 FIELD LOGIC
+	*********************************************************************/
+	scanFields(container, autoUpload = true) {
+		const fields = container.querySelectorAll(this.selectors.fields.field);
+		fields.forEach(uploader => this.registerField(uploader, autoUpload));
+	}
+
+	registerField(element, autoUpload = true, id = null) {
+		const data = {
+			element: element,
+			id: (id) ? id : this.determineFieldId(element),
+			config: this.extractFieldConfig(element, autoUpload),
+			uploads: new Set(),
+			operationId: null,
+			groups: [],
+			ui: window.uiFromSelectors(this.selectors.fields, element),
+			groupUI: window.uiFromSelectors(this.selectors.groups, element)
+		};
+
+
+		this.fields.set(data.id, data);
+
+		element.dataset.uploader = data.id;
+		this.getSelectionHandler(data.id);
+		if (data.config.type !== 'single') {
+			this.initSortable(data.id);
+		}
+
+		return data.id;
+	}
+
+	extractFieldConfig(fieldElement, autoUpload) {
+		return {
+			autoUpload: autoUpload,
+			destination: fieldElement.dataset.destination || 'meta', //TODO: why do we need this?
+			content: this.extractFieldContent(fieldElement),
+			mode: fieldElement.dataset.mode || 'direct',
+			type: fieldElement.dataset.type || 'single',
+			name: fieldElement.dataset.field,
+			itemID: this.extractFieldItemId(fieldElement)??0,
+			maxFiles: parseInt(fieldElement.dataset.maxFiles)??25,
+			subType: fieldElement.dataset.subtype?? 'image'
+		};
+	}
+
+	extractFieldContent(fieldElement) {
+		return fieldElement.dataset.content ||
+			fieldElement.closest('dialog')?.dataset.content ||
+			fieldElement.closest('form')?.dataset.save || null;
+	}
+	extractFieldItemId(fieldElement) {
+		return fieldElement.dataset.itemId ||
+		fieldElement.closest('dialog')?.dataset.itemId || null;
+	}
+
+	determineFieldId(fieldElement) {
+		let content = this.extractFieldContent(fieldElement);
+		content = (content === null) ? '' : content+'_';
+
+		let itemID = this.extractFieldItemId(fieldElement);
+		itemID = (itemID === null) ? '' : itemID+'_';
+
+		const field = fieldElement.dataset.field || '';
+
+		return `${content}${itemID}${field}`;
+	}
+
+	getFieldIdFromElement(el) {
+		const field = el.closest(this.selectors.fields.field);
+		return field?.dataset.uploader || null;
+	}
+
+	updateFieldProgress(fieldId, current, total, message) {
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+		window.showProgress(field.ui.progress,current, total, message);
+	}
+	/*********************************************************************
+	 IMAGE PROCESSING FILE PROCESSING
+	*********************************************************************/
+	getWorker() {
+		if (!this.workerState.worker && typeof OffscreenCanvas !== 'undefined') {
+			this.workerState.worker = new Worker('worker.js');
+			this.workerState.worker.onmessage = (e) => this.handleWorkerMessage(e);
+			this.workerState.worker.onerror = (e) => this.handleWorkerError(e);
+		}
+		return this.workerState.worker;
+	}
+
+	handleWorkerMessage(e) {
+		const { id, blob } = e.data;
+		const task = this.workerState.tasks.get(id);
+		if (task) {
+			clearTimeout(task.timeoutId);
+			task.resolve(blob);
+			this.workerState.tasks.delete(id);
+		}
+	}
+
+	handleWorkerError(e) {
+		// Reject all pending tasks
+		this.workerState.tasks.forEach(task => {
+			clearTimeout(task.timeoutId);
+			task.reject(e);
+		});
+		this.workerState.tasks.clear();
+		this.restartWorker();
+	}
+
+	restartWorker() {
+		if (this.workerState.worker) {
+			this.workerState.worker.terminate();
+			this.workerState.worker = null;
+		}
+		this.workerState.restart.count++;
+	}
+	async processImages(files, maxWidth = 2200, maxHeight = 2200){
+		const results = [];
+		const queue = [...files];
+		const concurrency = this.workerState.settings.maxConcurrent;
+
+		const processNext = async () => {
+			while (queue.length > 0) {
+				const file = queue.shift();
+				results.push(await this.processImage(file, maxWidth, maxHeight));
 			}
 		};
 
-		this.statusMapping = {
+		await Promise.all(
+			Array.from({length: Math.min(concurrency, files.length)}, () => processNext())
+		);
+
+		return results;
+	}
+	async processImage(file, maxWidth = 2200, maxHeight = 2200, timeout = 3000){
+		if (typeof OffscreenCanvas=== 'undefined') {
+			return this.resizeImage(file,maxWidth,maxHeight);
+		}
+		try {
+			return await this.withTimeout(
+				this.workerImage(file, maxWidth, maxHeight),
+				timeout
+			);
+		} catch (e) {
+			return this.resizeImage(file, maxWidth, maxHeight);
+		}
+	}
+	withTimeout(promise, ms) {
+		return Promise.race([
+			promise,
+			new Promise((_, reject) =>
+				setTimeout(() => reject(new Error('Timeout')), ms)
+			)
+		]);
+	}
+
+
+	async workerImage(file, maxWidth = 2200, maxHeight = 2200) {
+		const { settings, restart } = this.workerState;
+
+		if (restart.count >= restart.max) {
+			throw new Error('Worker max restarts exceeded');
+		}
+
+		const bitmap = await createImageBitmap(file);
+
+		let { width, height } = bitmap;
+		if (width > maxWidth || height > maxHeight) {
+			const ratio = Math.min(maxWidth / width, maxHeight / height);
+			width = Math.round(width * ratio);
+			height = Math.round(height * ratio);
+		}
+
+		const worker = this.getWorker();
+		const id = crypto.randomUUID();
+
+		return new Promise((resolve, reject) => {
+			const timeoutId = setTimeout(() => {
+				this.workerState.tasks.delete(id);
+				if (settings.restartAfterTimeout) {
+					this.restartWorker();
+				}
+				reject(new Error('Timeout'));
+			}, settings.timeout);
+
+			this.workerState.tasks.set(id, { resolve, reject, timeoutId });
+
+			worker.postMessage(
+				{ id, imageBitmap: bitmap, width, height, type: file.type, quality: 0.9 },
+				[bitmap]
+			);
+		});
+	}
+	resizeImage(file, maxWidth, maxHeight) {
+		return new Promise((resolve) => {
+			const img = new Image();
+			img.onload = () => {
+				URL.revokeObjectURL(img.src);
+				// Calculate new dimensions keeping aspect ratio
+				let { width, height } = img;
+
+				if (width > maxWidth || height > maxHeight) {
+					const ratio = Math.min(maxWidth / width, maxHeight / height);
+					width = Math.round(width * ratio);
+					height = Math.round(height * ratio);
+				}
+
+				// Draw to canvas at new size
+				const canvas = document.createElement('canvas');
+				canvas.width = width;
+				canvas.height = height;
+				canvas.getContext('2d').drawImage(img, 0, 0, width, height);
+
+				// Export as blob for upload
+				canvas.toBlob(resolve, file.type, 0.9);
+			};
+			img.src = URL.createObjectURL(file);
+		});
+	}
+
+	async processFiles(fieldId, files) {
+		let field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (field.groupUI.container) {
+			field.groupUI.container.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processed = 0;
+
+		this.updateFieldProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		// Create upload records for all files first
+		const uploadEntries = await Promise.all(
+			files.map(async (file) => {
+				const uploadId = window.generateID('upload');
+				const upload = await this.setUpload(uploadId, {
+					id: uploadId,
+					field: fieldId,
+					status: 'local_processing',
+					blob: null,
+					fields: {
+						originalName: file.name,
+						originalSize: file.size,
+						type: file.type,
+						lastModified: file.lastModified
+					}
+				});
+
+				const element = await this.createUpload(uploadId, file, fieldId);
+				this.uploads.set(uploadId, {
+					element: element,
+					ui: window.uiFromSelectors(this.selectors.items, element)
+				});
+
+				await this.addToGroup(uploadId, null);
+
+				return { uploadId, upload, file };
+			})
+		);
+
+		// Batch process images with concurrency control
+		const imageEntries = uploadEntries.filter(e => e.file.type.startsWith('image/'));
+		const otherEntries = uploadEntries.filter(e => !e.file.type.startsWith('image/'));
+
+		// Process images in batches
+		const processedBlobs = await this.processImages(
+			imageEntries.map(e => e.file)
+		);
+
+		// Update image uploads with processed blobs
+		for (let i = 0; i < imageEntries.length; i++) {
+			const { uploadId, upload } = imageEntries[i];
+			upload.blob = processedBlobs[i];
+			upload.fields.size = processedBlobs[i].size;
+			upload.status = 'queued';
+			await this.setUpload(uploadId, upload);
+			processed++;
+			this.updateFieldProgress(fieldId, processed, totalFiles, 'Processing files...');
+		}
+
+		// Handle non-image files (no processing needed)
+		for (const { uploadId, upload, file } of otherEntries) {
+			upload.blob = file;
+			upload.status = 'queued';
+			await this.setUpload(uploadId, upload);
+			processed++;
+			this.updateFieldProgress(fieldId, processed, totalFiles, 'Processing files...');
+		}
+
+		this.maybeLockUploads(fieldId);
+		if (field.config.autoUpload && field.config.destination !== 'post_group') {
+			await this.queueUploads('uploads', fieldId);
+		}
+	}
+	/*************************************************************
+	 RECOVERY
+	*************************************************************/
+	async checkRecovery() {
+		const pendingUploads = this.stores.uploads.filterByIndex({status: ['local_processing', 'queued', 'uploading']});
+		if (pendingUploads.length === 0) return;
+
+		let notification = window.getTemplate('restoreNotification');
+		if (!notification) {
+			this.error.log(
+				'No restore notification',
+				{
+					component: 'UploadManager',
+					src: window.location.href
+				}
+			);
+			return;
+		}
+		// Group by source page
+		const bySource = new Map();
+		pendingUploads.forEach(upload => {
+			const src = upload.src || 'unknown';
+			if (!bySource.has(src)) bySource.set(src, []);
+			bySource.get(src).push(upload);
+		});
+
+		const currentSrc = window.location.href;
+
+
+		let source = bySource.size > 1 ? ` across ${bySource.size} pages` : '';
+		let upload = pendingUploads.length > 1 ? 'uploads' : 'upload';
+		let message = `${pendingUploads.length} ${upload} can be recovered${source}`;
+
+		let details = notification.querySelector('.details');
+		if (details) {
+			details.textContent = message;
+		}
+
+		let i = 1;
+		for (const [src, uploads] of bySource) {
+			let template = window.getTemplate('restoreField');
+			if (!template) continue;
+			let fieldId = this.registerField(template,false, 'recovery_'+i);
+			let field = this.fields.get(fieldId);
+			i++;
+			let isCurrent = src === currentSrc;
+			let [
+				h3,
+				a,
+				grid
+			] = [
+				template.querySelector('h3'),
+				template.querySelector('h3 a'),
+				template.querySelector('.item-grid')
+			];
+
+			template.open = isCurrent;
+			if (!isCurrent) {
+				[a.href, a.title,a.textContent] =
+					[src, 'Navigate to Page and Restore', src];
+			} else {
+				a.remove();
+				h3.textContent = 'From this page:';
+			}
+
+			let filteredGroupIds = [...new Set(uploads.map(upload => upload.group??'preview'))];
+
+			for (let groupId of filteredGroupIds) {
+				let group = (groupId === 'preview') ? true : this.stores.groups.get(groupId);
+				if (!group) continue;
+
+				let groupElement = await this.createGroupElement(groupId,field.id);
+				let groupGrid = groupElement.querySelector('.item-grid');
+				let theseUploads = uploads.filter(upload => upload.group === (groupId === 'preview') ? null : groupId);
+				for (const [key, value] of Object.entries(group.fields ?? {})) {
+					let field = groupElement.querySelector(`input[name*="${key}"]`);
+					if (field) field.value = value;
+				}
+				for (let upload of theseUploads) {
+					let item = await this.createUpload(upload.id, this.formatFile(upload), field.id);
+					groupGrid.append(item);
+				}
+
+				grid.append(groupElement);
+			}
+			notification.querySelector('.wrap').append(template);
+		}
+		document.body.append(notification);
+		notification = document.querySelector('dialog.restore-uploads');
+		this.restoreModal = new window.jvbModal(notification);
+		this.restoreSelection = new window.jvbHandleSelection({
+			container: notification,
+			wrapper: '.restore-uploads .wrap',
+			bulkControls: '.selection-actions',
+			selectAll: '#select-all-restore',
+			count: '.selection-count'
+		});
+		this.restoreModal.handleOpen();
+	}
+
+	async handleRestoreSelected() {
+		if (!this.restoreSelection) return;
+
+		let selected = Array.from(this.restoreSelection.selectedItems);
+		if (selected.length === 0) {
+			return;
+		}
+
+		await this.restoreSelectedUploads(selected);
+	}
+	async handleRestoreAll() {
+		if (!this.restoreModal) return;
+		const allUploads = Array.from(this.restoreModal.modal.querySelectorAll('.item.upload')).map(item => item.dataset.uploadId);
+
+		await this.restoreSelectedUploads(allUploads);
+	}
+
+	async restoreSelectedUploads(selectedUploads) {
+		let currentPage = window.location.href;
+
+		let uploads = Array.from(this.stores.uploads.data.values()).filter(
+			upload => selectedUploads.includes(upload.id) && upload.src === currentPage
+		);
+
+		let groups = [... new Set(uploads.map(upload => upload.group))].filter(Boolean);
+
+		let fieldId = uploads[0].field;
+		let field = document.querySelector(`[data-uploader="${fieldId}"]`);
+		if (!field) {
+			console.log('No field found for '+fieldId);
+			return;
+		}
+		let fieldData = this.fields.get(fieldId);
+		if (fieldData.groupUI.container) {
+			fieldData.groupUI.container.hidden = false;
+		}
+
+		let usedIds = [];
+		for (let gr of groups) {
+
+			let group = this.stores.groups.get(gr);
+			 await this.createGroup(fieldId,gr);
+			 let element = this.groups.get(gr);
+
+			let theseUploads = uploads.filter(upload => upload.group === gr);
+			if (group && this.groups.has(gr)) {
+				let fields = group.fields;
+
+				for (const [key, value] of Object.entries(fields)) {
+					let fi = element.element.querySelector(`input[name*="${key}"]`);
+					if (fi) {
+						fi.value = value;
+					}
+				}
+			}else {
+				//Couldn't restore the group for some reason, just add it to the main preview grid instead
+				gr = null;
+			}
+
+			for (let upload of theseUploads) {
+				let item = await this.createUpload(upload.id, this.formatFile(upload), fieldId);
+				this.uploads.set(upload.id, {
+					element: item,
+					ui: window.uiFromSelectors(this.selectors.items, item)
+				});
+				await this.addToGroup(upload.id, gr);
+				usedIds.push(upload.id);
+			}
+
+		}
+
+		let remaining = uploads.filter(upload => !usedIds.includes(upload.id));
+		for (let upload of remaining) {
+			let item = await this.createUpload(upload.id, this.formatFile(upload), fieldId);
+			this.uploads.set(upload.id, {
+				element: item,
+				ui: window.uiFromSelectors(this.selectors.items, item)
+			});
+			await this.addToGroup(upload.id, null);
+		}
+
+		this.cleanupRestore();
+	}
+
+	cleanupRestore() {
+		this.restoreModal.handleClose();
+		this.restoreSelection.destroy();
+		this.restoreSelection = null;
+		this.restoreModal.destroy();
+		this.restoreModal.modal.remove();
+		this.restoreModal = null;
+	}
+	/*******************************************************************************
+	 STATUS MANAGEMENT
+	*******************************************************************************/
+	getStatusText(status) {
+		let map = {
 			'received': 'Image Received',
 			'local_processing': 'Processing Image...',
 			'queued': 'Waiting to upload...',
@@ -108,2510 +1080,10 @@
 			'failed_permanent': 'Upload failed permanently'
 		};
 
-		this.init();
-	}
-
-	async init() {
-		// this.initializeFields();
-		this.initListeners();
-
-		// Queue integration - handle completion/failure
-		this.queue.subscribe((event, operation) => {
-			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
-				return;
-			}
-
-			const fieldId = operation.data instanceof FormData
-				? operation.data.get('fieldId')
-				: operation.data?.fieldId;
-
-			switch(event) {
-				case 'cancel-operation':
-					if (fieldId) this.handleOperationCancelled(fieldId);
-					break;
-				case 'operation-status':
-					if (fieldId) this.updateFieldStatus(fieldId, operation.status);
-					break;
-				case 'operation-complete':
-					this.handleOperationComplete(operation, fieldId);
-					break;
-				case 'operation-failed':
-				case 'operation-failed-permanent':
-					this.handleOperationFailed(operation, fieldId);
-					break;
-			}
-		});
-
-		window.addEventListener('beforeunload', () => {
-			this.cleanupAllPreviewUrls();
-		});
-	}
-
-	initWorker() {
-		this.worker = {
-			worker: null,
-			timeout: null,
-			tasks: new Map(),
-			restart: { count: 0, max: 3 },
-			settings: {
-				timeout: 10000,
-				batchSize: 1,
-				maxConcurrent: 3,
-				restartAfterTimeout: true
-			}
-		};
-	}
-
-	/*******************************************************************************
-	 * FIELD MANAGEMENT
-	 *******************************************************************************/
-	scanFields(container, autoUpload) {
-		console.log(autoUpload, 'autoUpload');
-		const fields = container.querySelectorAll(this.selectors.field.field);
-		fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
-	}
-
-	registerUploader(uploader, autoUpload) {
-		const fieldId = this.determineFieldId(uploader);
-		const config = this.extractFieldConfig(uploader, autoUpload);
-		const ui = this.buildFieldUI(uploader);
-
-		console.log(config, 'registering with config');
-		// Store field data with Sets for runtime
-		const fieldData = {
-			id: fieldId,
-			config: config,
-			uploads: new Set(),
-			groups: [],
-			state: 'ready',
-			timestamp: Date.now()
-		};
-
-		// 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.type !== 'single') {
-			this.initSortable(fieldId);
-		}
-
-		return fieldId;
-	}
-
-	extractFieldConfig(fieldElement, autoUpload) {
-		return {
-			autoUpload: autoUpload,
-			destination: fieldElement.dataset.destination || 'meta',
-			content: fieldElement.dataset.content || null,
-			mode: fieldElement.dataset.mode || 'direct',
-			type: fieldElement.dataset.type || 'single',
-			name: fieldElement.dataset.field,
-			itemID: fieldElement.dataset.itemId || 0,
-			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
-			subtype: fieldElement.dataset.subtype || 'image'
-		};
-	}
-
-	buildFieldUI(fieldElement) {
-		let UI = {
-			field: fieldElement,
-			input: fieldElement.querySelector(this.selectors.field.input),
-			dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
-			preview: fieldElement.querySelector(this.selectors.field.preview),
-			progress: {
-				progress: fieldElement.querySelector(this.selectors.field.progress),
-				bar: fieldElement.querySelector('.bar'),
-				fill: fieldElement.querySelector('.fill'),
-				details: fieldElement.querySelector('.details'),
-				text: fieldElement.querySelector('.details .text'),
-				count: fieldElement.querySelector('.details .count')
-			}
-		};
-
-		let display = fieldElement.querySelector('.group-display');
-		if (display) {
-			UI.groups = {
-				display: display,
-				container: fieldElement.querySelector('.item-grid.groups'),
-				empty: fieldElement.querySelector('.empty-group'),
-				groups: new Map()
-			};
-		}
-
-		return UI;
-	}
-
-	/*******************************************************************************
-	 * SORTABLE INITIALIZATION
-	 *******************************************************************************/
-	initSortable(fieldId) {
-		if (!window.Sortable) return;
-
-		// Mount MultiDrag plugin once
-		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
-			Sortable.mount(new Sortable.MultiDrag());
-			Sortable._multiDragMounted = true;
-		}
-
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldEl) return;
-
-		// Initialize sortable on all existing grids
-		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
-		grids.forEach(grid => {
-			const groupId = grid.classList.contains('group')
-				? grid.closest('.upload-group')?.dataset.groupId
-				: null;
-			this.createSortableForGrid(grid, fieldId, groupId);
-		});
-
-		// Special handler for empty-group
-		const emptyGroup = fieldEl.element.querySelector('.empty-group');
-		if (emptyGroup && !emptyGroup.sortableInstance) {
-			emptyGroup.sortableInstance = new Sortable(emptyGroup, {
-				animation: 150,
-				draggable: '.item',
-				multiDrag: true,
-				selectedClass: 'selected-for-drag',
-				avoidImplicitDeselect: true,
-				group: { name: fieldId, pull: false, put: true },
-				ghostClass: 'sortable-ghost',
-				chosenClass: 'sortable-chosen',
-				dragClass: 'sortable-drag',
-				onEnd: (evt) => this.handleDrop(evt, fieldId)
-			});
-		}
-	}
-
-	syncSortableSelection(fieldId, selectedItems) {
-		// Update Sortable's selection state to match checkboxes
-		this.sortableInstances.forEach((instance, key) => {
-			if (key.startsWith(fieldId)) {
-				const grid = instance.el;
-				const items = grid.querySelectorAll('.item');
-
-				items.forEach(item => {
-					const uploadId = item.dataset.uploadId;
-					const shouldBeSelected = selectedItems.has(uploadId);
-
-					if (shouldBeSelected) {
-						Sortable.utils.select(item);
-					} else {
-						Sortable.utils.deselect(item);
-					}
-				});
-			}
-		});
-	}
-
-	handleDrop(evt, fieldId) {
-		const dropTarget = evt.to;
-		const sourceTarget = evt.from;
-		const items = evt.items?.length > 0 ? evt.items : [evt.item];
-		const uploadIds = items.map(item => item.dataset.uploadId);
-
-		// Determine drop target type
-		const targetType = this.getDropTargetType(dropTarget);
-
-		switch (targetType) {
-			case 'empty-group':
-				this.handleDropToEmptyGroup(items, uploadIds, fieldId);
-				break;
-
-			case 'preview':
-				this.handleDropToPreview(items, uploadIds, fieldId);
-				break;
-
-			case 'group':
-				this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
-				break;
-			default:
-				// Fallback: return to preview
-				this.handleDropToPreview(items, uploadIds, fieldId);
-				break;
-		}
-
-		// Update UI
-		this.updateSortableState(dropTarget);
-		if (sourceTarget !== dropTarget) {
-			this.updateSortableState(sourceTarget);
-		}
-	}
-
-	/**
-	 * Determine what type of drop target this is
-	 */
-	getDropTargetType(target) {
-		if (target.classList.contains('empty-group')) {
-			return 'empty-group';
-		}
-
-		if (target.classList.contains('preview')) {
-			return 'preview';
-		}
-
-		if (target.classList.contains('group')) {
-			return 'group';
-		}
-
-		return 'unknown';
-	}
-
-	/**
-	 * Handle drop to group: add to existing group
-	 */
-	handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
-		try {
-			// If same container, it's just a reorder
-			if (dropTarget === sourceTarget) {
-				this.handleReorder({ to: dropTarget, items: items });
-				return;
-			}
-
-			// Moving to different group
-			uploadIds.forEach(uploadId => {
-				this.addToGroup(uploadId, dropTarget, false);
-			});
-
-			this.schedulePersistance(fieldId);
-
-			const message = items.length > 1
-				? `Moved ${items.length} items to group`
-				: 'Moved item to group';
-			this.a11y.announce(message);
-
-			// Clear selection
-			const handler = this.selectionHandlers.get(fieldId);
-			handler?.clearSelection();
-		}  catch (error) {
-			this.handleDropError(items, fieldId, error);
-		}
-	}
-
-	/**
-	 * Handle drop to preview: remove from groups
-	 */
-	handleDropToPreview(items, uploadIds, fieldId) {
-		try {
-			uploadIds.forEach(uploadId => {
-				this.removeFromGroup(uploadId);
-			});
-
-			this.schedulePersistance(fieldId);
-
-			const message = items.length > 1
-				? `Moved ${items.length} items to preview`
-				: 'Moved item to preview';
-			this.a11y.announce(message);
-
-			// Clear selection
-			const handler = this.selectionHandlers.get(fieldId);
-			handler?.clearSelection();
-		} catch (error) {
-			this.handleDropError(items, fieldId, error);
-		}
-	}
-
-	/**
-	 * Handle drop errors consistently
-	 */
-	handleDropError(items, fieldId, error, message = 'An error occurred') {
-		console.error('Drop error:', error);
-
-		// Return items to preview as fallback
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (fieldEl?.ui?.preview) {
-			items.forEach(item => fieldEl.ui.preview.appendChild(item));
-		}
-
-		this.a11y.announce(`${message}. Items returned to preview.`);
-	}
-
-	/**
-	 * Handle drop to group: add to existing group
-	 */
-	handleDropToEmptyGroup(items, uploadIds, fieldId) {
-		try {
-			const group = this.createGroup(fieldId);
-			if (!group) {
-				this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
-				return;
-			}
-
-			// Move items to new group
-			items.forEach((item, index) => {
-				group.grid.appendChild(item);
-				this.addToGroup(uploadIds[index], group.grid, false);
-			});
-
-			this.schedulePersistance(fieldId);
-
-			const message = items.length > 1
-				? `Created group with ${items.length} items`
-				: 'Created group with item';
-			this.a11y.announce(message);
-
-			// Clear selection after move
-			const handler = this.selectionHandlers.get(fieldId);
-			handler?.clearSelection();
-		} catch (error) {
-			this.handleDropError(items, fieldId, error);
-		}
-	}
-
-	/**
-	 * 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', false);
-	}
-
-	/**
-	 * 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;
-
-		// Get current order from DOM
-		let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
-			.map(upload => upload.dataset.uploadId)
-			.filter(id => id);
-
-
-		// Update hidden input (for form submission)
-		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
-		if (hiddenInput && items.length > 0) {
-			hiddenInput.value = items.join(',');
-		}
-
-		// Update fieldState with new order
-		const fieldId = this.getFieldIdFromElement(grid);
-		if (fieldId) {
-			const fieldData = this.getFieldData(fieldId);
-
-			// If reordering within a group, update that group's uploads array
-			if (grid.classList.contains('group')) {
-				const groupId = grid.dataset.groupId;
-				const group = fieldData?.groups?.find(g => g.id === groupId);
-				if (group) {
-					group.uploads = items; // Update order
-				}
-			}
-			// If reordering in preview, the order is implicit by DOM position
-			// (we don't store preview order separately)
-
-			this.schedulePersistance(fieldId);
-		}
-
-		this.a11y.announce('Item reordered');
-
-		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
-			detail: {
-				from: evt.from,
-				to: evt.to,
-				oldIndex: evt.oldIndex,
-				newIndex: evt.newIndex,
-				items: items
-			},
-			bubbles: true
-		}));
-	}
-
-	/*******************************************************************************
-	 * 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;
-		const dropZone = e.target.closest(this.selectors.field.dropZone);
-		if (dropZone) {
-			e.preventDefault();
-			dropZone.classList.add('dragover');
-		}
-	}
-
-	handleExternalDragOver(e) {
-		if (!e.dataTransfer.types.includes('Files')) return;
-		const dropZone = e.target.closest(this.selectors.field.dropZone);
-		if (dropZone) {
-			e.preventDefault();
-			e.dataTransfer.dropEffect = 'copy';
-		}
-	}
-
-	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`);
-		}
-	}
-
-	/*******************************************************************************
-	 * CLICK & CHANGE HANDLERS
-	 *******************************************************************************/
-
-	handleClick(e) {
-		// 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);
-			if (dropZone && !e.target.matches('input, button, a')) {
-				const input = dropZone.querySelector(this.selectors.field.input);
-				input?.click();
-			}
-		}
-
-		// Group actions
-		const actionButton = e.target.closest('[data-action]');
-		if (actionButton) {
-			this.handleAction(actionButton);
-		}
-	}
-
-	handleChange(e) {
-		const fieldId = this.getFieldIdFromElement(e.target);
-
-		// File input change
-		if (e.target.matches(this.selectors.field.input)) {
-			const files = Array.from(e.target.files);
-			if (files.length > 0 && fieldId) {
-				this.processFiles(fieldId, files);
-			}
-		}
-
-		// Meta field changes
-		if (fieldId) {
-			const fieldData = this.getFieldData(fieldId);
-			if (!fieldData.config.autoUpload) {
-				return;
-			}
-			if (fieldData?.config.destination === 'post_group') {
-				this.handleGroupMetaChange(e.target);
-			} else {
-				this.queueUploadMeta(e);
-			}
-		}
-	}
-
-	/*******************************************************************************
-	 * FILE PROCESSING
-	 *******************************************************************************/
-
-	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 (fieldEl.ui.groups?.display) {
-			fieldEl.ui.groups.display.hidden = false;
-		}
-
-		const totalFiles = files.length;
-		let processedCount = 0;
-
-		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-
-		const processPromises = Array.from(files).map(async (file) => {
-			try {
-				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-				// Create initial upload data
-				const uploadData = {
-					id: uploadId,
-					attachmentId: null,
-					fieldId: fieldId,
-					status: 'local_processing',
-					groupId: null,
-					meta: {
-						originalName: file.name,
-						size: file.size,
-						type: file.type
-					}
-				};
-
-				// Save initial data
-				await this.uploadStore.save(uploadData);
-
-				// Process file
-				const preview = this.createPreviewUrl(file);
-				const processedFile = file.type.startsWith('image/')
-					? await this.processImage(file, fieldData.config.subtype)
-					: file;
-
-				// Show progress
-				this.showUploadProgress(uploadId, true);
-				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
-
-				// Store blob data (this updates the existing uploadData)
-				await this.saveBlobData(uploadId, processedFile || file);
-
-				// Create DOM element
-				const subtype = this.getSubtypeFromMime(file.type);
-				const element = this.createUploadElement({
-					id: uploadId,
-					preview: preview,
-					meta: uploadData.meta,
-					subtype: subtype
-				}, fieldData.config.destination === 'post_group');
-
-				// Add to preview grid
-				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
-					});
-				}
-
-				// Update status (gets existing data with blobData intact)
-				const storedUpload = this.uploadStore.get(uploadId);
-				if (storedUpload) {
-					storedUpload.status = 'processed';
-					await this.uploadStore.save(storedUpload);
-				}
-
-				// 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');
-
-				// Fade out progress
-				setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
-
-				return uploadId;
-
-			} catch (error) {
-				console.error('Error processing file:', file.name, error);
-				processedCount++;
-				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
-				return null;
-			}
-		});
-
-		await Promise.all(processPromises);
-
-		this.updateFieldState(fieldId);
-		this.refreshSortable(fieldId);
-
-		// Queue for upload if in direct mode
-		if (fieldData.config.autoUpload && fieldData.config.destination !== 'post_group') {
-			await this.queueUpload(fieldId);
-			this.maybeLockUploads(fieldId);
-		}
-	}
-
-	/*******************************************************************************
-	 * IMAGE PROCESSING
-	 *******************************************************************************/
-
-	async processImage(file, uploadId) {
-		const timeout = this.worker.settings.timeout;
-
-		return new Promise((resolve, reject) => {
-			let timeoutId;
-			let taskCompleted = false;
-
-			timeoutId = setTimeout(() => {
-				if (!taskCompleted) {
-					taskCompleted = true;
-					this.worker.tasks.delete(uploadId);
-					if (this.worker.settings.restartAfterTimeout) {
-						this.restartCompressionWorker();
-					}
-					reject(new Error(`Processing timeout for ${file.name}`));
-				}
-			}, timeout);
-
-			this.worker.tasks.set(uploadId, { file, timeoutId });
-
-			this.handleProcess(file, uploadId)
-				.then(result => {
-					if (!taskCompleted) {
-						taskCompleted = true;
-						clearTimeout(timeoutId);
-						this.worker.tasks.delete(uploadId);
-						resolve(result);
-					}
-				})
-				.catch(error => {
-					if (!taskCompleted) {
-						taskCompleted = true;
-						clearTimeout(timeoutId);
-						this.worker.tasks.delete(uploadId);
-						reject(error);
-					}
-				});
-		});
-	}
-
-	async handleProcess(file, uploadId) {
-		if (!file.type.startsWith('image/')) {
-			return file;
-		}
-
-		const maxDimension = this.getMaxDimension();
-		const quality = 0.85;
-
-		if (this.shouldUseWorker(file)) {
-			try {
-				if (!this.worker.worker) {
-					this.initCompressionWorker();
-				}
-				if (this.worker.worker) {
-					return await this.processWithWorker(file, uploadId, maxDimension, quality);
-				}
-			} catch (error) {
-				console.warn('Worker processing failed, falling back to main thread:', error);
-			}
-		}
-
-		return await this.processOnMainThread(file, maxDimension, quality);
-	}
-
-	async processOnMainThread(file, maxDimension, quality) {
-		return new Promise((resolve, reject) => {
-			const img = new Image();
-			const canvas = document.createElement('canvas');
-			const ctx = canvas.getContext('2d');
-			let objectUrl = null;
-
-			const cleanup = () => {
-				img.onload = null;
-				img.onerror = null;
-				if (objectUrl) {
-					URL.revokeObjectURL(objectUrl);
-					objectUrl = null;
-				}
-				canvas.width = 1;
-				canvas.height = 1;
-				ctx.clearRect(0, 0, 1, 1);
-			};
-
-			img.onload = () => {
-				try {
-					const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
-					canvas.width = width;
-					canvas.height = height;
-
-					ctx.imageSmoothingEnabled = true;
-					ctx.imageSmoothingQuality = 'high';
-					ctx.drawImage(img, 0, 0, width, height);
-
-					const outputFormat = this.getOptimalFormat(file);
-					const outputQuality = this.getOptimalQuality(file, quality);
-
-					canvas.toBlob(
-						(blob) => {
-							cleanup();
-							if (blob) {
-								const processedFile = new File(
-									[blob],
-									this.getProcessedFileName(file, outputFormat),
-									{ type: outputFormat, lastModified: Date.now() }
-								);
-								resolve(processedFile);
-							} else {
-								reject(new Error('Canvas toBlob failed'));
-							}
-						},
-						outputFormat,
-						outputQuality
-					);
-
-				} catch (error) {
-					cleanup();
-					reject(new Error(`Canvas processing failed: ${error.message}`));
-				}
-			};
-
-			img.onerror = () => {
-				cleanup();
-				reject(new Error(`Failed to load image: ${file.name}`));
-			};
-
-			try {
-				objectUrl = this.createPreviewUrl(file);
-				img.src = objectUrl;
-			} catch (error) {
-				cleanup();
-				reject(new Error(`Failed to create object URL: ${error.message}`));
-			}
-		});
-	}
-
-	getOptimalFormat(file) {
-		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
-			return file.type;
-		}
-		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
-	}
-
-	getOptimalQuality(file, requestedQuality) {
-		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
-		if (file.size < 2 * 1024 * 1024) return requestedQuality;
-		return Math.min(requestedQuality, 0.8);
-	}
-
-	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');
-	}
-
-	getMaxDimension() {
-		const screenWidth = window.screen.width;
-		const devicePixelRatio = window.devicePixelRatio || 1;
-		if (screenWidth * devicePixelRatio > 2560) return 2400;
-		if (screenWidth * devicePixelRatio > 1920) return 1920;
-		return 1200;
-	}
-
-	shouldUseWorker(file) {
-		return this.worker.worker &&
-			file.size > 1024 * 1024 &&
-			typeof OffscreenCanvas !== 'undefined';
-	}
-
-	async processWithWorker(file, uploadId, maxDimension, quality) {
-		return new Promise((resolve, reject) => {
-			if (!this.worker.worker) {
-				reject(new Error('Worker not available'));
-				return;
-			}
-
-			const messageId = `${uploadId}_${Date.now()}`;
-
-			const messageHandler = (e) => {
-				if (e.data.messageId !== messageId) return;
-
-				this.worker.worker.removeEventListener('message', messageHandler);
-				this.worker.worker.removeEventListener('error', errorHandler);
-
-				if (e.data.success) {
-					const processedFile = new File(
-						[e.data.blob],
-						this.getProcessedFileName(file, e.data.format || 'image/webp'),
-						{ type: e.data.format || 'image/webp', lastModified: Date.now() }
-					);
-					resolve(processedFile);
-				} else {
-					reject(new Error(e.data.error || 'Worker processing failed'));
-				}
-			};
-
-			const errorHandler = (error) => {
-				this.worker.worker.removeEventListener('message', messageHandler);
-				this.worker.worker.removeEventListener('error', errorHandler);
-				reject(new Error(`Worker error: ${error.message}`));
-			};
-
-			this.worker.worker.addEventListener('message', messageHandler);
-			this.worker.worker.addEventListener('error', errorHandler);
-
-			this.worker.worker.postMessage({
-				messageId,
-				file,
-				maxDimension,
-				quality,
-				outputFormat: this.getOptimalFormat(file)
-			});
-		});
-	}
-
-	restartCompressionWorker() {
-		if (this.worker.worker) {
-			this.worker.worker.terminate();
-			this.worker.worker = null;
-		}
-		this.worker.tasks.clear();
-		if (this.worker.restart.count >= this.worker.restart.max) {
-			console.error('Max worker restarts reached, disabling worker');
-			return;
-		}
-		this.worker.restart.count++;
-		this.initCompressionWorker();
-	}
-
-	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 {
-						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));
-
-		} catch (error) {
-			console.warn('Failed to initialize compression worker:', error);
-			this.worker.worker = null;
-		}
-	}
-
-	calculateOptimalDimensions(img, maxDimension) {
-		let { width, height } = img;
-		if (width <= maxDimension && height <= maxDimension) {
-			return { width, height };
-		}
-		const scale = Math.min(maxDimension / width, maxDimension / height);
-		return {
-			width: Math.round(width * scale),
-			height: Math.round(height * scale)
-		};
-	}
-
-	supportsWebP() {
-		const canvas = document.createElement('canvas');
-		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
-	}
-
-	createPreviewUrl(file) {
-		const url = URL.createObjectURL(file);
-		if (!this.previewUrls) this.previewUrls = new Set();
-		this.previewUrls.add(url);
-		return url;
-	}
-
-	revokePreviewUrl(url) {
-		if (url?.startsWith('blob:')) {
-			URL.revokeObjectURL(url);
-			this.previewUrls?.delete(url);
-		}
-	}
-
-	/*******************************************************************************
-	 * QUEUE INTEGRATION
-	 *******************************************************************************/
-
-	async submitUploads(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		const fieldEl = this.fieldElements.get(fieldId);
-		if (!fieldData?.uploads || fieldData.uploads.size === 0) {
-			return;
-		}
-
-		let uploadIds = Array.from(fieldData.uploads);
-		if (uploadIds.length === 0) {
-			this.error.log('No uploads to upload', {
-				component: 'UploadManager',
-				action: 'submitGroupedUploads',
-				fieldId: fieldId
-			});
-			return;
-		}
-
-		const fieldGroups = this.getFieldGroups(fieldId);
-
-		if (fieldGroups.length === 0) {
-			this.error.log('No groups created for post_group upload', {
-				component: 'UploadManager',
-				action: 'submitGroupedUploads',
-				fieldId: fieldId
-			});
-			return;
-		}
-
-		// Build posts array from groups
-		const posts = [];
-		const formData = new FormData();
-		let uploadMap = [];
-
-		// 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;
-			}
-
-			// Get uploads for this group
-			const groupUploadIds = uploadIds.filter(uploadId => {
-				const upload = this.uploadStore.get(uploadId);
-				return upload?.groupId === group.id;
-			});
-
-			// 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: 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(uploadId);
-				}
-			}
-
-			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', fieldData.config.content);
-		formData.append('user', fieldData.config.itemID);
-		formData.append('posts', JSON.stringify(posts));
-		formData.append('upload_ids', JSON.stringify(uploadMap));
-
-		const operation = {
-			endpoint: 'uploads/groups',
-			method: 'POST',
-			data: formData,
-			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: {
-				'action_nonce': window.auth.getNonce('dash')
-			},
-			append: '_upload',
-		};
-
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			// 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');
-				}
-			});
-
-			fieldData.operationId = operationId;
-			await this.saveFieldData(fieldData);
-
-			this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
-
-			return operationId;
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'submitGroupedUploads',
-				fieldId: fieldId
-			});
-			throw error;
-		}
-	}
-
-	async queueUpload(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
-
-		const uploads = Array.from(fieldData.uploads);
-		const data = this.prepareUploadData(fieldData, uploads);
-
-		this.a11y.announce('Queuing for upload');
-
-		const operation = {
-			endpoint: 'uploads',
-			method: 'POST',
-			data: data,
-			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': window.auth.getNonce('dash') },
-			append: '_upload'
-		};
-
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			// Update upload statuses
-			uploads.forEach(uploadId => {
-				const upload = this.uploadStore.get(uploadId);
-				if (upload) {
-					upload.operationId = operationId;
-					upload.status = 'queued';
-					this.uploadStore.save(upload);
-					this.updateUploadStatus(uploadId, 'queued');
-				}
-			});
-
-			fieldData.operationId = operationId;
-			await this.saveFieldData(fieldData);
-
-			return operationId;
-		} catch (error) {
-			throw error;
-		}
-	}
-
-	async prepareUploadData(fieldData, uploads) {
-		const formData = new FormData();
-		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 blobPromises = uploads.map(async (uploadId) => {
-			const upload = this.uploadStore.get(uploadId);
-			if (!upload) return;
-
-			const file = await this.getBlobData(uploadId);
-			if (file) {
-				formData.append('files[]', file);
-				uploadMap.push(upload.id);
-			}
-		});
-
-		await Promise.all(blobPromises);
-
-		formData.append('upload_ids', JSON.stringify(uploadMap));
-		return formData;
-	}
-
-	async queueUploadMeta(e) {
-		const uploadId = this.getUploadIdFromElement(e.target);
-		const upload = this.uploadStore.get(uploadId);
-		if (!upload) return;
-
-		const fieldData = this.getFieldData(upload.fieldId);
-		if (!fieldData) return;
-
-		let data = {};
-		data[e.target.name] = e.target.value;
-
-		upload.meta = { ...upload.meta, ...data };
-		await this.uploadStore.save(upload);
-
-		let queueData = {};
-		queueData[upload.attachmentId ?? upload.id] = upload.meta;
-
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			data: queueData,
-			title: 'Updating meta',
-			canMerge: true,
-			headers: { 'action_nonce': window.auth.getNonce('dash') }
-		};
-
-		try {
-			await this.queue.addToQueue(operation);
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'sendMetaUpdate',
-				uploadId: upload.id
-			});
-		}
-	}
-
-	/*******************************************************************************
-	 * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
-	 *******************************************************************************/
-
-	/**
-	 * Handle successful operation completion - CLEAR STORES
-	 */
-	async handleOperationComplete(operation, fieldId) {
-		const results = operation.result?.data || operation.serverData?.data || [];
-
-		// 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 (!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 {
-			// Otherwise just update field state
-			await this.saveFieldData(fieldData);
-		}
-
-		this.updateFieldState(fieldId);
-	}
-
-	/**
-	 * Handle operation failure
-	 */
-	handleOperationFailed(operation, fieldId) {
-		const uploadIds = operation.data instanceof FormData
-			? JSON.parse(operation.data.get('upload_ids') || '[]')
-			: operation.data.upload_ids || [];
-
-		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);
-			}
-		});
-
-		if (fieldId) {
-			this.updateFieldState(fieldId);
-		}
-	}
-
-	/**
-	 * Handle operation cancellation
-	 */
-	async handleOperationCancelled(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData) return;
-
-		const uploadsArray = fieldData.uploads instanceof Set
-			? Array.from(fieldData.uploads)
-			: fieldData.uploads;
-
-		for (const uploadId of uploadsArray) {
-			await this.clearUpload(uploadId, false);
-		}
-
-		await this.clearFieldFromStores(fieldId);
-		this.updateFieldState(fieldId);
-		this.a11y.announce('Upload cancelled');
-	}
-
-	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) {
-		let selected = [];
-		const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
-
-		checkboxes.forEach(checkbox => {
-			const item = checkbox.closest('.item');
-			if (item) {
-				selected.push({
-					uploadId: item.dataset.uploadId,
-					fieldId: item.dataset.fieldId
-				});
-			}
-		});
-
-		return selected;
-	}
-
-	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);
-			}
-		}
-	}
-
-	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
-		console.log(config);
-		if (config.autoUpload && 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) {
-			const group = this.createGroup(fieldId, groupData.id);
-			if (!group) {
-				console.warn('Failed to create group:', groupData.id);
-				continue;
-			}
-
-			const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
-			if (storedGroup) {
-				// Restore metadata
-				if (groupData.changes) {
-					storedGroup.changes = { ...groupData.changes };
-				}
-
-				// Preserve upload order
-				if (groupData.uploads) {
-					storedGroup.uploads = [...groupData.uploads];
-				}
-
-				// Restore form field values
-				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;
-					}
-				}
-			}
-		}
-
-		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];
-	}
-
-	/*******************************************************************************
-	 * 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;
-
-		const uploadCount = fieldData.uploads?.size || 0;
-
-		// For groupable uploads, set max to 20
-		const maxFiles = fieldData.config.destination === 'post_group'
-			? 20
-			: (fieldData.config?.maxFiles || 999);
-
-		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
-		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
-
-		// Show helpful message for groupable uploads
-		if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
-			this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
-		}
-	}
-
-	/*******************************************************************************
-	 * GROUP MANAGEMENT
-	 *******************************************************************************/
-	/**
-	 * Create sortable instance for a grid
-	 */
-	createSortableForGrid(grid, fieldId, groupId = null) {
-		if (!grid || grid.sortableInstance) return;
-
-		const sortableInstance = new Sortable(grid, {
-			animation: 150,
-			draggable: '.item',
-			multiDrag: true,
-			selectedClass: 'selected-for-drag',
-			avoidImplicitDeselect: true,
-			group: { name: fieldId, pull: true, put: true },
-			ghostClass: 'sortable-ghost',
-			chosenClass: 'sortable-chosen',
-			dragClass: 'sortable-drag',
-
-			// Centralized drop handler
-			onEnd: (evt) => this.handleDrop(evt, fieldId),
-
-			// Selection sync
-			onSelect: (evt) => {
-				const checkbox = evt.item.querySelector('[name*="select-item"]');
-				if (checkbox && !checkbox.checked) {
-					checkbox.checked = true;
-					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
-				}
-			},
-
-			onDeselect: (evt) => {
-				const checkbox = evt.item.querySelector('[name*="select-item"]');
-				if (checkbox && checkbox.checked) {
-					checkbox.checked = false;
-					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
-				}
-			},
-
-			onAdd: (evt) => this.updateSortableState(evt.to),
-			onRemove: (evt) => this.updateSortableState(evt.from)
-		});
-
-		grid.sortableInstance = sortableInstance;
-
-		const gridId = groupId
-			? `${fieldId}-group-${groupId}`
-			: `${fieldId}-preview`;
-
-		this.sortableInstances.set(gridId, sortableInstance);
-
-		return sortableInstance;
-	}
-	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 = [];
-		const existingGroup = fieldData.groups.find(g => g.id === groupId);
-		if (!existingGroup) {
-			fieldData.groups.push({
-				id: groupId,
-				uploads: [],
-				changes: {}
-			});
-			this.saveFieldData(fieldData);
-		}
-
-		// Initialize selection handler
-		this.addGroupSelectionHandler(fieldId, groupId);
-
-		if (grid) {
-			this.createSortableForGrid(grid, fieldId, groupId);
-		}
-
-		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 'restore-all':
-				this.handleRestoreAll().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':
-					// Sync with Sortable
-					this.syncSortableSelection(fieldId, data.selectedItems);
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'item-deselected':
-					this.syncSortableSelection(fieldId, data.selectedItems);
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'range-selected':
-					this.syncSortableSelection(fieldId, data.selectedItems);
-					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
-	}
-
-	/*******************************************************************************
-	 * 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]);
+		return map[status]||status;
 	}
-
 	getStatusProgress(status) {
-		const progress = {
+		let progress = {
 			'local_processing': 28,
 			'queued': 50,
 			'uploading': 66,
@@ -2619,16 +1091,21 @@
 			'processing': 89,
 			'completed': 100
 		};
-		return progress[status] || 0;
+		return progress[status]??0;
 	}
-
-
-	createUploadElement(upload, draggable = false) {
+	/*******************************************************************************
+	 UPLOAD METHODS
+	*******************************************************************************/
+	async createUpload(uploadId, file, fieldId) {
 		let image = window.getTemplate('uploadItem');
-		if (!image) return;
+		if (!image) return null;
 
-		image.dataset.uploadId = upload.id;
-		image.dataset.subtype = upload.subtype || 'image';
+		let field = this.fields.get(fieldId);
+		if (!field) return null;
+
+		image.dataset.uploadId = uploadId;
+		let mimeType = this.getSubtypeFromMime(file.type)||'image';
+		image.dataset.subtype = mimeType;
 
 		let [featured, img, video, preview, details] = [
 			image.querySelector('[name="featured"]'),
@@ -2638,33 +1115,37 @@
 			image.querySelector('details')
 		];
 
-		if (featured) featured.value = upload.id;
-
-		switch (upload.subtype) {
+		if (featured) featured.value = uploadId;
+		switch (mimeType) {
 			case 'image':
 				if (img) {
-					img.src = upload.preview;
-					img.alt = upload.meta?.originalName || '';
+					const previewUrl = this.createPreviewUrl(file);
+					img.src = previewUrl;
+					img.alt = file.name || '';
+					img.dataset.previewUrl = previewUrl;
 				}
 				video?.remove();
 				preview?.remove();
 				break;
 			case 'video':
-				if (video) video.src = upload.preview;
+				if (video){
+					const previewUrl = this.createPreviewUrl(file);
+					video.src = previewUrl;
+					video.dataset.previewUrl = previewUrl;
+				}
 				img?.remove();
 				preview?.remove();
 				break;
 			case 'document':
-				const fileName = upload.meta?.originalName || '';
-				const extension = fileName.split('.').pop()?.toLowerCase() || '';
-				const iconMap = {
+				let ext = file.name.split('.').pop()?.toLowerCase()??'';
+				let map = {
 					'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');
+				let icon = window.getIcon(map[ext]??'file');
 				if (preview) {
-					preview.innerText = fileName;
+					preview.innerText = file.name;
 					preview.prepend(icon);
 				}
 				img?.remove();
@@ -2677,13 +1158,12 @@
 			if (template) details.append(template);
 		}
 
-		image.draggable = draggable;
+		image.draggable = field.config.type !== 'single'??false;
 
-		// Update input IDs
-		image.querySelectorAll('input').forEach(input => {
+		image.querySelectorAll('input').forEach(input  => {
 			let id = input.id;
 			if (id) {
-				let newId = id + upload.id;
+				let newId = id + uploadId;
 				let label = input.parentNode.querySelector(`label[for="${id}"]`);
 				input.id = newId;
 				if (label) label.htmlFor = newId;
@@ -2693,391 +1173,527 @@
 		return image;
 	}
 
-	/*******************************************************************************
-	 * PERSISTENCE
-	 *******************************************************************************/
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
+	}
+	/**
+	 * Called by handleAction
+	 * @param button
+	 */
+	async handleRemoveItem(button) {
+		const item = button.closest(this.selectors.items.item);
+		if (!item) return;
 
-	schedulePersistance(fieldId) {
-		const key = `persist_${fieldId}`;
-		window.debouncer.schedule(
-			key,
-			() => this.persistFieldState(fieldId),
-			250
+		const uploadId = item.dataset.uploadId;
+		if (!confirm('Remove this item?')) return;
+		await this.removeUpload(uploadId);
+		this.a11y.announce('Item removed');
+	}
+
+	async setBulkUpload(uploads, key, value) {
+		const promises = Array.from(uploads).map(async (upload) => {
+			if (key === 'status') {
+				await this.setUploadStatus(upload, value);
+			}
+			upload[key] = value;
+			return this.stores.uploads.save(upload);
+		});
+		await Promise.all(promises);
+	}
+
+	async setUploadStatus(upload, status) {
+		if (upload.progress) {
+			window.showProgress(upload.progress, this.getStatusProgress(status), 100, this.getStatusText(status), this.queue.icons[status]??'');
+		}
+	}
+
+	async removeUpload(uploadId) {
+		let upload = this.stores.uploads.get(uploadId);
+		if (!upload) return;
+		if (upload.group) {
+			let group = this.stores.groups.get(upload.group);
+			group.uploads = group.uploads.filter(id => id !== uploadId);
+			if (group.uploads.length === 0) {
+				await this.removeGroup(group.id, false);
+			}
+		}
+
+		await this.clearUpload(uploadId);
+		this.maybeLockUploads(upload.field);
+
+		let handler = this.selectionHandlers.get(upload.field);
+		if (handler){
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	async clearUpload(uploadId) {
+		const element = this.uploads.get(uploadId);
+		if (element) {
+			this.revokePreviewUrl(element.preview);
+			if (element.element) {
+				const previewUrl = element.element.dataset.previewUrl;
+				this.revokePreviewUrl(previewUrl);
+				element.element.remove();
+			}
+		}
+		this.uploads.delete(uploadId);
+		await this.stores.uploads.delete(uploadId);
+	}
+
+	/*******************************************************************************
+	 GROUP METHODS
+	*******************************************************************************/
+	async handleAddToGroup(fieldId) {
+		const selected = this.selected.get(fieldId);
+		if (!selected || selected.size === 0) return;
+
+		let groupId = await this.createGroup(fieldId);
+		if (!groupId) return;
+
+		await Promise.all(
+			Array.from(selected).map(uploadId => this.addToGroup(uploadId, groupId))
+		);
+
+		this.selectionHandlers.get(fieldId)?.clearSelection();
+		this.a11y.announce(`Created group with ${selected.size} items`);
+	}
+	async createGroup(fieldId, groupId = null) {
+		let field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (!groupId) {
+			groupId = window.generateID('group');
+		}
+
+		const element = this.createGroupElement(groupId, fieldId);
+		if (!element) return null;
+
+		field.groupUI.grid.append(element);
+
+		// Create Sortable for this group's grid
+		const grid = element.querySelector('.item-grid');
+		if (grid) {
+			grid.dataset.groupId = groupId;
+			this.createSortableForGrid(fieldId, grid, groupId);
+		}
+
+		let storedData = this.stores.groups.data.has(groupId)
+			? this.stores.groups.data.get(groupId)
+			: {};
+
+		await this.setGroup(groupId, { ...storedData, id: groupId, field: fieldId });
+
+		return groupId;
+	}
+
+	createGroupElement(groupId, fieldId = null) {
+		let element = window.getTemplate('imageGroup');
+		if (!element) return;
+
+		element.dataset.groupId = groupId;
+		if (fieldId) {
+			element.dataset.fieldId = fieldId;
+		}
+
+		const selectAll = element.querySelector('[data-select-all]');
+		if (selectAll) {
+			const newId = `select-all-${groupId}`;
+			const label = element.querySelector(`label[for="${selectAll.id}"]`);
+			selectAll.id = newId;
+			selectAll.name = newId;
+			if (label) label.htmlFor = newId;
+		}
+
+		let fields = window.getTemplate('groupMetadata');
+		let container = element.querySelector('.fields');
+		if (fields && container) {
+			container.append(fields);
+
+			let title = container.querySelector('[name="post_title"]');
+			let excerpt = container.querySelector('[name="post_excerpt"]');
+
+			if (title) {
+				title.id = `${groupId}_title`;
+				title.name = `${groupId}[post_title]`;
+			}
+			if (excerpt) {
+				excerpt.id = `${groupId}_excerpt`;
+				excerpt.name = `${groupId}[post_excerpt]`;
+			}
+		} else {
+			element.querySelector('details')?.remove();
+		}
+
+		const grid = element.querySelector('.item-grid');
+		if (grid) {
+			grid.dataset.groupId = groupId;
+		}
+
+		this.groups.set(groupId, {
+			element: element,
+			ui: window.uiFromSelectors(this.selectors.group, element)
+		});
+		return element;
+	}
+
+
+	async setGroup(groupId, data) {
+		const defaults = {
+			id: groupId,
+			src: window.location.href,
+			uploads: [],
+			operationId: null,
+			field: null,
+			fields: {}
+		};
+		const group = {...defaults, ...data};
+		Object.preventExtensions(group);
+
+		await this.stores.groups.save(group);
+	}
+
+	async setBulkGroup(fieldId, key, value) {
+		let groups = this.stores.groups.filterByIndex({field:fieldId});
+		if (groups.length === 0) {
+			return;
+		}
+		let Promises = groups.map(group => {
+			group[key] = value;
+			this.stores.groups.save(group);
+		});
+		await Promise.all(Promises);
+	}
+
+	async addToGroup(uploadId, groupId = null){
+		const upload = this.stores.uploads.get(uploadId);
+		const element = this.uploads.get(uploadId);
+		if (!upload || !element) return;
+		const field = this.fields.get(upload.field);
+		if (!field) return;
+
+		//Check if it's already in this destination, it's probably a reorder
+		const isInDOM = element.element?.parentElement !== null;
+		if (isInDOM && ((!groupId && upload.group === null) || groupId === upload.group)) {
+			this.handleReorder(upload.field, groupId);
+			return;
+		}
+
+		if (upload.group) {
+			const group = this.stores.groups.get(upload.group);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					await this.removeGroup(group.id, false);
+				}
+			}
+		}
+
+		//clear any selection
+		if (element.ui.checkbox) element.ui.checkbox.checked = false;
+		if (this.selected.get(upload.field)?.has(uploadId)) {
+			this.selected.get(upload.field).delete(uploadId);
+		}
+		if (element.ui.featured) element.ui.featured.hidden = !groupId;
+
+		if (!groupId) {
+			upload.group = null;
+		} else {
+			if (element.ui.featured) element.ui.featured.name = `${groupId}_featured`;
+			let group = this.stores.groups.get(groupId);
+			if (group) {
+				group.uploads.push(uploadId);
+				upload.group = groupId;
+				this.stores.groups.save(group);
+			}
+		}
+
+		let target = (groupId) ? this.groups.get(groupId)?.ui.grid : field.ui.grid;
+		if (target) {
+			target.append(element.element)
+		}
+		this.stores.uploads.save(upload);
+	}
+
+	handleDeleteGroup(button) {
+		const group = button.closest(this.selectors.group.item);
+		if (!group) return;
+
+		let groupId = group.dataset.groupId;
+		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+			return;
+		}
+
+		let uploads = this.stores.uploads.filterByIndex({group: groupId});
+
+		Promise.all(
+			uploads.map(upload => this.addToGroup(upload.id, null))
+		).then(() => {
+			this.removeGroup(groupId, false).then(()=>{});
+			this.a11y.announce('Group deleted. Items returned to upload area');
+		});
+	}
+
+	async removeGroup(groupId, confirm = true) {
+		let element = this.groups.get(groupId);
+		let group = this.stores.groups.get(groupId);
+		if (!group) return;
+
+		let keepUploads = true;
+
+		if (confirm && group.uploads.length > 0) {
+			keepUploads = window.confirm('Keep uploads in this group?');
+		}
+
+		await Promise.all(
+			group.uploads.map(uploadId =>
+				keepUploads ? this.addToGroup(uploadId, null) : this.removeUpload(uploadId)
+			)
+		);
+
+		// Destroy the Sortable for this group
+		const sortableKey = this.getGroupKey(group.field, groupId);
+		const sortable = this.sortables.get(sortableKey);
+		if (sortable?.destroy) {
+			sortable.destroy();
+		}
+		this.sortables.delete(sortableKey);
+
+		if (element?.element) {
+			element.element.remove();
+		}
+		this.groups.delete(groupId);
+
+		await this.stores.groups.delete(groupId);
+
+		this.a11y.announce('Group removed');
+	}
+
+	maybeLockUploads(fieldId) {
+		let field = this.fields.get(fieldId);
+		if (!field || !field.ui.dropZone) return;
+
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		let count = uploads.length;
+		let max = field.config.maxFiles??25;
+
+		field.ui.dropZone.hidden = count >= max;
+	}
+	/*******************************************************************************
+	 OPERATION METHODS
+	*******************************************************************************/
+	async handleOperationCancelled(fieldId) {
+		const uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		const groups = this.stores.groups.filterByIndex({field: fieldId});
+
+		await Promise.all([
+			...uploads.map(upload => this.removeUpload(upload.id)),
+			...groups.map(group => this.removeGroup(group.id, false))
+		]);
+		this.a11y.announce('Upload Cancelled');
+	}
+
+	async handleOperationFailed(operation, fieldId) {
+		// Mark uploads as failed, maybe show retry UI
+		await this.setBulkUpload(
+			this.stores.uploads.filterByIndex({field: fieldId}),
+			'status',
+			'failed'
 		);
 	}
 
-	async persistFieldState(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData) return;
-
-		// Save with updated timestamp
-		await this.saveFieldData(fieldData);
+	async handleFieldStatus(fieldId, operation) {
+		let status = operation.status;
+		let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+		await this.setBulkUpload(uploads, 'status', status);
 	}
-
-	// 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
-		});
-	}
-
 	/*******************************************************************************
-	 HELPER to GET UPLOADED FILES
+	 SELECTION HANDLERS
 	*******************************************************************************/
-	/**
-	 * Get all files for a form (searches all upload fields within form)
-	 */
-	async getFilesForForm(formElement) {
-		const uploadFields = formElement.querySelectorAll('[data-upload-field]');
-		const allFiles = [];
-
-		for (const field of uploadFields) {
-			const fieldId = this.determineFieldId(field);
-			const files = await this.getFilesForField(fieldId);
-			allFiles.push(...files);
-		}
-
-		return allFiles;
+	getGroupKey(fieldId, groupId = null) {
+		return (groupId) ? `${fieldId}_${groupId}` : `${fieldId}`;
 	}
 
-	/**
-	 * Get all files for a specific field
-	 */
-	async getFilesForField(fieldId) {
-		const fieldData = this.getFieldData(fieldId);
-		if (!fieldData?.uploads) return [];
+	getSelectionHandler(fieldId) {
+		let key = this.getGroupKey(fieldId);
 
-		const files = [];
-		const uploadsArray = fieldData.uploads instanceof Set
-			? Array.from(fieldData.uploads)
-			: fieldData.uploads;
-
-		for (const uploadId of uploadsArray) {
-			const upload = this.uploadStore.get(uploadId);
-			if (!upload) continue;
-
-			// Get the actual File object from blob data
-			const file = await this.getBlobData(uploadId);
-			if (file) {
-				files.push({
-					file: file,
-					uploadId: uploadId,
-					fieldName: fieldData.config.name,
-					meta: upload.meta || {}
-				});
-			}
-		}
-
-		return files;
-	}
-
-	/*******************************************************************************
-	 * RECOVERY & RESTORATION
-	 *******************************************************************************/
-
-	handleFieldStoreEvent(event, data) {
-		switch(event) {
-			case 'data-loaded':
-				this.fieldStoreReady = true;
-				this.checkIfBothStoresReady();
-				break;
-		}
-	}
-
-	handleUploadStoreEvent(event, data) {
-		switch(event) {
-			case 'data-loaded':
-				this.uploadStoreReady = true;
-				this.checkIfBothStoresReady();
-				break;
-			case 'item-saved':
-				this.showSaveIndicator(data.key);
-				break;
-		}
-	}
-
-	checkIfBothStoresReady() {
-		if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
-			this.hasCheckedForUploads = true;
-			this.checkForStoredUploads();
-		}
-	}
-
-	async checkForStoredUploads() {
-		const allFieldStates = this.fieldStore.getAll();
-
-		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);
+		if (!this.selectionHandlers.has(key)) {
+			let field = this.fields.get(fieldId);
+			if (!field) return;
+			let handler = new window.jvbHandleSelection({
+				container:  field.element,
+				item: this.selectors.items.item,
+				count: this.selectors.fields.count,
+				bulkControls: this.selectors.fields.actions,
+				checkbox: this.selectors.items.checkbox,
+				selectAll: this.selectors.fields.selectAll,
+				wrapper: `${this.selectors.fields.preview}, ${this.selectors.group.item}`,
 			});
-		});
-		if (pendingFields.length === 0) return;
 
-		await this.showRecoveryNotification(pendingFields);
-	}
+			handler.subscribe((event, data) => {
+				this.selected.set(fieldId, data.selectedItems);
+				console.log(Array.from(this.selected));
+				this.syncSortableSelection(fieldId, data.selectedItems);
+			});
 
-	async showRecoveryNotification(pendingFields) {
-		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
-		const totalGroups = pendingFields.reduce((sum, field) =>
-			sum + (field.groups?.length || 0), 0);
-
-		let notification = window.getTemplate('restoreNotification');
-		if (!notification) {
-			console.error('Restore notification template not found');
-			return;
+			this.selectionHandlers.set(key, handler);
 		}
 
-		// Build appropriate message
-		let message;
-		if (totalGroups > 0) {
-			let group = totalGroups > 1 ? 'groups' : 'group';
-			let upload = totalUploads > 1 ? 'uploads' : 'upload';
-			message = `${totalGroups} ${group} with ${totalUploads} ${upload} can be restored.`;
+		return this.selectionHandlers.get(key);
+	}
+	/*******************************************************************************
+	 SORTABLE
+	*******************************************************************************/
+	initSortable(fieldId) {
+		if (!window.Sortable) return;
+
+		const field = this.fields.get(fieldId);
+		if (!field) return;
+
+		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+			Sortable.mount(new Sortable.MultiDrag());
+			Sortable._multiDragMounted = true;
+		}
+
+		// Create sortable for the main preview grid
+		this.createSortable(fieldId, field.ui.grid, null);
+
+		// Set up empty-group as native drop zone
+		this.initEmptyGroupDropZone(fieldId);
+	}
+
+	createSortable(fieldId, gridElement, groupId) {
+		if (!gridElement) return null;
+
+		const key = this.getGroupKey(fieldId, groupId);
+
+		// Already exists
+		if (this.sortables.has(key)) {
+			return this.sortables.get(key);
+		}
+
+		const sortable = new Sortable(gridElement, {
+			animation: 150,
+			draggable: '.item',
+			multiDrag: true,
+			selectedClass: 'selected',
+			avoidImplicitDeselect: true,
+			group: { name: fieldId, pull: true, put: true },
+			ghostClass: 'ghost',
+			chosenClass: 'chosen',
+			dragClass: 'dragging',
+
+			onStart: () => this.syncSortableSelection(fieldId),
+			onEnd: (evt) => this.sortableDrop(evt, fieldId),
+		});
+
+		this.sortables.set(key, sortable);
+		return sortable;
+	}
+
+	initEmptyGroupDropZone(fieldId) {
+		const field = this.fields.get(fieldId);
+		const emptyZone = field?.groupUI?.empty;
+		if (!emptyZone) return;
+
+		emptyZone.addEventListener('dragover', (e) => {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'move';
+			emptyZone.classList.add('drag-over');
+		});
+
+		emptyZone.addEventListener('dragleave', (e) => {
+			if (!emptyZone.contains(e.relatedTarget)) {
+				emptyZone.classList.remove('drag-over');
+			}
+		});
+
+		emptyZone.addEventListener('drop', async (e) => {
+			e.preventDefault();
+			emptyZone.classList.remove('drag-over');
+
+			// Get selected items from our tracking
+			const selectedItems = this.selected.get(fieldId);
+			if (!selectedItems || selectedItems.size === 0) return;
+
+			const groupId = await this.createGroup(fieldId);
+			if (!groupId) return;
+
+			await Promise.all(
+				Array.from(selectedItems).map(uploadId => this.addToGroup(uploadId, groupId))
+			);
+
+			this.selectionHandlers.get(fieldId)?.clearSelection();
+		});
+	}
+
+	async sortableDrop(evt, fieldId) {
+		const dropTarget = evt.to;
+
+		const items = evt.items?.length > 0 ? Array.from(evt.items) : [evt.item];
+		const uploadIds = items.map(item => item.dataset.uploadId).filter(Boolean);
+
+		if (uploadIds.length === 0) return;
+
+		// Determine target group from the grid's data attribute
+		const targetGroupId = dropTarget.dataset.groupId || null;
+
+		await Promise.all(
+			uploadIds.map(uploadId => this.addToGroup(uploadId, targetGroupId))
+		);
+
+		this.selectionHandlers.get(fieldId)?.clearSelection();
+	}
+
+	syncSortableSelection(fieldId) {
+		const selectedItems = this.selected.get(fieldId) || new Set();
+
+		for (const [uploadId, uploadData] of this.uploads) {
+			const upload = this.stores.uploads.get(uploadId);
+			if (!upload || upload.field !== fieldId) continue;
+
+			const element = uploadData.element;
+			if (!element) continue;
+
+			const shouldBeSelected = selectedItems.has(uploadId);
+
+			if (shouldBeSelected && !element.classList.contains('selected')) {
+				Sortable.utils.select(element);
+			} else if (!shouldBeSelected && element.classList.contains('selected')) {
+				Sortable.utils.deselect(element);
+			}
+		}
+	}
+
+	handleReorder(fieldId, groupId = null) {
+		let target = (groupId) ? this.groups.get(groupId)?.ui.grid : this.fields.get(fieldId)?.ui.grid;
+		if (!target) {
+			console.log ('Couldn\'t Reorder items...');
+			return;
+		}
+		//Get current order from DOM
+		let items = Array.from(target.querySelectorAll(this.selectors.items.item+':not(.ghost)'))
+			.map(upload => upload.dataset.uploadId)
+			.filter(id => id);
+
+
+		if (!groupId) {
+			let hiddenInput = this.fields.get(fieldId)?.ui.hidden;
+			if (hiddenInput) {
+				hiddenInput.value = items.join(',');
+			}
 		} else {
-			message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
-		}
-
-		const detailsEl = notification.querySelector('.restore-details');
-		if (detailsEl) {
-			detailsEl.textContent = message;
-		}
-
-		// Build the restoration preview
-		for (const field of pendingFields) {
-			let fieldTemplate = window.getTemplate('restoreField');
-			if (!fieldTemplate) continue;
-
-			// Set field name/title
-			const titleEl = fieldTemplate.querySelector('h3');
-			if (titleEl) {
-				titleEl.textContent = field.config.name || 'Unnamed Field';
+			let group = this.groups.get(groupId);
+			if (group) {
+				group.uploads = items;
 			}
-
-			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
-
-			// Process each upload
-			for (let uploadId of field.uploads) {
-				const upload = this.uploadStore.get(uploadId);
-				let uploadItem = window.getTemplate('uploadItem');
-				if (!uploadItem) continue;
-				//
-				// 	const imgEl = uploadItem.querySelector('img');
-				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
-				//
-				const file = await this.getBlobData(upload.id);
-				if (file) {
-
-					try {
-						// Create new blob URL from stored data
-						const previewUrl = this.createPreviewUrl(file);
-
-						let [
-							featured,
-							img,
-							video,
-							preview,
-							details
-						] = [
-							uploadItem.querySelector('[name="featured"]'),
-							uploadItem.querySelector('img'),
-							uploadItem.querySelector('video'),
-							uploadItem.querySelector('label > span'),
-							uploadItem.querySelector('details')
-						];
-
-						uploadItem.dataset.uploadId = upload.id;
-
-
-						uploadItem.dataset.fieldId = field.id;
-
-						let subtype = this.getSubtypeFromMime(file.type);
-						uploadItem.dataset.subtype = subtype;
-						switch (subtype) {
-							case 'image':
-								[
-									img.src,
-									img.alt
-								] = [
-									previewUrl,
-									file.name ?? upload.meta?.originalName ?? ''
-								];
-								video.remove();
-								preview.remove();
-								break;
-							case 'video':
-								video.src = previewUrl;
-								img.remove();
-								preview.remove();
-								break;
-							case 'document':
-								let extension = '';
-								let icon;
-								switch (extension) {
-									case 'pdf':
-										icon = window.getIcon('file-pdf');
-										break;
-									case 'csv':
-										icon = window.getIcon('file-csv');
-										break;
-									case 'doc':
-										icon = window.getIcon('file-doc');
-										break;
-									case 'txt':
-										icon = window.getIcon('file-txt');
-										break;
-									case 'xls':
-										icon = window.getIcon('file-xls');
-										break;
-									default:
-										icon = window.getIcon('file');
-										break;
-								}
-
-								preview.innerText = upload.originalFile.name;
-								preview.prepend(icon);
-								img.remove();
-								video.remove();
-								break;
-						}
-
-						// Store URL for cleanup later
-						uploadItem.dataset.previewUrl = previewUrl;
-					} catch (error) {
-						console.warn('Failed to create preview for upload:', upload.id, error);
-					}
-				}
-
-				// Set upload metadata
-				const nameEl = uploadItem.querySelector('summary span');
-				if (nameEl) {
-					nameEl.textContent = upload.meta?.originalName || 'Unknown file';
-				}
-
-				const metaEl = uploadItem.querySelector('details');
-				if (metaEl && upload.meta) {
-					metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
-				}
-
-				// Update input IDs safely
-				uploadItem.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;
-						}
-					}
-				});
-
-				if (itemGrid) {
-					itemGrid.appendChild(uploadItem);
-				}
-			}
-
-			notification.querySelector('.wrap').appendChild(itemGrid);
 		}
-
-		document.querySelector('.field.upload').appendChild(notification);
-		notification = document.querySelector('dialog.restore-uploads');
-		this.restoreModal = new window.jvbModal(notification);
-		this.restoreSelection = new window.jvbHandleSelection({
-			container: notification,
-			ui: {
-				selectAll: notification.querySelector('#select-all-restore'),
-				count: notification.querySelector('.selection-count'),
-			},
-		});
-
-		this.restoreModal.handleOpen();
-
+		this.a11y.announce('Items reordered');
 	}
-
-	async handleRestoreUploads() {
-		let notification = document.querySelector('dialog.restore-uploads');
-		if (!notification) {
-			return;
-		}
-
-		const selectedUploads = this.getSelectedRestorationUploads(notification);
-		if (selectedUploads.length === 0) {
-			return;
-		}
-		await this.restoreSelectedUploads(selectedUploads);
-
-		this.cleanupRestore();
-	}
-
-	async handleRestoreAll() {
-		let notification = document.querySelector('dialog.restore-uploads');
-		if (!notification) {
-			return;
-		}
-		// Gets ALL uploads from notification without checking selection
-		const allUploads = [];
-		notification.querySelectorAll('.item.upload').forEach(item => {
-			let uploadId = item.dataset.uploadId;
-			let fieldId = item.dataset.fieldId;
-			allUploads.push({ uploadId, fieldId });
-		});
-
-		await this.restoreSelectedUploads(allUploads);
-		this.cleanupRestore();
-	}
-
-	showSaveIndicator(key) {
-		// Optional: show user that state is being saved
-	}
-
-	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();
-	}
-
 	/*******************************************************************************
 	 * EVENT SYSTEM
 	 *******************************************************************************/
@@ -3089,49 +1705,95 @@
 
 	notify(event, data = {}) {
 		this.subscribers.forEach(cb => {
-			try {
-				cb(event, data);
-			} catch (error) {
-				console.error('Subscriber error:', error);
-			}
+			try { cb(event, data); } catch (e) { console.error('Subscriber error:', e); }
 		});
 	}
-
-	/*******************************************************************************
-	 * DESTROY & CLEANUP
-	 *******************************************************************************/
-
+	/********************************************************************
+	 CLEANUP
+	********************************************************************/
 	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.subscribers.clear();
+		this.previewUrls.forEach(url => {
+			this.revokePreviewUrl(url);
+		});
+		this.previewUrls.clear();
+	}
 
-		if (this.dragController) {
-			this.dragController.destroy();
+	cleanupAllPreviewUrls() {
+		this.previewUrls.forEach(url => this.revokePreviewUrl(url));
+		this.previewUrls.clear();
+	}
+
+	async handleClearCache() {
+		const currentSrc = window.location.href;
+
+		const uploads = this.stores.uploads.filterByIndex({src: currentSrc});
+		const groups = this.stores.groups.filterByIndex({src:currentSrc});
+
+		await Promise.all([
+			...uploads.map(upload => this.clearUpload(upload.id)),
+			...groups.map(group => {
+				this.groups.get(group.id)?.element?.remove();
+				this.groups.delete(group.id);
+				return this.stores.groups.delete(group.id);
+			})
+		]);
+
+		this.a11y.announce('Cache cleared for this page');
+	}
+
+	/**
+	 * Get files from all upload fields in a form
+	 * Returns array of {file, fieldName, uploadId, meta}
+	 */
+	async getFilesForForm(formElement) {
+		const uploadFields = formElement.querySelectorAll(this.selectors.fields.field);
+		const allFiles = [];
+
+		for (const fieldElement of uploadFields) {
+			const fieldId = this.determineFieldId(fieldElement);
+			const fieldName = fieldElement.dataset.field;
+			const uploads = this.stores.uploads.filterByIndex({ field: fieldId });
+
+			for (const upload of uploads) {
+				const file = this.formatFile(upload);
+				if (file) {
+					allFiles.push({
+						file: file,
+						fieldName: fieldName,
+						uploadId: upload.id,
+						meta: upload.fields || {}
+					});
+				}
+			}
 		}
 
-		this.selectionHandlers.forEach(handler => handler.destroy());
-		this.selectionHandlers.clear();
+		return allFiles;
+	}
 
-		this.cleanupAllPreviewUrls();
+	/**
+	 * Clear all uploads and groups for a specific field from stores
+	 */
+	async clearFieldFromStores(fieldId) {
+		const uploads = this.stores.uploads.filterByIndex({ field: fieldId });
+		const groups = this.stores.groups.filterByIndex({ field: fieldId });
 
-		this.sortableInstances.forEach(instance => {
-			if (instance?.destroy) instance.destroy();
-		});
-		this.sortableInstances.clear();
+		// Clear all uploads
+		await Promise.all(
+			uploads.map(upload => this.clearUpload(upload.id))
+		);
 
-		this.uploadElements.clear();
-		this.fieldElements.clear();
-		this.groupElements.clear();
-		this.selected.clear();
-		this.subscribers.clear();
+		// Clear all groups
+		await Promise.all(
+			groups.map(group => {
+				this.groups.get(group.id)?.element?.remove();
+				this.groups.delete(group.id);
+				return this.stores.groups.delete(group.id);
+			})
+		);
 	}
 }
 
-// Initialize when DOM is ready
 document.addEventListener('DOMContentLoaded', async function () {
 	window.auth.subscribe((event) => {
 		if (event === 'auth-loaded') {
diff --git a/assets/js/concise/UploadManagerC.js b/assets/js/concise/UploadManagerC.js
new file mode 100644
index 0000000..4e755ea
--- /dev/null
+++ b/assets/js/concise/UploadManagerC.js
@@ -0,0 +1,2278 @@
+/**
+ * UploadManager - Refactored with simplified store architecture
+ *
+ * Architecture:
+ * - uploadStore: Individual uploads with blob data, keyed by uploadId
+ * - groupStore: Group metadata + upload references, keyed by groupId
+ * - Runtime Maps: DOM references only (fieldElements, uploadElements, groupElements)
+ *
+ * Flow: File → Process → Store → Queue → Server → Clear stores
+ * Recovery: Check stores on load → Show notification → Restore to DOM
+ */
+class UploadManager {
+	constructor() {
+		this.queue = window.jvbQueue;
+		this.a11y = window.jvbA11y;
+		this.error = window.jvbError;
+
+		// Store initialization flags
+		this.storesReady = false;
+		this.hasCheckedForRecovery = false;
+
+		// Register stores
+		const { uploads, groups } = window.jvbStore.register(
+			'uploads',
+			[
+				{
+					storeName: 'uploads',
+					keyPath: 'id',
+					storeBlobs: true,
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'status', keyPath: 'status' },
+						{ name: 'groupId', keyPath: 'groupId' },
+						{ name: 'pageUrl', keyPath: 'pageUrl' }
+					],
+					TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
+					delayFetch: true
+				},
+				{
+					storeName: 'groups',
+					keyPath: 'id',
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'pageUrl', keyPath: 'pageUrl' }
+					],
+					TTL: 7 * 24 * 60 * 60 * 1000,
+					delayFetch: true
+				}
+			]
+		);
+
+		this.uploadStore = uploads;
+		this.groupStore = groups;
+
+		// Subscribe to store events
+		this.uploadStore.subscribe(this.handleStoreEvent.bind(this, 'uploads'));
+		this.groupStore.subscribe(this.handleStoreEvent.bind(this, 'groups'));
+
+		// RUNTIME DATA - DOM references only, not persisted
+		this.fieldElements = new Map();   // fieldId → { element, ui, config }
+		this.uploadElements = new Map();  // uploadId → { element, preview, location }
+		this.groupElements = new Map();   // groupId → { element, grid, fieldId }
+
+		// Selection state
+		this.selected = new Map();		// fieldId -> { }
+		this.selectionHandlers = new Map();
+		this.sortableInstances = new Map();
+
+		// Preview URL tracking for cleanup
+		this.previewUrls = new Set();
+
+		// Worker for image processing
+		this.worker = this.createWorkerConfig();
+
+		// Notification subscribers
+		this.subscribers = new Set();
+
+		// Selectors
+		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',
+			'processing': 'Processing Image...',
+			'queued': 'Waiting to upload...',
+			'uploading': 'Uploading to Server',
+			'pending': 'Sent to server, awaiting processing.',
+			'server_processing': 'Processing on server...',
+			'completed': 'Upload complete!',
+			'failed': 'Upload failed (will retry)',
+			'failed_permanent': 'Upload failed permanently'
+		};
+
+		this.init();
+	}
+
+	/*******************************************************************************
+	 * INITIALIZATION
+	 *******************************************************************************/
+
+	async init() {
+		this.initListeners();
+		this.initQueueSubscription();
+
+		window.addEventListener('beforeunload', () => this.cleanupAllPreviewUrls());
+	}
+
+	createWorkerConfig() {
+		return {
+			worker: null,
+			tasks: new Map(),
+			restart: { count: 0, max: 3 },
+			settings: {
+				timeout: 10000,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
+			}
+		};
+	}
+
+	initQueueSubscription() {
+		this.queue.subscribe((event, operation) => {
+			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
+				return;
+			}
+
+			const fieldId = operation.data instanceof FormData
+				? operation.data.get('fieldId')
+				: operation.data?.fieldId;
+
+			switch (event) {
+				case 'cancel-operation':
+					if (fieldId) this.handleOperationCancelled(fieldId);
+					break;
+				case 'operation-status':
+					if (fieldId) this.updateFieldStatus(fieldId, operation.status);
+					break;
+				case 'operation-complete':
+					this.handleOperationComplete(operation, fieldId);
+					break;
+				case 'operation-failed':
+				case 'operation-failed-permanent':
+					this.handleOperationFailed(operation, fieldId);
+					break;
+			}
+		});
+	}
+
+	/*******************************************************************************
+	 * STORE EVENT HANDLING & RECOVERY
+	 *******************************************************************************/
+
+	handleStoreEvent(storeName, event, data) {
+		if (event === 'data-loaded') {
+			this.checkStoresReady();
+		}
+	}
+
+	checkStoresReady() {
+		// Both stores need to be ready before checking for recovery
+		const uploadsReady = this.uploadStore.getStore()._initialized;
+		const groupsReady = this.groupStore.getStore()._initialized;
+
+		if (uploadsReady && groupsReady && !this.hasCheckedForRecovery) {
+			this.hasCheckedForRecovery = true;
+			this.storesReady = true;
+			this.checkForRecoverableUploads();
+		}
+	}
+
+	async checkForRecoverableUploads() {
+		const allUploads = this.uploadStore.getAll();
+
+		// Find uploads that weren't completed and don't have an active operation
+		const recoverableUploads = allUploads.filter(upload =>
+			!upload.operationId &&
+			['processed', 'processing', 'queued'].includes(upload.status)
+		);
+
+		if (recoverableUploads.length === 0) return;
+
+		// Group by fieldId for display
+		const byField = this.groupUploadsByField(recoverableUploads);
+		await this.showRecoveryNotification(byField);
+	}
+
+	groupUploadsByField(uploads) {
+		const byField = new Map();
+
+		uploads.forEach(upload => {
+			if (!byField.has(upload.fieldId)) {
+				byField.set(upload.fieldId, {
+					fieldId: upload.fieldId,
+					pageUrl: upload.pageUrl,
+					uploads: [],
+					groups: new Map()
+				});
+			}
+
+			const fieldData = byField.get(upload.fieldId);
+			fieldData.uploads.push(upload);
+
+			// Track groups
+			if (upload.groupId) {
+				const group = this.groupStore.get(upload.groupId);
+				if (group) {
+					fieldData.groups.set(upload.groupId, group);
+				}
+			}
+		});
+
+		return byField;
+	}
+
+	/*******************************************************************************
+	 * FIELD MANAGEMENT - Runtime only, no persistence
+	 *******************************************************************************/
+
+	scanFields(container, autoUpload = false) {
+		const fields = container.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
+	}
+
+	registerUploader(uploader, autoUpload = false) {
+		const fieldId = this.determineFieldId(uploader);
+		const config = this.extractFieldConfig(uploader, autoUpload);
+		const ui = this.buildFieldUI(uploader);
+
+		// Store DOM references only - not persisted
+		this.fieldElements.set(fieldId, { element: uploader, ui, config });
+
+		uploader.dataset.uploader = fieldId;
+		this.addFieldSelectionHandler(fieldId);
+
+		if (config.type !== 'single') {
+			this.initSortable(fieldId);
+		}
+
+		return fieldId;
+	}
+
+	extractFieldConfig(fieldElement, autoUpload) {
+		return {
+			autoUpload,
+			destination: fieldElement.dataset.destination || 'meta',
+			content: fieldElement.dataset.content || null,
+			mode: fieldElement.dataset.mode || 'direct',
+			type: fieldElement.dataset.type || 'single',
+			name: fieldElement.dataset.field,
+			itemID: fieldElement.dataset.itemId || 0,
+			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+			subtype: fieldElement.dataset.subtype || 'image'
+		};
+	}
+
+	buildFieldUI(fieldElement) {
+		const UI = {
+			field: fieldElement,
+			input: fieldElement.querySelector(this.selectors.field.input),
+			dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
+			preview: fieldElement.querySelector(this.selectors.field.preview),
+			progress: {
+				container: fieldElement.querySelector(this.selectors.field.progress),
+				bar: fieldElement.querySelector('.bar'),
+				fill: fieldElement.querySelector('.fill'),
+				details: fieldElement.querySelector('.details'),
+				text: fieldElement.querySelector('.details .text'),
+				count: fieldElement.querySelector('.details .count')
+			}
+		};
+
+		const display = fieldElement.querySelector('.group-display');
+		if (display) {
+			UI.groups = {
+				display,
+				container: fieldElement.querySelector('.item-grid.groups'),
+				empty: fieldElement.querySelector('.empty-group'),
+				groups: new Map()
+			};
+		}
+
+		return UI;
+	}
+
+	/**
+	 * Get uploads for a field - derived from store, not cached
+	 */
+	getFieldUploads(fieldId) {
+		return this.uploadStore.getAll().filter(u => u.fieldId === fieldId);
+	}
+
+	/**
+	 * Get groups for a field - derived from store
+	 */
+	getFieldGroups(fieldId) {
+		return this.groupStore.getAll().filter(g => g.fieldId === fieldId);
+	}
+
+	/**
+	 * Get upload count for a field
+	 */
+	getFieldUploadCount(fieldId) {
+		return this.getFieldUploads(fieldId).length;
+	}
+
+	/*******************************************************************************
+	 * FILE PROCESSING
+	 *******************************************************************************/
+
+	async processFiles(fieldId, files) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const { config, ui } = fieldEl;
+
+		// Show group display, hide upload zone
+		if (ui.dropZone) ui.dropZone.hidden = true;
+		if (ui.groups?.display) ui.groups.display.hidden = false;
+
+		const totalFiles = files.length;
+		let processedCount = 0;
+
+		this.updateFieldProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		const processPromises = Array.from(files).map(async (file) => {
+			try {
+				const uploadId = this.generateId('upload');
+
+				// Create upload data
+				const uploadData = {
+					id: uploadId,
+					fieldId,
+					pageUrl: window.location.href,
+					status: 'processing',
+					groupId: null,
+					attachmentId: null,
+					meta: {
+						originalName: file.name,
+						size: file.size,
+						type: file.type
+					}
+				};
+
+				// Save initial state
+				await this.uploadStore.save(uploadData);
+
+				// Process file
+				const preview = this.createPreviewUrl(file);
+				const processedFile = file.type.startsWith('image/')
+					? await this.processImage(file, uploadId)
+					: file;
+
+				// Show progress
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'processing');
+
+				// Store blob data
+				await this.saveBlobData(uploadId, processedFile || file);
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				const element = this.createUploadElement({
+					id: uploadId,
+					preview,
+					meta: uploadData.meta,
+					subtype
+				}, config.destination === 'post_group');
+
+				// Add to preview grid
+				if (ui.preview) {
+					ui.preview.appendChild(element);
+					this.uploadElements.set(uploadId, { element, preview, location: ui.preview });
+				}
+
+				// Update status
+				await this.updateUploadStatus(uploadId, 'processed');
+
+				// Update progress
+				processedCount++;
+				this.updateFieldProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				this.updateUploadItemProgress(uploadId, 100, 'processed');
+
+				setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
+
+				return uploadId;
+
+			} catch (error) {
+				console.error('Error processing file:', file.name, error);
+				processedCount++;
+				this.updateFieldProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				return null;
+			}
+		});
+
+		await Promise.all(processPromises);
+
+		this.updateFieldState(fieldId);
+		this.refreshSortable(fieldId);
+
+		// Queue for upload if auto-upload enabled
+		if (config.autoUpload && config.destination !== 'post_group') {
+			await this.queueUpload(fieldId);
+			this.maybeLockUploads(fieldId);
+		}
+	}
+
+	/*******************************************************************************
+	 * IMAGE PROCESSING (unchanged logic, just cleaner structure)
+	 *******************************************************************************/
+
+	async processImage(file, uploadId) {
+		const timeout = this.worker.settings.timeout;
+
+		return new Promise((resolve, reject) => {
+			let timeoutId;
+			let completed = false;
+
+			timeoutId = setTimeout(() => {
+				if (!completed) {
+					completed = true;
+					this.worker.tasks.delete(uploadId);
+					if (this.worker.settings.restartAfterTimeout) {
+						this.restartWorker();
+					}
+					reject(new Error(`Processing timeout for ${file.name}`));
+				}
+			}, timeout);
+
+			this.worker.tasks.set(uploadId, { file, timeoutId });
+
+			this.handleImageProcess(file, uploadId)
+				.then(result => {
+					if (!completed) {
+						completed = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						resolve(result);
+					}
+				})
+				.catch(error => {
+					if (!completed) {
+						completed = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						reject(error);
+					}
+				});
+		});
+	}
+
+	async handleImageProcess(file, uploadId) {
+		if (!file.type.startsWith('image/')) return file;
+
+		const maxDimension = this.getMaxDimension();
+		const quality = 0.95;
+
+		if (this.shouldUseWorker(file)) {
+			try {
+				if (!this.worker.worker) this.initWorker();
+				if (this.worker.worker) {
+					return await this.processWithWorker(file, uploadId, maxDimension, quality);
+				}
+			} catch (error) {
+				console.warn('Worker failed, using main thread:', error);
+			}
+		}
+
+		return this.processOnMainThread(file, maxDimension, quality);
+	}
+
+	async processOnMainThread(file, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			const img = new Image();
+			const canvas = document.createElement('canvas');
+			const ctx = canvas.getContext('2d');
+			let objectUrl = null;
+
+			const cleanup = () => {
+				img.onload = img.onerror = null;
+				if (objectUrl) {
+					URL.revokeObjectURL(objectUrl);
+					objectUrl = null;
+				}
+				canvas.width = canvas.height = 1;
+				ctx.clearRect(0, 0, 1, 1);
+			};
+
+			img.onload = () => {
+				try {
+					const { width, height } = this.calculateDimensions(img, maxDimension);
+					canvas.width = width;
+					canvas.height = height;
+
+					ctx.imageSmoothingEnabled = true;
+					ctx.imageSmoothingQuality = 'high';
+					ctx.drawImage(img, 0, 0, width, height);
+
+					const outputFormat = this.getOptimalFormat(file);
+					const outputQuality = this.getOptimalQuality(file, quality);
+
+					canvas.toBlob(
+						(blob) => {
+							cleanup();
+							if (blob) {
+								resolve(new File(
+									[blob],
+									this.getProcessedFileName(file, outputFormat),
+									{ type: outputFormat, lastModified: Date.now() }
+								));
+							} else {
+								reject(new Error('Canvas toBlob failed'));
+							}
+						},
+						outputFormat,
+						outputQuality
+					);
+				} catch (error) {
+					cleanup();
+					reject(new Error(`Canvas processing failed: ${error.message}`));
+				}
+			};
+
+			img.onerror = () => {
+				cleanup();
+				reject(new Error(`Failed to load image: ${file.name}`));
+			};
+
+			try {
+				objectUrl = this.createPreviewUrl(file);
+				img.src = objectUrl;
+			} catch (error) {
+				cleanup();
+				reject(new Error(`Failed to create object URL: ${error.message}`));
+			}
+		});
+	}
+
+	// Worker methods (simplified)
+	initWorker() {
+		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 {
+						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 });
+						self.postMessage({ messageId, success: true, 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));
+		} catch (error) {
+			console.warn('Failed to initialize worker:', error);
+			this.worker.worker = null;
+		}
+	}
+
+	async processWithWorker(file, uploadId, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			if (!this.worker.worker) {
+				reject(new Error('Worker not available'));
+				return;
+			}
+
+			const messageId = `${uploadId}_${Date.now()}`;
+			const outputFormat = this.getOptimalFormat(file);
+
+			const handler = (e) => {
+				if (e.data.messageId !== messageId) return;
+				this.worker.worker.removeEventListener('message', handler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+
+				if (e.data.success) {
+					resolve(new File(
+						[e.data.blob],
+						this.getProcessedFileName(file, e.data.format || outputFormat),
+						{ type: e.data.format || outputFormat, lastModified: Date.now() }
+					));
+				} else {
+					reject(new Error(e.data.error || 'Worker processing failed'));
+				}
+			};
+
+			const errorHandler = (error) => {
+				this.worker.worker.removeEventListener('message', handler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+				reject(new Error(`Worker error: ${error.message}`));
+			};
+
+			this.worker.worker.addEventListener('message', handler);
+			this.worker.worker.addEventListener('error', errorHandler);
+			this.worker.worker.postMessage({ messageId, file, maxDimension, quality, outputFormat });
+		});
+	}
+
+	restartWorker() {
+		if (this.worker.worker) {
+			this.worker.worker.terminate();
+			this.worker.worker = null;
+		}
+		this.worker.tasks.clear();
+
+		if (this.worker.restart.count < this.worker.restart.max) {
+			this.worker.restart.count++;
+			this.initWorker();
+		}
+	}
+
+	// Image processing helpers
+	calculateDimensions(img, maxDimension) {
+		let { width, height } = img;
+		if (width <= maxDimension && height <= maxDimension) {
+			return { width, height };
+		}
+		const scale = Math.min(maxDimension / width, maxDimension / height);
+		return { width: Math.round(width * scale), height: Math.round(height * scale) };
+	}
+
+	getMaxDimension() {
+		const screenWidth = window.screen.width;
+		const dpr = window.devicePixelRatio || 1;
+		if (screenWidth * dpr > 2560) return 2400;
+		if (screenWidth * dpr > 1920) return 1920;
+		return 1200;
+	}
+
+	shouldUseWorker(file) {
+		return this.worker.worker && file.size > 1024 * 1024 && typeof OffscreenCanvas !== 'undefined';
+	}
+
+	getOptimalFormat(file) {
+		if (file.type === 'image/gif' || file.type === 'image/svg+xml') return file.type;
+		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
+	}
+
+	getOptimalQuality(file, requestedQuality) {
+		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
+		if (file.size < 2 * 1024 * 1024) return requestedQuality;
+		return Math.min(requestedQuality, 0.8);
+	}
+
+	getProcessedFileName(file, outputFormat) {
+		const baseName = file.name.replace(/\.[^/.]+$/, '');
+		const extensions = { 'image/webp': '.webp', 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif' };
+		return baseName + (extensions[outputFormat] || '.jpg');
+	}
+
+	supportsWebP() {
+		const canvas = document.createElement('canvas');
+		return canvas.toDataURL('image/webp').startsWith('data:image/webp');
+	}
+
+	/*******************************************************************************
+	 * BLOB DATA MANAGEMENT
+	 *******************************************************************************/
+
+	async saveBlobData(uploadId, file) {
+		const arrayBuffer = await file.arrayBuffer();
+		const upload = this.uploadStore.get(uploadId) || { id: uploadId };
+
+		upload.blobData = {
+			buffer: arrayBuffer,
+			name: file.name,
+			type: file.type,
+			size: file.size,
+			lastModified: file.lastModified || Date.now()
+		};
+
+		await this.uploadStore.save(upload);
+	}
+
+	async getBlobData(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload?.blobData) return null;
+
+		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
+		});
+	}
+
+	/*******************************************************************************
+	 * QUEUE INTEGRATION
+	 *******************************************************************************/
+
+	async queueUpload(fieldId) {
+		const uploads = this.getFieldUploads(fieldId);
+		if (uploads.length === 0) return;
+
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const formData = await this.prepareUploadFormData(fieldId, uploads, fieldEl.config);
+
+		this.a11y.announce('Queuing for upload');
+
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: formData,
+			title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
+			popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: { 'action_nonce': window.auth.getNonce('dash') },
+			append: '_upload'
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			// Update upload statuses
+			for (const upload of uploads) {
+				upload.operationId = operationId;
+				upload.status = 'queued';
+				await this.uploadStore.save(upload);
+				this.updateUploadUI(upload.id);
+			}
+
+			return operationId;
+		} catch (error) {
+			throw error;
+		}
+	}
+
+	async prepareUploadFormData(fieldId, uploads, config) {
+		const formData = new FormData();
+
+		formData.append('content', config.content);
+		formData.append('mode', config.mode);
+		formData.append('field_name', config.name);
+		formData.append('fieldId', fieldId);
+		formData.append('field_type', config.type);
+		formData.append('subtype', config.subtype);
+		formData.append('item_id', config.itemID);
+		formData.append('destination', config.destination || 'meta');
+
+		const uploadMap = [];
+
+		for (const upload of uploads) {
+			const file = await this.getBlobData(upload.id);
+			if (file) {
+				formData.append('files[]', file);
+				uploadMap.push(upload.id);
+			}
+		}
+
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+		return formData;
+	}
+
+	async submitGroupedUploads(fieldId) {
+		const uploads = this.getFieldUploads(fieldId);
+		const groups = this.getFieldGroups(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+
+		if (uploads.length === 0 || !fieldEl) return;
+
+		const formData = new FormData();
+		const posts = [];
+		const uploadMap = [];
+
+		// Process each group
+		for (const group of groups) {
+			const groupUploads = uploads.filter(u => u.groupId === group.id);
+
+			const post = {
+				images: [],
+				fields: { ...group.fields }
+			};
+
+			for (const upload of groupUploads) {
+				const file = await this.getBlobData(upload.id);
+				if (file) {
+					formData.append('files[]', file);
+					post.images.push({ upload_id: upload.id, index: uploadMap.length });
+					uploadMap.push(upload.id);
+
+					// Check featured
+					const uploadEl = this.uploadElements.get(upload.id);
+					const featured = uploadEl?.element?.querySelector('[name="featured"]');
+					if (featured?.checked) {
+						post.fields.featured = upload.id;
+					}
+				}
+			}
+
+			if (post.images.length > 0) {
+				posts.push(post);
+			}
+		}
+
+		// Handle ungrouped uploads - each becomes its own post
+		const ungrouped = uploads.filter(u => !u.groupId);
+		for (const upload of ungrouped) {
+			const file = await this.getBlobData(upload.id);
+			if (file) {
+				formData.append('files[]', file);
+				posts.push({
+					images: [{ upload_id: upload.id, index: uploadMap.length }],
+					fields: {}
+				});
+				uploadMap.push(upload.id);
+			}
+		}
+
+		formData.append('content', fieldEl.config.content);
+		formData.append('user', fieldEl.config.itemID);
+		formData.append('posts', JSON.stringify(posts));
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		const operation = {
+			endpoint: 'uploads/groups',
+			method: 'POST',
+			data: formData,
+			title: `Creating ${posts.length} ${fieldEl.config.content}${posts.length > 1 ? 's' : ''}...`,
+			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: { 'action_nonce': window.auth.getNonce('dash') },
+			append: '_upload'
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			for (const upload of uploads) {
+				upload.operationId = operationId;
+				upload.status = 'queued';
+				await this.uploadStore.save(upload);
+				this.updateUploadUI(upload.id);
+			}
+
+			this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''}`);
+			return operationId;
+		} catch (error) {
+			this.error.log(error, { component: 'UploadManager', action: 'submitGroupedUploads', fieldId });
+			throw error;
+		}
+	}
+
+	async queueUploadMeta(e) {
+		const uploadId = this.getUploadIdFromElement(e.target);
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		const data = { [e.target.name]: e.target.value };
+		upload.meta = { ...upload.meta, ...data };
+		await this.uploadStore.save(upload);
+
+		const queueData = { [upload.attachmentId ?? upload.id]: upload.meta };
+
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			data: queueData,
+			title: 'Updating meta',
+			canMerge: true,
+			headers: { 'action_nonce': window.auth.getNonce('dash') }
+		};
+
+		try {
+			await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, { component: 'UploadManager', action: 'queueUploadMeta', uploadId });
+		}
+	}
+
+	/*******************************************************************************
+	 * QUEUE EVENT HANDLERS - Cleanup after success
+	 *******************************************************************************/
+
+	async handleOperationComplete(operation, fieldId) {
+		const results = operation.result?.data || operation.serverData?.data || [];
+
+		// Update attachment IDs
+		for (const result of results) {
+			const upload = this.uploadStore.get(result.upload_id);
+			if (upload) {
+				upload.attachmentId = result.attachment_id;
+				upload.status = 'completed';
+				await this.uploadStore.save(upload);
+				this.updateUploadUI(result.upload_id);
+			}
+		}
+
+		if (!fieldId) return;
+
+		// Clear completed uploads and their groups
+		await this.clearCompletedUploads(fieldId);
+
+		this.updateFieldState(fieldId);
+		this.a11y.announce('All uploads completed successfully');
+	}
+
+	async clearCompletedUploads(fieldId) {
+		const uploads = this.getFieldUploads(fieldId);
+		const groupIds = new Set();
+
+		for (const upload of uploads) {
+			if (upload.status === 'completed') {
+				if (upload.groupId) groupIds.add(upload.groupId);
+				await this.clearUpload(upload.id);
+			}
+		}
+
+		// Clear associated groups
+		for (const groupId of groupIds) {
+			await this.groupStore.delete(groupId);
+			this.groupElements.delete(groupId);
+		}
+	}
+
+	handleOperationFailed(operation, fieldId) {
+		const uploadIds = operation.data instanceof FormData
+			? JSON.parse(operation.data.get('upload_ids') || '[]')
+			: operation.data?.upload_ids || [];
+
+		const status = operation.status === 'operation-failed-permanent' ? 'failed_permanent' : 'failed';
+
+		uploadIds.forEach(async uploadId => {
+			await this.updateUploadStatus(uploadId, status);
+		});
+
+		if (fieldId) this.updateFieldState(fieldId);
+	}
+
+	async handleOperationCancelled(fieldId) {
+		const uploads = this.getFieldUploads(fieldId);
+		const groups = this.getFieldGroups(fieldId);
+
+		for (const upload of uploads) {
+			await this.clearUpload(upload.id);
+		}
+
+		for (const group of groups) {
+			await this.groupStore.delete(group.id);
+			this.groupElements.delete(group.id);
+		}
+
+		this.updateFieldState(fieldId);
+		this.a11y.announce('Upload cancelled');
+	}
+
+	/*******************************************************************************
+	 * GROUP MANAGEMENT
+	 *******************************************************************************/
+
+	async createGroup(fieldId, groupId = null) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return null;
+
+		groupId = groupId || this.generateId('group');
+
+		// Create group data
+		const groupData = {
+			id: groupId,
+			fieldId,
+			pageUrl: window.location.href,
+			uploads: [],
+			fields: {}
+		};
+
+		await this.groupStore.save(groupData);
+
+		// Create DOM element
+		const element = this.createGroupElement(groupId, fieldId);
+		if (!element) return null;
+
+		const grid = element.querySelector('.item-grid.group');
+
+		// Store DOM references
+		this.groupElements.set(groupId, { element, grid, fieldId });
+
+		// Insert into DOM
+		if (fieldEl.ui.groups?.container && fieldEl.ui.groups.empty) {
+			fieldEl.ui.groups.container.insertBefore(element, fieldEl.ui.groups.empty);
+		} else if (fieldEl.ui.groups?.container) {
+			fieldEl.ui.groups.container.appendChild(element);
+		}
+
+		// Initialize sortable and selection
+		this.addGroupSelectionHandler(fieldId, groupId);
+		if (grid) this.createSortableForGrid(grid, fieldId, groupId);
+
+		return { id: groupId, element, grid };
+	}
+
+	createGroupElement(groupId, fieldId) {
+		const element = window.getTemplate('imageGroup');
+		if (!element) return null;
+
+		element.dataset.groupId = groupId;
+		element.dataset.fieldId = fieldId;
+
+		const fields = window.getTemplate('groupMetadata');
+		const fieldsContainer = element.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 fieldEl = this.fieldElements.get(fieldId);
+			if (fieldEl?.config.content) {
+				const summary = element.querySelector('summary');
+				if (summary) summary.textContent = fieldEl.config.content + ' Fields';
+			}
+		} else {
+			element.querySelector('details')?.remove();
+		}
+
+		const gridContainer = element.querySelector('.item-grid.group');
+		if (gridContainer) gridContainer.dataset.groupId = groupId;
+
+		return element;
+	}
+
+	async deleteGroup(groupId, moveUploadsToPreview = true) {
+		const groupEl = this.groupElements.get(groupId);
+		if (!groupEl) return;
+
+		const group = this.groupStore.get(groupId);
+		const fieldEl = this.fieldElements.get(groupEl.fieldId);
+
+		if (moveUploadsToPreview && group?.uploads) {
+			for (const uploadId of group.uploads) {
+				await this.removeFromGroup(uploadId);
+			}
+		}
+
+		// Remove from store
+		await this.groupStore.delete(groupId);
+
+		// Remove DOM element
+		groupEl.element?.remove();
+
+		// Cleanup
+		this.groupElements.delete(groupId);
+
+		const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
+		this.sortableInstances.get(sortableKey)?.destroy();
+		this.sortableInstances.delete(sortableKey);
+
+		this.a11y.announce('Group removed');
+	}
+
+	async addToGroup(uploadId, targetGrid, persist = true) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const groupId = targetGrid.dataset.groupId;
+		const group = this.groupStore.get(groupId);
+
+		// Remove from previous group if needed
+		if (upload.groupId && upload.groupId !== groupId) {
+			const oldGroup = this.groupStore.get(upload.groupId);
+			if (oldGroup) {
+				oldGroup.uploads = oldGroup.uploads.filter(id => id !== uploadId);
+				if (oldGroup.uploads.length === 0) {
+					await this.deleteGroup(upload.groupId, false);
+				} else {
+					await this.groupStore.save(oldGroup);
+				}
+			}
+		}
+
+		// Add to new group
+		upload.groupId = groupId;
+		if (group && !group.uploads.includes(uploadId)) {
+			group.uploads.push(uploadId);
+			await this.groupStore.save(group);
+		}
+
+		// Update upload
+		await this.uploadStore.save(upload);
+
+		// Move DOM element
+		targetGrid.appendChild(uploadEl.element);
+		uploadEl.location = targetGrid;
+
+		// Show featured radio
+		const featured = uploadEl.element.querySelector('[name="featured"]');
+		if (featured) {
+			featured.hidden = false;
+			featured.name = `${groupId}_featured`;
+		}
+
+		// Clear checkbox
+		const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
+		if (checkbox) checkbox.checked = false;
+
+		this.updateSortableState(targetGrid);
+	}
+
+	async removeFromGroup(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const fieldEl = this.fieldElements.get(upload.fieldId);
+		if (!fieldEl?.ui?.preview) return;
+
+		// Update group
+		if (upload.groupId) {
+			const group = this.groupStore.get(upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					await this.deleteGroup(upload.groupId, false);
+				} else {
+					await this.groupStore.save(group);
+				}
+			}
+			upload.groupId = null;
+			await this.uploadStore.save(upload);
+		}
+
+		// Move to 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.updateSortableState(fieldEl.ui.preview);
+	}
+
+	async removeUpload(fieldId, uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+
+		if (upload?.groupId) {
+			const group = this.groupStore.get(upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					await this.deleteGroup(upload.groupId, false);
+				} else {
+					await this.groupStore.save(group);
+				}
+			}
+		}
+
+		uploadEl?.element?.remove();
+		await this.clearUpload(uploadId);
+
+		this.updateFieldState(fieldId);
+		this.maybeLockUploads(fieldId);
+
+		this.selectionHandlers.get(fieldId)?.deselect(uploadId);
+		this.a11y.announce('Upload removed');
+	}
+
+	async handleGroupMetaChange(input) {
+		const groupEl = input.closest(this.selectors.groups.container);
+		if (!groupEl) return;
+
+		const groupId = groupEl.dataset.groupId;
+		const group = this.groupStore.get(groupId);
+		if (!group) return;
+
+		let name = input.name;
+		if (name.includes(groupId)) {
+			name = name.replace(`${groupId}_`, '').replace(`${groupId}[`, '').replace(']', '');
+		}
+
+		group.fields[name] = input.value;
+		await this.groupStore.save(group);
+	}
+
+	/*******************************************************************************
+	 * SORTABLE & DRAG/DROP
+	 *******************************************************************************/
+
+	initSortable(fieldId) {
+		if (!window.Sortable) return;
+
+		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+			Sortable.mount(new Sortable.MultiDrag());
+			Sortable._multiDragMounted = true;
+		}
+
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => {
+			const groupId = grid.classList.contains('group')
+				? grid.closest('.upload-group')?.dataset.groupId
+				: null;
+			this.createSortableForGrid(grid, fieldId, groupId);
+		});
+
+		// Empty group drop zone
+		const emptyGroup = fieldEl.element.querySelector('.empty-group');
+		if (emptyGroup && !emptyGroup.sortableInstance) {
+			emptyGroup.sortableInstance = new Sortable(emptyGroup, {
+				animation: 150,
+				draggable: '.item',
+				multiDrag: true,
+				selectedClass: 'selected-for-drag',
+				avoidImplicitDeselect: true,
+				group: { name: fieldId, pull: false, put: true },
+				ghostClass: 'sortable-ghost',
+				onEnd: (evt) => this.handleDrop(evt, fieldId)
+			});
+		}
+	}
+
+	createSortableForGrid(grid, fieldId, groupId = null) {
+		if (!grid || grid.sortableInstance) return;
+
+		const instance = new Sortable(grid, {
+			animation: 150,
+			draggable: '.item',
+			multiDrag: true,
+			selectedClass: 'selected-for-drag',
+			avoidImplicitDeselect: true,
+			group: { name: fieldId, pull: true, put: true },
+			ghostClass: 'sortable-ghost',
+			chosenClass: 'sortable-chosen',
+			dragClass: 'sortable-drag',
+			onEnd: (evt) => this.handleDrop(evt, fieldId),
+			onSelect: (evt) => this.syncCheckboxToSortable(evt.item, true),
+			onDeselect: (evt) => this.syncCheckboxToSortable(evt.item, false),
+			onAdd: (evt) => this.updateSortableState(evt.to),
+			onRemove: (evt) => this.updateSortableState(evt.from)
+		});
+
+		grid.sortableInstance = instance;
+
+		const gridId = groupId ? `${fieldId}-group-${groupId}` : `${fieldId}-preview`;
+		this.sortableInstances.set(gridId, instance);
+
+		return instance;
+	}
+
+	syncCheckboxToSortable(item, selected) {
+		const checkbox = item.querySelector('[name*="select-item"]');
+		if (checkbox && checkbox.checked !== selected) {
+			checkbox.checked = selected;
+			checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+		}
+	}
+
+	/**
+	 * Unified drop handler - consolidated from multiple methods
+	 */
+	async handleDrop(evt, fieldId) {
+		const target = evt.to;
+		const source = evt.from;
+		const items = evt.items?.length > 0 ? evt.items : [evt.item];
+		const uploadIds = items.map(item => item.dataset.uploadId);
+
+		// Same container = reorder only
+		if (target === source) {
+			this.handleReorder(evt);
+			return;
+		}
+
+		const targetType = this.getDropTargetType(target);
+
+		try {
+			switch (targetType) {
+				case 'empty-group':
+					const group = await this.createGroup(fieldId);
+					if (!group) throw new Error('Group creation failed');
+					for (const uploadId of uploadIds) {
+						await this.addToGroup(uploadId, group.grid, false);
+					}
+					break;
+
+				case 'preview':
+					for (const uploadId of uploadIds) {
+						await this.removeFromGroup(uploadId);
+					}
+					break;
+
+				case 'group':
+					for (const uploadId of uploadIds) {
+						await this.addToGroup(uploadId, target, false);
+					}
+					break;
+
+				default:
+					throw new Error('Unknown drop target');
+			}
+
+			this.finalizeDrop(fieldId, items.length, targetType);
+
+		} catch (error) {
+			console.error('Drop error:', error);
+			// Return items to preview as fallback
+			const fieldEl = this.fieldElements.get(fieldId);
+			if (fieldEl?.ui?.preview) {
+				items.forEach(item => fieldEl.ui.preview.appendChild(item));
+			}
+			this.a11y.announce('An error occurred. Items returned to preview.');
+		}
+
+		this.updateSortableState(target);
+		if (source !== target) this.updateSortableState(source);
+	}
+
+	finalizeDrop(fieldId, count, targetType) {
+		const messages = {
+			'empty-group': count > 1 ? `Created group with ${count} items` : 'Created group with item',
+			'preview': count > 1 ? `Moved ${count} items to preview` : 'Moved item to preview',
+			'group': count > 1 ? `Moved ${count} items to group` : 'Moved item to group'
+		};
+
+		this.a11y.announce(messages[targetType] || 'Items moved');
+		this.selectionHandlers.get(fieldId)?.clearSelection();
+	}
+
+	getDropTargetType(target) {
+		if (target.classList.contains('empty-group')) return 'empty-group';
+		if (target.classList.contains('preview')) return 'preview';
+		if (target.classList.contains('group')) return 'group';
+		return 'unknown';
+	}
+
+	handleReorder(evt) {
+		const grid = evt.to;
+		const fieldWrapper = grid.closest('.field, .upload');
+		if (!fieldWrapper) return;
+
+		const items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
+			.map(el => el.dataset.uploadId)
+			.filter(Boolean);
+
+		const hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+		if (hiddenInput && items.length > 0) {
+			hiddenInput.value = items.join(',');
+		}
+
+		// Update group order in store
+		const groupId = grid.dataset.groupId;
+		if (groupId) {
+			const group = this.groupStore.get(groupId);
+			if (group) {
+				group.uploads = items;
+				this.groupStore.save(group);
+			}
+		}
+
+		this.a11y.announce('Item reordered');
+
+		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
+			detail: { from: evt.from, to: evt.to, items },
+			bubbles: true
+		}));
+	}
+
+	updateSortableState(grid) {
+		const sortable = grid?.sortableInstance;
+		if (sortable) sortable.option('disabled', false);
+	}
+
+	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));
+	}
+
+	syncSortableSelection(fieldId, selectedItems) {
+		this.sortableInstances.forEach((instance, key) => {
+			if (key.startsWith(fieldId)) {
+				instance.el.querySelectorAll('.item').forEach(item => {
+					const uploadId = item.dataset.uploadId;
+					if (selectedItems.has(uploadId)) {
+						Sortable.utils.select(item);
+					} else {
+						Sortable.utils.deselect(item);
+					}
+				});
+			}
+		});
+	}
+
+	/*******************************************************************************
+	 * EVENT LISTENERS
+	 *******************************************************************************/
+
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
+		this.dragEnterHandler = this.handleDragEnter.bind(this);
+		this.dragLeaveHandler = this.handleDragLeave.bind(this);
+		this.dragOverHandler = this.handleDragOver.bind(this);
+		this.dropHandler = this.handleExternalDrop.bind(this);
+
+		document.addEventListener('click', this.clickHandler);
+		document.addEventListener('change', this.changeHandler);
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('drop', this.dropHandler);
+	}
+
+	handleClick(e) {
+		// Trigger file input
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone && !e.target.matches('input, button, a')) {
+			dropZone.querySelector(this.selectors.field.input)?.click();
+		}
+
+		// Action buttons
+		const actionButton = e.target.closest('[data-action]');
+		if (actionButton) this.handleAction(actionButton);
+	}
+
+	handleChange(e) {
+		const fieldId = this.getFieldIdFromElement(e.target);
+		if (!fieldId) return;
+
+		// File input
+		if (e.target.matches(this.selectors.field.input)) {
+			const files = Array.from(e.target.files);
+			if (files.length > 0) this.processFiles(fieldId, files);
+			return;
+		}
+
+		// Meta field changes
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.config.autoUpload) return;
+
+		if (fieldEl.config.destination === 'post_group') {
+			this.handleGroupMetaChange(e.target);
+		} else {
+			this.queueUploadMeta(e);
+		}
+	}
+
+	handleDragEnter(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
+	}
+
+	handleDragLeave(e) {
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone && !dropZone.contains(e.relatedTarget)) {
+			dropZone.classList.remove('dragover');
+		}
+	}
+
+	handleDragOver(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'copy';
+		}
+	}
+
+	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`);
+		}
+	}
+
+	/*******************************************************************************
+	 * ACTION HANDLERS
+	 *******************************************************************************/
+
+	handleAction(button) {
+		const action = button.dataset.action;
+		const fieldId = this.getFieldIdFromElement(button);
+
+		switch (action) {
+			case 'add-to-group':
+				this.handleAddToGroup(fieldId);
+				break;
+			case 'delete-group':
+				this.handleDeleteGroup(button);
+				break;
+			case 'delete-upload':
+			case 'remove-from-group':
+				this.handleRemoveItem(button);
+				break;
+			case 'upload':
+				this.handleSubmitUploads(fieldId);
+				break;
+			case 'restore':
+				this.handleRestoreSelected();
+				break;
+			case 'restore-all':
+				this.handleRestoreAll();
+				break;
+			case 'clear-cache':
+				this.handleClearCache();
+				break;
+		}
+	}
+
+	async handleAddToGroup(fieldId) {
+		const selected = this.selected.get(fieldId);
+
+		if (!selected || selected.size === 0) {
+			await this.createGroup(fieldId);
+		} else {
+			const group = await this.createGroup(fieldId);
+			if (!group) return;
+
+			for (const uploadId of selected) {
+				await this.addToGroup(uploadId, group.grid, false);
+			}
+
+			this.selectionHandlers.get(fieldId)?.clearSelection();
+			this.a11y.announce(`Created group with ${selected.size} items`);
+		}
+	}
+
+	async handleDeleteGroup(button) {
+		const group = button.closest(this.selectors.groups.container);
+		if (!group) return;
+
+		const groupId = group.dataset.groupId;
+
+		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+			return;
+		}
+
+		await this.deleteGroup(groupId, true);
+	}
+
+	async handleRemoveItem(button) {
+		const item = button.closest(this.selectors.items.item);
+		if (!item) return;
+
+		if (!confirm('Remove this item?')) return;
+
+		const uploadId = item.dataset.uploadId;
+		const fieldId = this.getFieldIdFromElement(item);
+
+		await this.removeUpload(fieldId, uploadId);
+	}
+
+	handleSubmitUploads(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		fieldEl.element.closest('details')?.removeAttribute('open');
+		document.body.classList.add('uploading');
+
+		if (fieldEl.config.destination === 'post_group') {
+			this.submitGroupedUploads(fieldId);
+		} else {
+			this.queueUpload(fieldId);
+		}
+	}
+
+	/*******************************************************************************
+	 * SELECTION MANAGEMENT
+	 *******************************************************************************/
+
+	addFieldSelectionHandler(fieldId) {
+		if (this.selectionHandlers.has(fieldId)) return;
+
+		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) => {
+			if (['item-selected', 'item-deselected', 'range-selected'].includes(event)) {
+				this.syncSortableSelection(fieldId, data.selectedItems);
+				this.selected.set(fieldId, data.selectedItems);
+			}
+		});
+
+		this.selectionHandlers.set(fieldId, handler);
+	}
+
+	addGroupSelectionHandler(fieldId, groupId) {
+		const key = `${fieldId}_${groupId}`;
+		if (this.selectionHandlers.has(key)) return;
+
+		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) => {
+			if (['item-selected', 'item-deselected', 'range-selected'].includes(event)) {
+				this.selected.set(fieldId, data.selectedItems);
+			}
+		});
+
+		this.selectionHandlers.set(key, handler);
+	}
+
+	/*******************************************************************************
+	 * UI UPDATES
+	 *******************************************************************************/
+
+	updateFieldState(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const uploadCount = this.getFieldUploadCount(fieldId);
+		const groupCount = this.getFieldGroups(fieldId).length;
+
+		fieldEl.element.dataset.hasUploads = uploadCount > 0;
+		fieldEl.element.dataset.uploadCount = uploadCount;
+		fieldEl.element.dataset.hasGroups = groupCount > 0;
+
+		if (fieldEl.ui.preview) {
+			fieldEl.ui.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
+		}
+	}
+
+	updateFieldProgress(fieldId, current, total, message) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const progress = fieldEl?.ui?.progress;
+		if (!progress?.container) return;
+
+		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.container.hidden = current === total;
+	}
+
+	updateFieldStatus(fieldId, status) {
+		// Status is now derived from uploads, no field-level status needed
+	}
+
+	async updateUploadStatus(uploadId, status) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		upload.status = status;
+		await 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;
+
+		// Update class
+		uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
+		uploadEl.element.classList.add(`status-${upload.status}`);
+
+		// Update progress
+		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);
+		const progress = uploadEl?.element?.querySelector('.progress');
+		if (!progress) return;
+
+		if (show) {
+			progress.style.removeProperty('animation');
+			progress.hidden = false;
+		} else {
+			progress.style.animation = 'fadeOut var(--transition-base)';
+			setTimeout(() => { progress.hidden = true; }, 300);
+		}
+	}
+
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		const progress = uploadEl?.element?.querySelector('.progress');
+		if (!progress) return;
+
+		const fill = progress.querySelector('.fill');
+		const details = progress.querySelector('.details');
+		const icon = progress.querySelector('.icon');
+
+		if (fill) fill.style.width = `${percent}%`;
+		if (status && details) details.textContent = this.statusMapping[status] || status;
+		if (status && icon) icon.innerHTML = this.getStatusIcon(status).outerHTML;
+	}
+
+	maybeLockUploads(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.ui?.dropZone) return;
+
+		const uploadCount = this.getFieldUploadCount(fieldId);
+		const maxFiles = fieldEl.config.destination === 'post_group' ? 20 : (fieldEl.config.maxFiles || 999);
+
+		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
+		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+
+		if (fieldEl.config.destination === 'post_group' && uploadCount >= maxFiles) {
+			this.a11y.announce('Maximum of 20 uploads reached.');
+		}
+	}
+
+	/*******************************************************************************
+	 * CLEANUP
+	 *******************************************************************************/
+
+	async clearUpload(uploadId) {
+		const uploadEl = this.uploadElements.get(uploadId);
+
+		if (uploadEl) {
+			this.revokePreviewUrl(uploadEl.preview);
+			if (uploadEl.element?.dataset.previewUrl) {
+				this.revokePreviewUrl(uploadEl.element.dataset.previewUrl);
+			}
+		}
+
+		this.uploadElements.delete(uploadId);
+		await this.uploadStore.delete(uploadId);
+	}
+
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		this.previewUrls.add(url);
+		return url;
+	}
+
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls.delete(url);
+		}
+	}
+
+	cleanupAllPreviewUrls() {
+		this.previewUrls.forEach(url => {
+			try { URL.revokeObjectURL(url); } catch (e) {}
+		});
+		this.previewUrls.clear();
+	}
+
+	/*******************************************************************************
+	 * RECOVERY & RESTORATION
+	 *******************************************************************************/
+
+	async showRecoveryNotification(byField) {
+		let totalUploads = 0;
+		let totalGroups = 0;
+
+		byField.forEach(field => {
+			totalUploads += field.uploads.length;
+			totalGroups += field.groups.size;
+		});
+
+		const notification = window.getTemplate('restoreNotification');
+		if (!notification) return;
+
+		const message = totalGroups > 0
+			? `${totalGroups} group${totalGroups > 1 ? 's' : ''} with ${totalUploads} upload${totalUploads > 1 ? 's' : ''} can be restored.`
+			: `${totalUploads} upload${totalUploads > 1 ? 's' : ''} can be recovered.`;
+
+		const detailsEl = notification.querySelector('.restore-details');
+		if (detailsEl) detailsEl.textContent = message;
+
+		// Build preview
+		for (const [fieldId, fieldData] of byField) {
+			const fieldTemplate = window.getTemplate('restoreField');
+			if (!fieldTemplate) continue;
+
+			const titleEl = fieldTemplate.querySelector('h3');
+			if (titleEl) titleEl.textContent = fieldId;
+
+			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+			for (const upload of fieldData.uploads) {
+				const file = await this.getBlobData(upload.id);
+				if (!file) continue;
+
+				const uploadItem = this.createUploadElement({
+					id: upload.id,
+					preview: this.createPreviewUrl(file),
+					meta: upload.meta,
+					subtype: this.getSubtypeFromMime(file.type)
+				}, false);
+
+				uploadItem.dataset.fieldId = fieldId;
+				itemGrid?.appendChild(uploadItem);
+			}
+
+			notification.querySelector('.wrap')?.appendChild(fieldTemplate);
+		}
+
+		document.querySelector('.field.upload')?.appendChild(notification);
+
+		const dialog = document.querySelector('dialog.restore-uploads');
+		if (dialog) {
+			this.restoreModal = new window.jvbModal(dialog);
+			this.restoreSelection = new window.jvbHandleSelection({
+				container: dialog,
+				ui: {
+					selectAll: dialog.querySelector('#select-all-restore'),
+					count: dialog.querySelector('.selection-count')
+				}
+			});
+			this.restoreModal.handleOpen();
+		}
+	}
+
+	async handleRestoreSelected() {
+		const dialog = document.querySelector('dialog.restore-uploads');
+		if (!dialog) return;
+
+		const selected = [];
+		dialog.querySelectorAll('[type=checkbox]:checked').forEach(checkbox => {
+			const item = checkbox.closest('.item');
+			if (item) {
+				selected.push({
+					uploadId: item.dataset.uploadId,
+					fieldId: item.dataset.fieldId
+				});
+			}
+		});
+
+		if (selected.length > 0) {
+			await this.restoreUploads(selected);
+		}
+
+		this.cleanupRestoreModal();
+	}
+
+	async handleRestoreAll() {
+		const dialog = document.querySelector('dialog.restore-uploads');
+		if (!dialog) return;
+
+		const all = [];
+		dialog.querySelectorAll('.item.upload').forEach(item => {
+			all.push({
+				uploadId: item.dataset.uploadId,
+				fieldId: item.dataset.fieldId
+			});
+		});
+
+		await this.restoreUploads(all);
+		this.cleanupRestoreModal();
+	}
+
+	async restoreUploads(items) {
+		const byField = new Map();
+
+		items.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		for (const [fieldId, uploadIds] of byField) {
+			await this.restoreFieldUploads(fieldId, uploadIds);
+		}
+	}
+
+	async restoreFieldUploads(fieldId, uploadIds) {
+		// Find or register field element
+		let fieldEl = this.fieldElements.get(fieldId);
+
+		if (!fieldEl) {
+			// Try to find by data attribute
+			const element = document.querySelector(`[data-uploader="${fieldId}"]`);
+			if (element) {
+				this.registerUploader(element);
+				fieldEl = this.fieldElements.get(fieldId);
+			}
+		}
+
+		if (!fieldEl) {
+			console.warn(`Field ${fieldId} not found for restoration`);
+			return;
+		}
+
+		// Show upload UI
+		if (fieldEl.ui.dropZone) fieldEl.ui.dropZone.hidden = true;
+		if (fieldEl.ui.groups?.display) fieldEl.ui.groups.display.hidden = false;
+
+		// Restore groups first
+		const groups = this.getFieldGroups(fieldId);
+		for (const group of groups) {
+			await this.restoreGroup(fieldId, group);
+		}
+
+		// Restore uploads
+		for (const uploadId of uploadIds) {
+			const upload = this.uploadStore.get(uploadId);
+			if (upload) {
+				await this.restoreUploadElement(fieldId, upload);
+			}
+		}
+
+		this.updateFieldState(fieldId);
+		this.refreshSortable(fieldId);
+		this.maybeLockUploads(fieldId);
+
+		// Auto-upload if configured
+		if (fieldEl.config.autoUpload && fieldEl.config.destination !== 'post_group') {
+			await this.queueUpload(fieldId);
+		}
+	}
+
+	async restoreGroup(fieldId, groupData) {
+		const group = await this.createGroup(fieldId, groupData.id);
+		if (!group) return;
+
+		// Restore field values
+		if (groupData.fields) {
+			const titleInput = group.element.querySelector('[name*="post_title"]');
+			const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
+
+			if (titleInput && groupData.fields.post_title) {
+				titleInput.value = groupData.fields.post_title;
+			}
+			if (excerptInput && groupData.fields.post_excerpt) {
+				excerptInput.value = groupData.fields.post_excerpt;
+			}
+		}
+	}
+
+	async restoreUploadElement(fieldId, upload) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const file = await this.getBlobData(upload.id);
+		if (!file) return;
+
+		const preview = this.createPreviewUrl(file);
+		const element = this.createUploadElement({
+			id: upload.id,
+			preview,
+			meta: upload.meta,
+			subtype: this.getSubtypeFromMime(file.type)
+		}, fieldEl.config.destination === 'post_group');
+
+		// Determine location
+		let location;
+		if (upload.groupId) {
+			const groupEl = this.groupElements.get(upload.groupId);
+			location = groupEl?.grid || fieldEl.ui.preview;
+		} else {
+			location = fieldEl.ui.preview;
+		}
+
+		location.appendChild(element);
+		this.uploadElements.set(upload.id, { element, preview, location });
+
+		// Update upload status
+		upload.status = 'processed';
+		await this.uploadStore.save(upload);
+	}
+
+	handleClearCache() {
+		if (!confirm('Discard these uploads?')) return;
+		this.cleanupStoredData();
+		this.cleanupRestoreModal();
+	}
+
+	async cleanupStoredData() {
+		await this.uploadStore.clear();
+		await this.groupStore.clear();
+	}
+
+	cleanupRestoreModal() {
+		if (this.restoreModal) {
+			this.restoreModal.handleClose();
+			this.restoreSelection?.destroy();
+			this.restoreModal.destroy();
+			this.restoreModal.modal.remove();
+			this.restoreModal = null;
+			this.restoreSelection = null;
+		}
+	}
+
+	/*******************************************************************************
+	 * HELPER METHODS
+	 *******************************************************************************/
+
+	generateId(prefix) {
+		return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+	}
+
+	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}`;
+	}
+
+	getFieldIdFromElement(el) {
+		const field = el.closest(this.selectors.field.field);
+		return field?.dataset.uploader || null;
+	}
+
+	getUploadIdFromElement(el) {
+		const item = el.closest(this.selectors.items.item);
+		return item?.dataset.uploadId || null;
+	}
+
+	getSubtypeFromMime(mimeType) {
+		if (mimeType.startsWith('image/')) return 'image';
+		if (mimeType.startsWith('video/')) return 'video';
+		return 'document';
+	}
+
+	getStatusIcon(status) {
+		return window.getIcon(this.queue.icons[status]);
+	}
+
+	getStatusProgress(status) {
+		const progress = {
+			'processing': 28,
+			'queued': 50,
+			'uploading': 66,
+			'pending': 75,
+			'server_processing': 89,
+			'completed': 100
+		};
+		return progress[status] || 0;
+	}
+
+	formatBytes(bytes, decimals = 2) {
+		if (bytes === 0) return '0 Bytes';
+		const k = 1024;
+		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+		const i = Math.floor(Math.log(bytes) / Math.log(k));
+		return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
+	}
+
+	createUploadElement(upload, draggable = false) {
+		const element = window.getTemplate('uploadItem');
+		if (!element) return null;
+
+		element.dataset.uploadId = upload.id;
+		element.dataset.subtype = upload.subtype || 'image';
+
+		const featured = element.querySelector('[name="featured"]');
+		const img = element.querySelector('img');
+		const video = element.querySelector('video');
+		const preview = element.querySelector('label > span');
+		const details = element.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 ext = 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[ext] || 'file');
+				if (preview) {
+					preview.innerText = fileName;
+					preview.prepend(icon);
+				}
+				img?.remove();
+				video?.remove();
+				break;
+		}
+
+		if (details) {
+			const template = window.getTemplate('uploadMeta');
+			if (template) details.append(template);
+		}
+
+		element.draggable = draggable;
+
+		// Update input IDs
+		element.querySelectorAll('input').forEach(input => {
+			const id = input.id;
+			if (id) {
+				const newId = id + upload.id;
+				const label = input.parentNode.querySelector(`label[for="${id}"]`);
+				input.id = newId;
+				if (label) label.htmlFor = newId;
+			}
+		});
+
+		return element;
+	}
+
+	/**
+	 * Get all files for a form
+	 */
+	async getFilesForForm(formElement) {
+		const uploadFields = formElement.querySelectorAll('[data-upload-field]');
+		const allFiles = [];
+
+		for (const field of uploadFields) {
+			const fieldId = this.determineFieldId(field);
+			const files = await this.getFilesForField(fieldId);
+			allFiles.push(...files);
+		}
+
+		return allFiles;
+	}
+
+	/**
+	 * Get all files for a field
+	 */
+	async getFilesForField(fieldId) {
+		const uploads = this.getFieldUploads(fieldId);
+		const files = [];
+
+		for (const upload of uploads) {
+			const file = await this.getBlobData(upload.id);
+			if (file) {
+				files.push({
+					file,
+					uploadId: upload.id,
+					fieldName: this.fieldElements.get(fieldId)?.config.name,
+					meta: upload.meta || {}
+				});
+			}
+		}
+
+		return files;
+	}
+
+	/*******************************************************************************
+	 * EVENT SYSTEM
+	 *******************************************************************************/
+
+	subscribe(callback) {
+		this.subscribers.add(callback);
+		return () => this.subscribers.delete(callback);
+	}
+
+	notify(event, data = {}) {
+		this.subscribers.forEach(cb => {
+			try { cb(event, data); } catch (e) { console.error('Subscriber error:', e); }
+		});
+	}
+
+	/*******************************************************************************
+	 * DESTROY
+	 *******************************************************************************/
+
+	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.selectionHandlers.forEach(handler => handler.destroy());
+		this.selectionHandlers.clear();
+
+		this.cleanupAllPreviewUrls();
+
+		this.sortableInstances.forEach(instance => instance?.destroy?.());
+		this.sortableInstances.clear();
+
+		this.uploadElements.clear();
+		this.fieldElements.clear();
+		this.groupElements.clear();
+		this.selected.clear();
+		this.subscribers.clear();
+	}
+}
+
+// Initialize
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbUploads = new UploadManager();
+		}
+	});
+});
diff --git a/assets/js/concise/UploadManagerOld.js b/assets/js/concise/UploadManagerOld.js
new file mode 100644
index 0000000..3f1ae58
--- /dev/null
+++ b/assets/js/concise/UploadManagerOld.js
@@ -0,0 +1,3141 @@
+/**
+ * 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
+		this.queue = window.jvbQueue;
+		this.a11y = window.jvbA11y;
+		this.error = window.jvbError;
+		this.fieldStoreReady = false;
+		this.uploadStoreReady = false;
+		this.hasCheckedForUploads = false;
+		const {fields, uploads} = window.jvbStore.register(
+			'uploads',
+			[
+				{
+					storeName: 'fields',
+					keyPath: 'id',
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'timestamp', keyPath: 'timestamp' },
+						{ name: 'content', keyPath: 'content' },
+						{ name: 'itemId', keyPath: 'itemId' },
+						{ name: 'status', keyPath: 'status' }
+					],
+					TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
+					delayFetch: true
+				},
+				{
+					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
+				}
+			]
+		);
+		this.fieldStore = fields;
+		this.uploadStore = uploads;
+
+		window.jvbUploadBlobs = this.uploadStore;
+
+		// Subscribe to store events
+		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
+		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+
+		// 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 }
+
+		// Selection and UI state
+		this.selected = new Map();
+		this.selectionHandlers = new Map();
+		this.previewUrls = new Set();
+		this.sortableInstances = new Map();
+
+		// Worker for image processing
+		this.initWorker();
+
+		// Notification subscribers
+		this.subscribers = new Set();
+
+		// Selectors
+		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.init();
+	}
+
+	async init() {
+		// this.initializeFields();
+		this.initListeners();
+
+		// Queue integration - handle completion/failure
+		this.queue.subscribe((event, operation) => {
+			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
+				return;
+			}
+
+			const fieldId = operation.data instanceof FormData
+				? operation.data.get('fieldId')
+				: operation.data?.fieldId;
+
+			switch(event) {
+				case 'cancel-operation':
+					if (fieldId) this.handleOperationCancelled(fieldId);
+					break;
+				case 'operation-status':
+					if (fieldId) this.updateFieldStatus(fieldId, operation.status);
+					break;
+				case 'operation-complete':
+					this.handleOperationComplete(operation, fieldId);
+					break;
+				case 'operation-failed':
+				case 'operation-failed-permanent':
+					this.handleOperationFailed(operation, fieldId);
+					break;
+			}
+		});
+
+		window.addEventListener('beforeunload', () => {
+			this.cleanupAllPreviewUrls();
+		});
+	}
+
+	initWorker() {
+		this.worker = {
+			worker: null,
+			timeout: null,
+			tasks: new Map(),
+			restart: { count: 0, max: 3 },
+			settings: {
+				timeout: 10000,
+				batchSize: 1,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
+			}
+		};
+	}
+
+	/*******************************************************************************
+	 * FIELD MANAGEMENT
+	 *******************************************************************************/
+	scanFields(container, autoUpload) {
+		console.log(autoUpload, 'autoUpload');
+		const fields = container.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
+	}
+
+	registerUploader(uploader, autoUpload) {
+		const fieldId = this.determineFieldId(uploader);
+		const config = this.extractFieldConfig(uploader, autoUpload);
+		const ui = this.buildFieldUI(uploader);
+
+		console.log(config, 'registering with config');
+		// Store field data with Sets for runtime
+		const fieldData = {
+			id: fieldId,
+			config: config,
+			uploads: new Set(),
+			groups: [],
+			state: 'ready',
+			timestamp: Date.now()
+		};
+
+		// 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.type !== 'single') {
+			this.initSortable(fieldId);
+		}
+
+		return fieldId;
+	}
+
+	extractFieldConfig(fieldElement, autoUpload) {
+		return {
+			autoUpload: autoUpload,
+			destination: fieldElement.dataset.destination || 'meta',
+			content: fieldElement.dataset.content || null,
+			mode: fieldElement.dataset.mode || 'direct',
+			type: fieldElement.dataset.type || 'single',
+			name: fieldElement.dataset.field,
+			itemID: fieldElement.dataset.itemId || 0,
+			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+			subtype: fieldElement.dataset.subtype || 'image'
+		};
+	}
+
+	buildFieldUI(fieldElement) {
+		let UI = {
+			field: fieldElement,
+			input: fieldElement.querySelector(this.selectors.field.input),
+			dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
+			preview: fieldElement.querySelector(this.selectors.field.preview),
+			progress: {
+				progress: fieldElement.querySelector(this.selectors.field.progress),
+				bar: fieldElement.querySelector('.bar'),
+				fill: fieldElement.querySelector('.fill'),
+				details: fieldElement.querySelector('.details'),
+				text: fieldElement.querySelector('.details .text'),
+				count: fieldElement.querySelector('.details .count')
+			}
+		};
+
+		let display = fieldElement.querySelector('.group-display');
+		if (display) {
+			UI.groups = {
+				display: display,
+				container: fieldElement.querySelector('.item-grid.groups'),
+				empty: fieldElement.querySelector('.empty-group'),
+				groups: new Map()
+			};
+		}
+
+		return UI;
+	}
+
+	/*******************************************************************************
+	 * SORTABLE INITIALIZATION
+	 *******************************************************************************/
+	initSortable(fieldId) {
+		if (!window.Sortable) return;
+
+		// Mount MultiDrag plugin once
+		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+			Sortable.mount(new Sortable.MultiDrag());
+			Sortable._multiDragMounted = true;
+		}
+
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		// Initialize sortable on all existing grids
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => {
+			const groupId = grid.classList.contains('group')
+				? grid.closest('.upload-group')?.dataset.groupId
+				: null;
+			this.createSortableForGrid(grid, fieldId, groupId);
+		});
+
+		// Special handler for empty-group
+		const emptyGroup = fieldEl.element.querySelector('.empty-group');
+		if (emptyGroup && !emptyGroup.sortableInstance) {
+			emptyGroup.sortableInstance = new Sortable(emptyGroup, {
+				animation: 150,
+				draggable: '.item',
+				multiDrag: true,
+				selectedClass: 'selected-for-drag',
+				avoidImplicitDeselect: true,
+				group: { name: fieldId, pull: false, put: true },
+				ghostClass: 'sortable-ghost',
+				chosenClass: 'sortable-chosen',
+				dragClass: 'sortable-drag',
+				onEnd: (evt) => this.handleDrop(evt, fieldId)
+			});
+		}
+	}
+
+	syncSortableSelection(fieldId, selectedItems) {
+		// Update Sortable's selection state to match checkboxes
+		this.sortableInstances.forEach((instance, key) => {
+			if (key.startsWith(fieldId)) {
+				const grid = instance.el;
+				const items = grid.querySelectorAll('.item');
+
+				items.forEach(item => {
+					const uploadId = item.dataset.uploadId;
+					const shouldBeSelected = selectedItems.has(uploadId);
+
+					if (shouldBeSelected) {
+						Sortable.utils.select(item);
+					} else {
+						Sortable.utils.deselect(item);
+					}
+				});
+			}
+		});
+	}
+
+	handleDrop(evt, fieldId) {
+		const dropTarget = evt.to;
+		const sourceTarget = evt.from;
+		const items = evt.items?.length > 0 ? evt.items : [evt.item];
+		const uploadIds = items.map(item => item.dataset.uploadId);
+
+		// Determine drop target type
+		const targetType = this.getDropTargetType(dropTarget);
+
+		switch (targetType) {
+			case 'empty-group':
+				this.handleDropToEmptyGroup(items, uploadIds, fieldId);
+				break;
+
+			case 'preview':
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
+
+			case 'group':
+				this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
+				break;
+			default:
+				// Fallback: return to preview
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
+		}
+
+		// Update UI
+		this.updateSortableState(dropTarget);
+		if (sourceTarget !== dropTarget) {
+			this.updateSortableState(sourceTarget);
+		}
+	}
+
+	/**
+	 * Determine what type of drop target this is
+	 */
+	getDropTargetType(target) {
+		if (target.classList.contains('empty-group')) {
+			return 'empty-group';
+		}
+
+		if (target.classList.contains('preview')) {
+			return 'preview';
+		}
+
+		if (target.classList.contains('group')) {
+			return 'group';
+		}
+
+		return 'unknown';
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
+		try {
+			// If same container, it's just a reorder
+			if (dropTarget === sourceTarget) {
+				this.handleReorder({ to: dropTarget, items: items });
+				return;
+			}
+
+			// Moving to different group
+			uploadIds.forEach(uploadId => {
+				this.addToGroup(uploadId, dropTarget, false);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to group`
+				: 'Moved item to group';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		}  catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop to preview: remove from groups
+	 */
+	handleDropToPreview(items, uploadIds, fieldId) {
+		try {
+			uploadIds.forEach(uploadId => {
+				this.removeFromGroup(uploadId);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to preview`
+				: 'Moved item to preview';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop errors consistently
+	 */
+	handleDropError(items, fieldId, error, message = 'An error occurred') {
+		console.error('Drop error:', error);
+
+		// Return items to preview as fallback
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (fieldEl?.ui?.preview) {
+			items.forEach(item => fieldEl.ui.preview.appendChild(item));
+		}
+
+		this.a11y.announce(`${message}. Items returned to preview.`);
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToEmptyGroup(items, uploadIds, fieldId) {
+		try {
+			const group = this.createGroup(fieldId);
+			if (!group) {
+				this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
+				return;
+			}
+
+			// Move items to new group
+			items.forEach((item, index) => {
+				group.grid.appendChild(item);
+				this.addToGroup(uploadIds[index], group.grid, false);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Created group with ${items.length} items`
+				: 'Created group with item';
+			this.a11y.announce(message);
+
+			// Clear selection after move
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * 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', false);
+	}
+
+	/**
+	 * 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;
+
+		// Get current order from DOM
+		let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
+			.map(upload => upload.dataset.uploadId)
+			.filter(id => id);
+
+
+		// Update hidden input (for form submission)
+		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+		if (hiddenInput && items.length > 0) {
+			hiddenInput.value = items.join(',');
+		}
+
+		// Update fieldState with new order
+		const fieldId = this.getFieldIdFromElement(grid);
+		if (fieldId) {
+			const fieldData = this.getFieldData(fieldId);
+
+			// If reordering within a group, update that group's uploads array
+			if (grid.classList.contains('group')) {
+				const groupId = grid.dataset.groupId;
+				const group = fieldData?.groups?.find(g => g.id === groupId);
+				if (group) {
+					group.uploads = items; // Update order
+				}
+			}
+			// If reordering in preview, the order is implicit by DOM position
+			// (we don't store preview order separately)
+
+			this.schedulePersistance(fieldId);
+		}
+
+		this.a11y.announce('Item reordered');
+
+		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
+			detail: {
+				from: evt.from,
+				to: evt.to,
+				oldIndex: evt.oldIndex,
+				newIndex: evt.newIndex,
+				items: items
+			},
+			bubbles: true
+		}));
+	}
+
+	/*******************************************************************************
+	 * 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;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
+	}
+
+	handleExternalDragOver(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'copy';
+		}
+	}
+
+	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`);
+		}
+	}
+
+	/*******************************************************************************
+	 * CLICK & CHANGE HANDLERS
+	 *******************************************************************************/
+
+	handleClick(e) {
+		// 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);
+			if (dropZone && !e.target.matches('input, button, a')) {
+				const input = dropZone.querySelector(this.selectors.field.input);
+				input?.click();
+			}
+		}
+
+		// Group actions
+		const actionButton = e.target.closest('[data-action]');
+		if (actionButton) {
+			this.handleAction(actionButton);
+		}
+	}
+
+	handleChange(e) {
+		const fieldId = this.getFieldIdFromElement(e.target);
+
+		// File input change
+		if (e.target.matches(this.selectors.field.input)) {
+			const files = Array.from(e.target.files);
+			if (files.length > 0 && fieldId) {
+				this.processFiles(fieldId, files);
+			}
+		}
+
+		// Meta field changes
+		if (fieldId) {
+			const fieldData = this.getFieldData(fieldId);
+			if (!fieldData.config.autoUpload) {
+				return;
+			}
+			if (fieldData?.config.destination === 'post_group') {
+				this.handleGroupMetaChange(e.target);
+			} else {
+				this.queueUploadMeta(e);
+			}
+		}
+	}
+
+	/*******************************************************************************
+	 * FILE PROCESSING
+	 *******************************************************************************/
+
+	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 (fieldEl.ui.groups?.display) {
+			fieldEl.ui.groups.display.hidden = false;
+		}
+
+		const totalFiles = files.length;
+		let processedCount = 0;
+
+		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+		const processPromises = Array.from(files).map(async (file) => {
+			try {
+				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+				// Create initial upload data
+				const uploadData = {
+					id: uploadId,
+					attachmentId: null,
+					fieldId: fieldId,
+					status: 'local_processing',
+					groupId: null,
+					meta: {
+						originalName: file.name,
+						size: file.size,
+						type: file.type
+					}
+				};
+
+				// Save initial data
+				await this.uploadStore.save(uploadData);
+
+				// Process file
+				const preview = this.createPreviewUrl(file);
+				const processedFile = file.type.startsWith('image/')
+					? await this.processImage(file, fieldData.config.subtype)
+					: file;
+
+				// Show progress
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+				// Store blob data (this updates the existing uploadData)
+				await this.saveBlobData(uploadId, processedFile || file);
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				const element = this.createUploadElement({
+					id: uploadId,
+					preview: preview,
+					meta: uploadData.meta,
+					subtype: subtype
+				}, fieldData.config.destination === 'post_group');
+
+				// Add to preview grid
+				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
+					});
+				}
+
+				// Update status (gets existing data with blobData intact)
+				const storedUpload = this.uploadStore.get(uploadId);
+				if (storedUpload) {
+					storedUpload.status = 'processed';
+					await this.uploadStore.save(storedUpload);
+				}
+
+				// 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');
+
+				// Fade out progress
+				setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
+
+				return uploadId;
+
+			} catch (error) {
+				console.error('Error processing file:', file.name, error);
+				processedCount++;
+				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+				return null;
+			}
+		});
+
+		await Promise.all(processPromises);
+
+		this.updateFieldState(fieldId);
+		this.refreshSortable(fieldId);
+
+		// Queue for upload if in direct mode
+		if (fieldData.config.autoUpload && fieldData.config.destination !== 'post_group') {
+			await this.queueUpload(fieldId);
+			this.maybeLockUploads(fieldId);
+		}
+	}
+
+	/*******************************************************************************
+	 * IMAGE PROCESSING
+	 *******************************************************************************/
+
+	async processImage(file, uploadId) {
+		const timeout = this.worker.settings.timeout;
+
+		return new Promise((resolve, reject) => {
+			let timeoutId;
+			let taskCompleted = false;
+
+			timeoutId = setTimeout(() => {
+				if (!taskCompleted) {
+					taskCompleted = true;
+					this.worker.tasks.delete(uploadId);
+					if (this.worker.settings.restartAfterTimeout) {
+						this.restartCompressionWorker();
+					}
+					reject(new Error(`Processing timeout for ${file.name}`));
+				}
+			}, timeout);
+
+			this.worker.tasks.set(uploadId, { file, timeoutId });
+
+			this.handleProcess(file, uploadId)
+				.then(result => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						resolve(result);
+					}
+				})
+				.catch(error => {
+					if (!taskCompleted) {
+						taskCompleted = true;
+						clearTimeout(timeoutId);
+						this.worker.tasks.delete(uploadId);
+						reject(error);
+					}
+				});
+		});
+	}
+
+	async handleProcess(file, uploadId) {
+		if (!file.type.startsWith('image/')) {
+			return file;
+		}
+
+		const maxDimension = this.getMaxDimension();
+		const quality = 0.85;
+
+		if (this.shouldUseWorker(file)) {
+			try {
+				if (!this.worker.worker) {
+					this.initCompressionWorker();
+				}
+				if (this.worker.worker) {
+					return await this.processWithWorker(file, uploadId, maxDimension, quality);
+				}
+			} catch (error) {
+				console.warn('Worker processing failed, falling back to main thread:', error);
+			}
+		}
+
+		return await this.processOnMainThread(file, maxDimension, quality);
+	}
+
+	async processOnMainThread(file, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			const img = new Image();
+			const canvas = document.createElement('canvas');
+			const ctx = canvas.getContext('2d');
+			let objectUrl = null;
+
+			const cleanup = () => {
+				img.onload = null;
+				img.onerror = null;
+				if (objectUrl) {
+					URL.revokeObjectURL(objectUrl);
+					objectUrl = null;
+				}
+				canvas.width = 1;
+				canvas.height = 1;
+				ctx.clearRect(0, 0, 1, 1);
+			};
+
+			img.onload = () => {
+				try {
+					const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
+					canvas.width = width;
+					canvas.height = height;
+
+					ctx.imageSmoothingEnabled = true;
+					ctx.imageSmoothingQuality = 'high';
+					ctx.drawImage(img, 0, 0, width, height);
+
+					const outputFormat = this.getOptimalFormat(file);
+					const outputQuality = this.getOptimalQuality(file, quality);
+
+					canvas.toBlob(
+						(blob) => {
+							cleanup();
+							if (blob) {
+								const processedFile = new File(
+									[blob],
+									this.getProcessedFileName(file, outputFormat),
+									{ type: outputFormat, lastModified: Date.now() }
+								);
+								resolve(processedFile);
+							} else {
+								reject(new Error('Canvas toBlob failed'));
+							}
+						},
+						outputFormat,
+						outputQuality
+					);
+
+				} catch (error) {
+					cleanup();
+					reject(new Error(`Canvas processing failed: ${error.message}`));
+				}
+			};
+
+			img.onerror = () => {
+				cleanup();
+				reject(new Error(`Failed to load image: ${file.name}`));
+			};
+
+			try {
+				objectUrl = this.createPreviewUrl(file);
+				img.src = objectUrl;
+			} catch (error) {
+				cleanup();
+				reject(new Error(`Failed to create object URL: ${error.message}`));
+			}
+		});
+	}
+
+	getOptimalFormat(file) {
+		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
+			return file.type;
+		}
+		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
+	}
+
+	getOptimalQuality(file, requestedQuality) {
+		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
+		if (file.size < 2 * 1024 * 1024) return requestedQuality;
+		return Math.min(requestedQuality, 0.8);
+	}
+
+	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');
+	}
+
+	getMaxDimension() {
+		const screenWidth = window.screen.width;
+		const devicePixelRatio = window.devicePixelRatio || 1;
+		if (screenWidth * devicePixelRatio > 2560) return 2400;
+		if (screenWidth * devicePixelRatio > 1920) return 1920;
+		return 1200;
+	}
+
+	shouldUseWorker(file) {
+		return this.worker.worker &&
+			file.size > 1024 * 1024 &&
+			typeof OffscreenCanvas !== 'undefined';
+	}
+
+	async processWithWorker(file, uploadId, maxDimension, quality) {
+		return new Promise((resolve, reject) => {
+			if (!this.worker.worker) {
+				reject(new Error('Worker not available'));
+				return;
+			}
+
+			const messageId = `${uploadId}_${Date.now()}`;
+
+			const messageHandler = (e) => {
+				if (e.data.messageId !== messageId) return;
+
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+
+				if (e.data.success) {
+					const processedFile = new File(
+						[e.data.blob],
+						this.getProcessedFileName(file, e.data.format || 'image/webp'),
+						{ type: e.data.format || 'image/webp', lastModified: Date.now() }
+					);
+					resolve(processedFile);
+				} else {
+					reject(new Error(e.data.error || 'Worker processing failed'));
+				}
+			};
+
+			const errorHandler = (error) => {
+				this.worker.worker.removeEventListener('message', messageHandler);
+				this.worker.worker.removeEventListener('error', errorHandler);
+				reject(new Error(`Worker error: ${error.message}`));
+			};
+
+			this.worker.worker.addEventListener('message', messageHandler);
+			this.worker.worker.addEventListener('error', errorHandler);
+
+			this.worker.worker.postMessage({
+				messageId,
+				file,
+				maxDimension,
+				quality,
+				outputFormat: this.getOptimalFormat(file)
+			});
+		});
+	}
+
+	restartCompressionWorker() {
+		if (this.worker.worker) {
+			this.worker.worker.terminate();
+			this.worker.worker = null;
+		}
+		this.worker.tasks.clear();
+		if (this.worker.restart.count >= this.worker.restart.max) {
+			console.error('Max worker restarts reached, disabling worker');
+			return;
+		}
+		this.worker.restart.count++;
+		this.initCompressionWorker();
+	}
+
+	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 {
+						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));
+
+		} catch (error) {
+			console.warn('Failed to initialize compression worker:', error);
+			this.worker.worker = null;
+		}
+	}
+
+	calculateOptimalDimensions(img, maxDimension) {
+		let { width, height } = img;
+		if (width <= maxDimension && height <= maxDimension) {
+			return { width, height };
+		}
+		const scale = Math.min(maxDimension / width, maxDimension / height);
+		return {
+			width: Math.round(width * scale),
+			height: Math.round(height * scale)
+		};
+	}
+
+	supportsWebP() {
+		const canvas = document.createElement('canvas');
+		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
+	}
+
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		if (!this.previewUrls) this.previewUrls = new Set();
+		this.previewUrls.add(url);
+		return url;
+	}
+
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls?.delete(url);
+		}
+	}
+
+	/*******************************************************************************
+	 * QUEUE INTEGRATION
+	 *******************************************************************************/
+
+	async submitUploads(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) {
+			return;
+		}
+
+		let uploadIds = Array.from(fieldData.uploads);
+		if (uploadIds.length === 0) {
+			this.error.log('No uploads to upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
+
+		const fieldGroups = this.getFieldGroups(fieldId);
+
+		if (fieldGroups.length === 0) {
+			this.error.log('No groups created for post_group upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
+
+		// Build posts array from groups
+		const posts = [];
+		const formData = new FormData();
+		let uploadMap = [];
+
+		// 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;
+			}
+
+			// Get uploads for this group
+			const groupUploadIds = uploadIds.filter(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				return upload?.groupId === group.id;
+			});
+
+			// 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: 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(uploadId);
+				}
+			}
+
+			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', fieldData.config.content);
+		formData.append('user', fieldData.config.itemID);
+		formData.append('posts', JSON.stringify(posts));
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		const operation = {
+			endpoint: 'uploads/groups',
+			method: 'POST',
+			data: formData,
+			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: {
+				'action_nonce': window.auth.getNonce('dash')
+			},
+			append: '_upload',
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			// 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');
+				}
+			});
+
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
+
+			this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
+
+			return operationId;
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			throw error;
+		}
+	}
+
+	async queueUpload(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
+
+		const uploads = Array.from(fieldData.uploads);
+		const data = this.prepareUploadData(fieldData, uploads);
+
+		this.a11y.announce('Queuing for upload');
+
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: data,
+			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': window.auth.getNonce('dash') },
+			append: '_upload'
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			// Update upload statuses
+			uploads.forEach(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					upload.status = 'queued';
+					this.uploadStore.save(upload);
+					this.updateUploadStatus(uploadId, 'queued');
+				}
+			});
+
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
+
+			return operationId;
+		} catch (error) {
+			throw error;
+		}
+	}
+
+	async prepareUploadData(fieldData, uploads) {
+		const formData = new FormData();
+		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 blobPromises = uploads.map(async (uploadId) => {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) return;
+
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				formData.append('files[]', file);
+				uploadMap.push(upload.id);
+			}
+		});
+
+		await Promise.all(blobPromises);
+
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+		return formData;
+	}
+
+	async queueUploadMeta(e) {
+		const uploadId = this.getUploadIdFromElement(e.target);
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		if (!fieldData) return;
+
+		let data = {};
+		data[e.target.name] = e.target.value;
+
+		upload.meta = { ...upload.meta, ...data };
+		await this.uploadStore.save(upload);
+
+		let queueData = {};
+		queueData[upload.attachmentId ?? upload.id] = upload.meta;
+
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			data: queueData,
+			title: 'Updating meta',
+			canMerge: true,
+			headers: { 'action_nonce': window.auth.getNonce('dash') }
+		};
+
+		try {
+			await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sendMetaUpdate',
+				uploadId: upload.id
+			});
+		}
+	}
+
+	/*******************************************************************************
+	 * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
+	 *******************************************************************************/
+
+	/**
+	 * Handle successful operation completion - CLEAR STORES
+	 */
+	async handleOperationComplete(operation, fieldId) {
+		const results = operation.result?.data || operation.serverData?.data || [];
+
+		// 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 (!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 {
+			// Otherwise just update field state
+			await this.saveFieldData(fieldData);
+		}
+
+		this.updateFieldState(fieldId);
+	}
+
+	/**
+	 * Handle operation failure
+	 */
+	handleOperationFailed(operation, fieldId) {
+		const uploadIds = operation.data instanceof FormData
+			? JSON.parse(operation.data.get('upload_ids') || '[]')
+			: operation.data.upload_ids || [];
+
+		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);
+			}
+		});
+
+		if (fieldId) {
+			this.updateFieldState(fieldId);
+		}
+	}
+
+	/**
+	 * Handle operation cancellation
+	 */
+	async handleOperationCancelled(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
+
+		for (const uploadId of uploadsArray) {
+			await this.clearUpload(uploadId, false);
+		}
+
+		await this.clearFieldFromStores(fieldId);
+		this.updateFieldState(fieldId);
+		this.a11y.announce('Upload cancelled');
+	}
+
+	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) {
+		let selected = [];
+		const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
+
+		checkboxes.forEach(checkbox => {
+			const item = checkbox.closest('.item');
+			if (item) {
+				selected.push({
+					uploadId: item.dataset.uploadId,
+					fieldId: item.dataset.fieldId
+				});
+			}
+		});
+
+		return selected;
+	}
+
+	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);
+			}
+		}
+	}
+
+	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
+		console.log(config);
+		if (config.autoUpload && 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) {
+			const group = this.createGroup(fieldId, groupData.id);
+			if (!group) {
+				console.warn('Failed to create group:', groupData.id);
+				continue;
+			}
+
+			const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
+			if (storedGroup) {
+				// Restore metadata
+				if (groupData.changes) {
+					storedGroup.changes = { ...groupData.changes };
+				}
+
+				// Preserve upload order
+				if (groupData.uploads) {
+					storedGroup.uploads = [...groupData.uploads];
+				}
+
+				// Restore form field values
+				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;
+					}
+				}
+			}
+		}
+
+		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];
+	}
+
+	/*******************************************************************************
+	 * 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;
+
+		const uploadCount = fieldData.uploads?.size || 0;
+
+		// For groupable uploads, set max to 20
+		const maxFiles = fieldData.config.destination === 'post_group'
+			? 20
+			: (fieldData.config?.maxFiles || 999);
+
+		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
+		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+
+		// Show helpful message for groupable uploads
+		if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
+			this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
+		}
+	}
+
+	/*******************************************************************************
+	 * GROUP MANAGEMENT
+	 *******************************************************************************/
+	/**
+	 * Create sortable instance for a grid
+	 */
+	createSortableForGrid(grid, fieldId, groupId = null) {
+		if (!grid || grid.sortableInstance) return;
+
+		const sortableInstance = new Sortable(grid, {
+			animation: 150,
+			draggable: '.item',
+			multiDrag: true,
+			selectedClass: 'selected-for-drag',
+			avoidImplicitDeselect: true,
+			group: { name: fieldId, pull: true, put: true },
+			ghostClass: 'sortable-ghost',
+			chosenClass: 'sortable-chosen',
+			dragClass: 'sortable-drag',
+
+			// Centralized drop handler
+			onEnd: (evt) => this.handleDrop(evt, fieldId),
+
+			// Selection sync
+			onSelect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && !checkbox.checked) {
+					checkbox.checked = true;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onDeselect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && checkbox.checked) {
+					checkbox.checked = false;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onAdd: (evt) => this.updateSortableState(evt.to),
+			onRemove: (evt) => this.updateSortableState(evt.from)
+		});
+
+		grid.sortableInstance = sortableInstance;
+
+		const gridId = groupId
+			? `${fieldId}-group-${groupId}`
+			: `${fieldId}-preview`;
+
+		this.sortableInstances.set(gridId, sortableInstance);
+
+		return sortableInstance;
+	}
+	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).slice(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 = [];
+		const existingGroup = fieldData.groups.find(g => g.id === groupId);
+		if (!existingGroup) {
+			fieldData.groups.push({
+				id: groupId,
+				uploads: [],
+				changes: {}
+			});
+			this.saveFieldData(fieldData);
+		}
+
+		// Initialize selection handler
+		this.addGroupSelectionHandler(fieldId, groupId);
+
+		if (grid) {
+			this.createSortableForGrid(grid, fieldId, groupId);
+		}
+
+		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 'restore-all':
+				this.handleRestoreAll().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':
+					// Sync with Sortable
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'item-deselected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'range-selected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					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
+	}
+
+	/*******************************************************************************
+	 * 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;
+	}
+
+
+	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
+	 *******************************************************************************/
+
+	schedulePersistance(fieldId) {
+		const key = `persist_${fieldId}`;
+		window.debouncer.schedule(
+			key,
+			() => this.persistFieldState(fieldId),
+			250
+		);
+	}
+
+	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
+		});
+	}
+
+	/*******************************************************************************
+	 HELPER to GET UPLOADED FILES
+	*******************************************************************************/
+	/**
+	 * Get all files for a form (searches all upload fields within form)
+	 */
+	async getFilesForForm(formElement) {
+		const uploadFields = formElement.querySelectorAll('[data-upload-field]');
+		const allFiles = [];
+
+		for (const field of uploadFields) {
+			const fieldId = this.determineFieldId(field);
+			const files = await this.getFilesForField(fieldId);
+			allFiles.push(...files);
+		}
+
+		return allFiles;
+	}
+
+	/**
+	 * Get all files for a specific field
+	 */
+	async getFilesForField(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads) return [];
+
+		const files = [];
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
+
+		for (const uploadId of uploadsArray) {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) continue;
+
+			// Get the actual File object from blob data
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				files.push({
+					file: file,
+					uploadId: uploadId,
+					fieldName: fieldData.config.name,
+					meta: upload.meta || {}
+				});
+			}
+		}
+
+		return files;
+	}
+
+	/*******************************************************************************
+	 * RECOVERY & RESTORATION
+	 *******************************************************************************/
+
+	handleFieldStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.fieldStoreReady = true;
+				this.checkIfBothStoresReady();
+				break;
+		}
+	}
+
+	handleUploadStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.uploadStoreReady = true;
+				this.checkIfBothStoresReady();
+				break;
+			case 'item-saved':
+				this.showSaveIndicator(data.key);
+				break;
+		}
+	}
+
+	checkIfBothStoresReady() {
+		if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
+			this.hasCheckedForUploads = true;
+			this.checkForStoredUploads();
+		}
+	}
+
+	async checkForStoredUploads() {
+		const allFieldStates = this.fieldStore.getAll();
+
+		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);
+			});
+		});
+		if (pendingFields.length === 0) return;
+
+		await this.showRecoveryNotification(pendingFields);
+	}
+
+	async showRecoveryNotification(pendingFields) {
+		const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+		const totalGroups = pendingFields.reduce((sum, field) =>
+			sum + (field.groups?.length || 0), 0);
+
+		let notification = window.getTemplate('restoreNotification');
+		if (!notification) {
+			console.error('Restore notification template not found');
+			return;
+		}
+
+		// Build appropriate message
+		let message;
+		if (totalGroups > 0) {
+			let group = totalGroups > 1 ? 'groups' : 'group';
+			let upload = totalUploads > 1 ? 'uploads' : 'upload';
+			message = `${totalGroups} ${group} with ${totalUploads} ${upload} can be restored.`;
+		} else {
+			message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
+		}
+
+		const detailsEl = notification.querySelector('.restore-details');
+		if (detailsEl) {
+			detailsEl.textContent = message;
+		}
+
+		// Build the restoration preview
+		for (const field of pendingFields) {
+			let fieldTemplate = window.getTemplate('restoreField');
+			if (!fieldTemplate) continue;
+
+			// Set field name/title
+			const titleEl = fieldTemplate.querySelector('h3');
+			if (titleEl) {
+				titleEl.textContent = field.config.name || 'Unnamed Field';
+			}
+
+			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+			// Process each upload
+			for (let uploadId of field.uploads) {
+				const upload = this.uploadStore.get(uploadId);
+				let uploadItem = window.getTemplate('uploadItem');
+				if (!uploadItem) continue;
+				//
+				// 	const imgEl = uploadItem.querySelector('img');
+				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
+				//
+				const file = await this.getBlobData(upload.id);
+				if (file) {
+
+					try {
+						// Create new blob URL from stored data
+						const previewUrl = this.createPreviewUrl(file);
+
+						let [
+							featured,
+							img,
+							video,
+							preview,
+							details
+						] = [
+							uploadItem.querySelector('[name="featured"]'),
+							uploadItem.querySelector('img'),
+							uploadItem.querySelector('video'),
+							uploadItem.querySelector('label > span'),
+							uploadItem.querySelector('details')
+						];
+
+						uploadItem.dataset.uploadId = upload.id;
+
+
+						uploadItem.dataset.fieldId = field.id;
+
+						let subtype = this.getSubtypeFromMime(file.type);
+						uploadItem.dataset.subtype = subtype;
+						switch (subtype) {
+							case 'image':
+								[
+									img.src,
+									img.alt
+								] = [
+									previewUrl,
+									file.name ?? upload.meta?.originalName ?? ''
+								];
+								video.remove();
+								preview.remove();
+								break;
+							case 'video':
+								video.src = previewUrl;
+								img.remove();
+								preview.remove();
+								break;
+							case 'document':
+								let extension = '';
+								let icon;
+								switch (extension) {
+									case 'pdf':
+										icon = window.getIcon('file-pdf');
+										break;
+									case 'csv':
+										icon = window.getIcon('file-csv');
+										break;
+									case 'doc':
+										icon = window.getIcon('file-doc');
+										break;
+									case 'txt':
+										icon = window.getIcon('file-txt');
+										break;
+									case 'xls':
+										icon = window.getIcon('file-xls');
+										break;
+									default:
+										icon = window.getIcon('file');
+										break;
+								}
+
+								preview.innerText = upload.originalFile.name;
+								preview.prepend(icon);
+								img.remove();
+								video.remove();
+								break;
+						}
+
+						// Store URL for cleanup later
+						uploadItem.dataset.previewUrl = previewUrl;
+					} catch (error) {
+						console.warn('Failed to create preview for upload:', upload.id, error);
+					}
+				}
+
+				// Set upload metadata
+				const nameEl = uploadItem.querySelector('summary span');
+				if (nameEl) {
+					nameEl.textContent = upload.meta?.originalName || 'Unknown file';
+				}
+
+				const metaEl = uploadItem.querySelector('details');
+				if (metaEl && upload.meta) {
+					metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
+				}
+
+				// Update input IDs safely
+				uploadItem.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;
+						}
+					}
+				});
+
+				if (itemGrid) {
+					itemGrid.appendChild(uploadItem);
+				}
+			}
+
+			notification.querySelector('.wrap').appendChild(itemGrid);
+		}
+
+		document.querySelector('.field.upload').appendChild(notification);
+		notification = document.querySelector('dialog.restore-uploads');
+		this.restoreModal = new window.jvbModal(notification);
+		this.restoreSelection = new window.jvbHandleSelection({
+			container: notification,
+			ui: {
+				selectAll: notification.querySelector('#select-all-restore'),
+				count: notification.querySelector('.selection-count'),
+			},
+		});
+
+		this.restoreModal.handleOpen();
+
+	}
+
+	async handleRestoreUploads() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
+
+		const selectedUploads = this.getSelectedRestorationUploads(notification);
+		if (selectedUploads.length === 0) {
+			return;
+		}
+		await this.restoreSelectedUploads(selectedUploads);
+
+		this.cleanupRestore();
+	}
+
+	async handleRestoreAll() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
+		// Gets ALL uploads from notification without checking selection
+		const allUploads = [];
+		notification.querySelectorAll('.item.upload').forEach(item => {
+			let uploadId = item.dataset.uploadId;
+			let fieldId = item.dataset.fieldId;
+			allUploads.push({ uploadId, fieldId });
+		});
+
+		await this.restoreSelectedUploads(allUploads);
+		this.cleanupRestore();
+	}
+
+	showSaveIndicator(key) {
+		// Optional: show user that state is being saved
+	}
+
+	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();
+	}
+
+	/*******************************************************************************
+	 * EVENT SYSTEM
+	 *******************************************************************************/
+
+	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);
+			}
+		});
+	}
+
+	/*******************************************************************************
+	 * DESTROY & CLEANUP
+	 *******************************************************************************/
+
+	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);
+
+		if (this.dragController) {
+			this.dragController.destroy();
+		}
+
+		this.selectionHandlers.forEach(handler => handler.destroy());
+		this.selectionHandlers.clear();
+
+		this.cleanupAllPreviewUrls();
+
+		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();
+	}
+}
+
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbUploads = new UploadManager();
+		}
+	});
+});
diff --git a/assets/js/concise/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
index 3b9e355..4a7b85b 100644
--- a/assets/js/concise/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -274,6 +274,26 @@
 	return div.innerHTML;
 }
 
+window.generateID = function(prefix = 'jvb') {
+	return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`;
+}
+
+window.showProgress = function(elements, current, total, message = '', icon = '') {
+	const show = current < total;
+	if (elements.progress && show) {
+		window.fade(elements.progress, true);
+	}
+	const percent = total > 0 ? (current / total) * 100 : 0;
+	if (elements.fill) elements.fill.style.width = `${percent}%`;
+	if (elements.details) elements.details.textContent = message;
+	if (elements.count) elements.count.textContent = `${current}/${total}`;
+	if (elements.icon) elements.icon.className = (icon === '') ? 'icon' : 'icon icon-'+icon;
+
+	if (elements.progress && current === total) {
+		window.fade(elements.progress, false);
+	}
+}
+
 /**
  * Format a date string for display
  * @param {string} dateString - ISO date string
diff --git a/assets/js/concise/navigation.js b/assets/js/concise/navigation.js
index 77c15e9..cc2648d 100644
--- a/assets/js/concise/navigation.js
+++ b/assets/js/concise/navigation.js
@@ -72,7 +72,7 @@
 			this.toggleNav(!nav.classList.contains('open'), nav.id);
 		}
 
-		let submenuToggle = e.target.closest('[data-action="toggle-submenu"]')
+		let submenuToggle = e.target.closest('[data-action="toggle-submenu"], .has-submenu .a')
 		if (submenuToggle) {
 			let li = submenuToggle.closest('li');
 			this.toggleSubmenu(!li.classList.contains('open'), li);
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index 3a56d7e..8723a48 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:"content",headers:{action_nonce:window.auth.getNonce("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:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],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 s={};s[t]=e,this.savePosts(s,"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&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let a=[];switch(!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.post_status)&&a.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let o=t.config.element.querySelectorAll(".selected input:checked");o.forEach((e=>{l[e.value]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&a.push(e.value)})),s=`Updating ${o.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=i,s=`Saving ${s} Changes`)}if(a.length>0){let e=0;a.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)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}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.ui.uploader&&(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,s)=>{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 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.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")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const a=this.store.get(i);if(!a)return;a.fields[l]=this.getInputValue(s),this.store.save(a);let o={};o[i]=a.fields,this.savePosts(o,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),a=s.closest("tr.timeline-point"),o=this.store.get(l);if(!o)return;const n=this.getInputValue(s);if(a){const e=a.dataset.imageId;o.fields.timeline||(o.fields.timeline={}),o.fields.timeline[e]||(o.fields.timeline[e]={}),o.fields.timeline[e][i]=n}else o.fields[i]=n;this.store.save(o);let r={};r[l]=o.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 s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;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("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(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.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&&!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&&(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 s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),a=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),a&&s.thumbnail&&(a.src=s.thumbnail,a.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)}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 s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){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",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:"content",headers:{action_nonce:window.auth.getNonce("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:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],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 s={};s[t]=e,this.savePosts(s,"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","uploads/groups"].includes(t.endpoint)&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let o=[];switch(!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.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]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(e.value)})),s=`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"]]=i,s=`Saving ${s} 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)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}async handleQueueSuccess(e,t){this.store.clearCache(),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.ui.uploader&&(window.jvbUploads.scanFields(document.querySelector(this.elements.uploader)),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&(console.log(t),t===this.ui.uploader.querySelector("[data-uploader]")?.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearHttpHeaders()})))}))),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.ui.uploader&&(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,s)=>{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 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.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")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(i);if(!o)return;o.fields[l]=this.getInputValue(s),this.store.save(o);let a={};a[i]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),o=s.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(s);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][i]=n}else a.fields[i]=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 s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;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("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(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.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&&!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&&(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 s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),o=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),o&&s.thumbnail&&(o.src=s.thumbnail,o.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)}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 s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){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",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(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 3fcaee1..ea47195 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,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()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let i=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,r={config:{dbName:i.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};r.config.headers={"X-WP-Nonce":window.auth.getNonce(),...r.config.headers},i.stores[t.storeName]=s,this.stores.set(s,r),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const r={};for(const[e,t]of Object.entries(i.stores))r[e]=this.getStoreAPI(t);return r}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),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}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(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:i}=t,r=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,i,(e=>{r.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}r.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,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(((i,r)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>i(e.target.result),a.onerror=e=>r(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})}}async loadFromObjectStore(e,t,s){const i=this.stores.get(e);return i?.db&&i.db.objectStoreNames.contains(t)?new Promise((e=>{const r=i.db.transaction([t],"readonly").objectStore(t).getAll();r.onsuccess=t=>{const i=t.target.result||[];i.forEach(s),e(i)},r.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).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 ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,i){const r=this.stores.get(e);return r?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=r.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>a(o.error);try{h=i(c,o)}catch(e){a(e)}}))):null}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),i=t.cache.get(s);if(i&&this.isCacheValid(i,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:i.items||[]}),i;t.config.showLoading&&this.setLoading(!0);const r=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&i&&(i.etag&&(a["If-None-Match"]=i.etag),i.lastModified&&(a["If-Modified-Since"]=i.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(r,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return i?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:i.items||[]}),i):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}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 i=t.config.apiBase+t.config.endpoint;return s.toString()?`${i}?${s}`:i}async processFetchedData(e,t,s,i){const r=this.stores.get(e),a=t.items||[],o=[];r.db&&a.length>0&&await this.withTransaction(e,r.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const i=this._saveItem(e,s);o.push(i),t.put(i.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters},etag:i.headers.get("ETag"),lastModified:i.headers.get("Last-Modified")};r.cache.set(s,n),r.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),r.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}},o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}_saveItem(e,t){const s=this.stores.get(e),i=this.processForStorage(t,s.config.validateData);if(!i.valid)throw new Error(`Non-serializable data: ${i.error}`);const r=i.data,a=this.getItemKey(r,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:r,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),i=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(i.processed)})),this.notify(e,"item-saved",{item:i.item,key:i.key,previousItem:i.previousItem}),i.key}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const i=typeof e;if(["string","number","boolean"].includes(i))return{valid:!0,data:e};if("function"===i)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:null};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:null};if(e instanceof FormData)return t?{valid:!1,error:`FormData at ${s}`}:{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e))return{valid:!0,data:e};if(e instanceof Set){const i=Array.from(e);return this.processForStorage(i,t,s)}if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const i=[];for(let r=0;r<e.length;r++){const a=this.processForStorage(e[r],t,`${s}[${r}]`);if(!a.valid)return a;null!==a.data&&i.push(a.data)}return{valid:!0,data:i}}if("object"===i){const i={};for(const[r,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${r}`);if(!e.valid)return e;null!==e.data&&(i[r]=e.data)}return{valid:!0,data:i}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:null}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{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),i=t.cache.get(s);return i&&i.items?i.items.reduce(((e,s)=>{const i=t.data.get(s);return i&&e.push(i),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const i=this.stores.get(e),r={...i.filters};s?i.filters={...i.config.filters}:Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete i.filters[e]:i.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:r,filters:i.filters,updates:t}),i.config.endpoint&&await this.fetch(e)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e])))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}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}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const i=this.subscribers.get(e);i&&i.forEach((i=>{try{i(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let i=e;for(const e of s)i=i?.[e];return i}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",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,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()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}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),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),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),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}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,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})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).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 ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let d;o.oncomplete=()=>e(d),o.onerror=()=>a(o.error);try{d=r(c,o)}catch(e){a(e)}}))):null}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};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}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,r){const i=this.stores.get(e),a=t.items||[],o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified")};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}},o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:(console.debug(`[DataStore] Stripped function at ${s}`),{valid:!0,data:void 0});if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:(console.debug(`[DataStore] Stripped DOM element at ${s}`),{valid:!0,data:void 0});if(e instanceof FormData)return t?{valid:!1,error:`FormData at ${s}`}:(console.debug(`[DataStore] Converted FormData at ${s}`),{valid:!0,data:this.formDataToObject(e)});if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:(console.debug(`[DataStore] Stripped unknown type at ${s}`),{valid:!0,data:void 0})}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{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())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}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);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s?r.filters={...r.config.filters}:Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),r.config.endpoint&&await this.fetch(e)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e])))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}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}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);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)}}))}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",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/handleSelection.min.js b/assets/js/min/handleSelection.min.js
index eb95d96..e97ee16 100644
--- a/assets/js/min/handleSelection.min.js
+++ b/assets/js/min/handleSelection.min.js
@@ -1 +1 @@
-window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.ui=e.ui||{},this.itemSelector=e.itemSelector||".item",this.checkboxSelector=e.checkboxSelector||'[name*="select-item"]',this.selectedItems=new Set,this.lastSelected=null,this.subscribers=new Set,this.init()}init(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("keydown",this.keyHandler)}handleKeys(e){(e.ctrlKey||e.metaKey)&&"a"===e.key&&(e.preventDefault(),this.ui.selectAll&&(this.ui.selectAll.checked=!0,this.selectAll(!0),window.jvbA11y&&window.jvbA11y.announce("All items selected"))),"Escape"===e.key&&this.selectedItems.size>0&&(this.selectAll(!1),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")),("Delete"===e.key||"Backspace"===e.key)&&!e.target.matches("input, textarea")&&this.selectedItems>0&&(e.preventDefault(),confirm(`Remove ${this.selectedItems.size} selected item${1!==this.selectedItems.size?"s":""}?`)&&this.deselect(this.selectedItems))}handleClick(e){const t=e.target.closest(`${this.checkboxSelector}, label[for]`);if(!t)return;const s="LABEL"===t.tagName?document.getElementById(t.getAttribute("for")):t;if(s)if(e.shiftKey&&this.lastSelected)e.preventDefault(),this.handleRangeSelection(s);else{const e=s.closest(this.itemSelector);e&&(this.lastSelected=e)}}handleChange(e){if(this.ui.selectAll&&e.target===this.ui.selectAll)this.selectAll(e.target.checked);else{const t=e.target.closest(this.checkboxSelector);if(!t)return;this.toggleSelection(this.getItemId(t))}}toggleSelection(e){if(!e)return;let t=!0;this.selectedItems.has(e)?(t=!1,this.selectedItems.delete(e)):this.selectedItems.add(e),t?this.notify("item-selected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}):this.notify("item-deselected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(this.itemSelector);e||(this.selectedItems.clear(),this.ui.selectAll&&(this.ui.selectAll.checked=!1)),t.forEach((t=>{const s=this.getItemId(t),i=t.querySelector(this.checkboxSelector);i&&(i.checked=e),e&&s&&this.selectedItems.add(s)})),this.notify("select-all",{container:this.container,selected:e,items:t}),this.updateSelectionUI()}clearSelection(){this.selectAll(!1)}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(this.itemSelector));const t=e.closest(this.itemSelector);if(!t)return;const s=Array.from(this.container.querySelectorAll(this.itemSelector)),i=s.indexOf(this.lastSelected),c=s.indexOf(t);if(-1===i||-1===c)return;const l=Math.min(i,c),n=Math.max(i,c);let h=!e.checked;for(let e=l;e<=n;e++){const t=s[e],i=t.querySelector(this.checkboxSelector),c=this.getItemId(t);i&&c&&(i.checked=h,this.selectedItems.add(c))}this.lastSelected=t,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:this.selectedItems,container:this.container});const r=n-l+1;window.jvbA11y&&window.jvbA11y.announce(`Selected ${r} items in range`)}updateSelectionUI(){const e=this.selectedItems.size,t=this.container.querySelectorAll(this.itemSelector).length;if(this.ui.bulkControls&&(this.ui.bulkControls.hidden=0===e),this.ui.count){const t=1===e?"item":"items";this.ui.count.textContent=0===e?"":`{ ${e} ${t} selected }`,this.ui.count.hidden=0===e}if(this.ui.selectAll){this.ui.selectAll.checked=t>0&&e===t,this.ui.selectAll.indeterminate=e>0&&e<t;const s=this.ui.selectAll.nextElementSibling||this.ui.selectAll.previousElementSibling;s&&"LABEL"===s.tagName&&(s.textContent=t>0&&e===t?"Clear Selection":"Select All")}}getItemId(e){const t=e.closest(this.itemSelector);return t?t.dataset.id||t.dataset.itemId||t.dataset.uploadId||t.id:null}isSelected(e){return this.selectedItems.has(e)}select(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.add(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!0)}})),this.updateSelectionUI(),this.notify("item-selected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}deselect(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.delete(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!1)}})),this.updateSelectionUI(),this.notify("item-deselected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.container&&(this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("keydown",this.keyHandler)),this.clearSelection(),this.subscribers.clear(),this.container=null,this.ui=null,this.lastSelected=null}};
\ No newline at end of file
+window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.selectors={item:e.item||".item",count:e.count||".selection-count",bulkControls:e.bulkControls||".selection-actions",checkbox:e.checkbox||'[name*="select-item"]',selectAll:e.selectAll||"[data-select-all]",wrapper:e.wrapper||":has(.item-grid)"},this.ui=window.uiFromSelectors(this.selectors,this.container),this.selectedItems=new Set,this.lastSelected=null,this.lastSelectedWrapper=null,this.lastClicked=null,this.subscribers=new Set,this.initListeners()}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("keydown",this.keyHandler)}handleChange(e){if(e.target.matches(this.selectors.selectAll))this.handleSelectAll(e.target);else if(e.target.matches(this.selectors.checkbox)){const t=e.target.closest(this.selectors.item);if(!t)return;const s=this.getItemWrapper(t),i=this.getItemId(t);this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&this.clearSelection(),e.target.checked?this.select(i,!1):this.deselect(i,!1),this.lastSelected=i,this.lastSelectedWrapper=s}}handleClick(e){const t=e.target.closest(this.selectors.item),s=t?this.getItemWrapper(t):null;if(s&&(this.lastClicked=s),t&&!e.target.matches(this.selectors.checkbox)&&this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&(this.clearSelection(),this.lastSelectedWrapper=s),!e.shiftKey)return;if(!e.target.closest(this.selectors.checkbox)||!this.lastSelected||!this.lastSelectedWrapper)return;if(!t||!s)return;if(s!==this.lastSelectedWrapper)return;const i=Array.from(s.querySelectorAll(this.selectors.item)),l=this.getItemId(t),c=i.findIndex((e=>this.getItemId(e)===this.lastSelected)),r=i.findIndex((e=>this.getItemId(e)===l));if(-1===c||-1===r)return;const[h,n]=[Math.min(c,r),Math.max(c,r)];i.slice(h,n+1).forEach((e=>{this.select(this.getItemId(e))})),this.notify("range-selected",{selectedItems:new Set(this.selectedItems),wrapper:s})}getItemWrapper(e){if(!e)return null;const t=this.selectors.wrapper.split(",").map((e=>e.trim()));for(const s of t){const t=e.closest(s);if(t)return t}return null}handleKeys(e){if((e.ctrlKey||e.metaKey)&&"a"===e.key&&(e.preventDefault(),this.lastClicked)){let e=this.lastClicked.querySelector(this.selectors.selectAll);e&&(e.checked=!0)}"Escape"===e.key&&this.selectedItems.size>0&&(this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared"))}handleSelectAll(e){const t=this.getItemWrapper(e)||e.closest(this.selectors.wrapper);if(!t)return;this.lastSelectedWrapper&&t!==this.lastSelectedWrapper&&this.clearSelection();const s=t.querySelectorAll(this.selectors.item),i=Array.from(s).map((e=>this.getItemId(e)));e.checked?(i.forEach((e=>this.select(e,!0,!1))),this.lastSelectedWrapper=t):(i.forEach((e=>this.deselect(e,!0,!1))),0===this.selectedItems.size&&(this.lastSelectedWrapper=null));let l=e.nextElementSibling||e.previousElementSibling;l&&"LABEL"===l.tagName&&(l.textContent=e.checked&&s.length>0?"Clear Selection":"Select All"),this.updateSelectionUI(),this.notify("select-all",{wrapper:t,checked:e.checked,ids:i,selectedItems:new Set(this.selectedItems)})}getItemId(e){return e.dataset.uploadId}select(e,t=!0,s=!0){this.selectedItems.has(e)||(this.selectedItems.add(e),t&&this.setCheckboxState(e,!0),s&&this.updateSelectionUI(),this.notify("item-selected",{id:e,selectedItems:new Set(this.selectedItems)}))}deselect(e,t=!0,s=!0){this.selectedItems.has(e)&&(this.selectedItems.delete(e),t&&this.setCheckboxState(e,!1),s&&this.updateSelectionUI(),this.notify("item-deselected",{id:e,selectedItems:new Set(this.selectedItems)}))}toggle(e){this.selectedItems.has(e)?this.deselect(e):this.select(e),this.updateSelectionUI()}clearSelection(){this.selectedItems.forEach((e=>this.setCheckboxState(e,!1))),this.selectedItems.clear(),this.lastSelected=null,this.lastSelectedWrapper=null,this.container.querySelectorAll(this.selectors.selectAll).forEach((e=>{e.checked=!1;const t=e.nextElementSibling||e.previousElementSibling;"LABEL"===t?.tagName&&(t.textContent="Select All")})),this.updateSelectionUI(),this.notify("selection-cleared",{selectedItems:new Set})}isSelected(e){return this.selectedItems.has(e)}getSelection(){return new Set(this.selectedItems)}setCheckboxState(e,t){const s=this.container.querySelector(`[data-upload-id="${e}"]`),i=s?.querySelector(this.selectors.checkbox);i&&i.checked!==t&&(i.checked=t)}updateSelectionUI(){if(!this.lastClicked||!this.ui.count)return;const e=this.selectedItems.size;let t=this.lastClicked.querySelector(this.selectors.bulkControls);t&&(t.hidden=0===e);let s=this.lastClicked.querySelector(this.selectors.count);if(s){const t=1===e?"item":"items";s.textContent=0===e?"":`{ ${e} ${t} selected }`,s.hidden=0===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("HandleSelection subscriber error:",e)}}))}destroy(){this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("keydown",this.keyHandler),this.subscribers.clear(),this.selectedItems.clear()}};
\ No newline at end of file
diff --git a/assets/js/min/navigation.min.js b/assets/js/min/navigation.min.js
index e34308d..282d0da 100644
--- a/assets/js/min/navigation.min.js
+++ b/assets/js/min/navigation.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"]');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index 719e968..a589cbf 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){if(this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.user=window.auth.getUser(),!this.user)return console.log("Queue: User not logged in, queue disabled"),this.store=null,void(this.canUpdateUI=!1);this.headers={"X-WP-Nonce":window.auth.getNonce(),...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const t=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1});this.store=t.queue,this.classes=["offline","synced","pending"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.updateUI=()=>window.debouncer.schedule("queue-ui-update",this._updateUI.bind(this),100),this.initQueue()}async initQueue(){this.maybeStartPolling()||this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.maybeStartPolling(),this.updateUI();break;case"item-saved":console.log(t,"Item saved data"),t.previousItem&&t.previousItem.status!==t.item.status&&this.handleOperationStatusChange(t.item,t.previousItem.status),this.maybeStartPolling();break;default:this.updateUI()}}))}maybeStartPolling(){return this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&(this.startPolling(),!0)}handleOperationStatusChange(e){switch(e.status){case"completed":console.log(e),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.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)}hideQueue(){this.ui.panel.hidden=!0,this.ui.toggle.hidden=!0}showQueue(){this.ui.panel.hidden=!1,this.ui.toggle.hidden=!1}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){this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.maybeStartPolling()?this.showQueue():this.hideQueue()}else this.stopActivityTracking()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading"),e.data?._isFormData&&(e.data=await this.store.objectToFormData(e.data));const t=`${this.config.apiBase}${e.endpoint}`;let s;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}`);a.id&&e.id!==a.id?e=await this.handleServerMerge(e,a):(e.status=a.status||"pending",e.serverData=a,this.updateOperationStatus(e.id,e.status)),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)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(s.data=window.deepMerge(s.data,e.data),s.status=t.status||"pending",s.serverData=t,this.updateOperationStatus(s.id,s.status),this.removeOperationFromUI(e.id),this.clearQueue(e.id),s):(this.clearQueue(e.id),e.id=t.id,e.status=t.status||"pending",e.serverData=t,this.updateOperationStatus(e.id,e.status),e)}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(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))}getOperationIds(e){return e.map((e=>e.id))}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)return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationFromUI(e)));try{const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:window.auth.getUser()})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);this.notify(`${t}-operation`,i),s?this.clearQueue(e):(i.status="queued",i.retries=0,this.setQueue(i),this.updateOperationStatus(i.id,i.status))})),"retry"===t&&this.startActivityTracking(),this.updateUI(),a}catch(s){return await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t))),{success:!1,error:s.message}}}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),document.addEventListener("click",this.clickHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.isPolling||this.isProcessing)return e.preventDefault(),"You have unsaved changes in the queue. Proceed?"},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.getOperationIds(this.getOperationsByStatus("completed"));e.length>0&&this.updateServerOperations(e,"dismiss")}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationIds(this.getOperationsByStatus("failed"));e.length>0&&this.updateServerOperations(e,"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)}}initUI(){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=window.uiFromSelectors(this.selectors),this.ui.panel||(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))}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)}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){if(this.subscribers)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",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){if(this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.user=window.auth.getUser(),!this.user)return console.log("Queue: User not logged in, queue disabled"),this.store=null,void(this.canUpdateUI=!1);this.headers={"X-WP-Nonce":window.auth.getNonce(),...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const t=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1});this.store=t.queue,this.classes=["offline","synced","pending"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.updateUI=()=>window.debouncer.schedule("queue-ui-update",this._updateUI.bind(this),100),this.initQueue()}async initQueue(){this.maybeStartPolling()||this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.maybeStartPolling(),this.updateUI();break;case"item-saved":console.log(t,"Item saved data"),t.previousItem&&t.previousItem.status!==t.item.status&&this.handleOperationStatusChange(t.item,t.previousItem.status),this.maybeStartPolling();break;default:this.updateUI()}}))}maybeStartPolling(){return this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&(this.startPolling(),!0)}handleOperationStatusChange(e){switch(e.status){case"completed":console.log(e),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:{},sendNow:!1,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;if(t.sendNow)return this.processOperation(t).then((()=>{})),this.store.clearCache(),window.debouncer.schedule("fastQueue",this.startPolling.bind(this),200),this.showQueue(),t.id;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 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.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)}hideQueue(){this.ui.panel.hidden=!0,this.ui.toggle.hidden=!0}showQueue(){this.ui.panel.hidden=!1,this.ui.toggle.hidden=!1}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){this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.maybeStartPolling()?this.showQueue():this.hideQueue()}else this.stopActivityTracking()}async processOperation(e,t=!1){try{t||(this.updateOperationStatus(e.id,"uploading"),e.data?._isFormData&&(e.data=await this.store.objectToFormData(e.data)));const s=`${this.config.apiBase}${e.endpoint}`;let i;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),i=e.data):(i=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const a=await fetch(s,{method:e.method,headers:e.headers,body:i}),n=await a.json();if(t&&(e.data={}),!a.ok||!1===n.success)throw new Error(n.message||`HTTP ${a.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status||"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),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)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(s.data=window.deepMerge(s.data,e.data),s.status=t.status||"pending",s.serverData=t,this.updateOperationStatus(s.id,s.status),this.removeOperationFromUI(e.id),this.clearQueue(e.id),s):(this.clearQueue(e.id),e.id=t.id,e.status=t.status||"pending",e.serverData=t,this.updateOperationStatus(e.id,e.status),e)}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(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))}getOperationIds(e){return e.map((e=>e.id))}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)return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationFromUI(e)));try{const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:window.auth.getUser()})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);this.notify(`${t}-operation`,i),s?this.clearQueue(e):(i.status="queued",i.retries=0,this.setQueue(i),this.updateOperationStatus(i.id,i.status))})),"retry"===t&&this.startActivityTracking(),this.updateUI(),a}catch(s){return await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t))),{success:!1,error:s.message}}}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),document.addEventListener("click",this.clickHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.isPolling||this.isProcessing)return e.preventDefault(),"You have unsaved changes in the queue. Proceed?"},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.getOperationIds(this.getOperationsByStatus("completed"));e.length>0&&this.updateServerOperations(e,"dismiss")}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationIds(this.getOperationsByStatus("failed"));e.length>0&&this.updateServerOperations(e,"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)}}initUI(){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=window.uiFromSelectors(this.selectors),this.ui.panel||(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"),d=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),d&&(d.style.width=`${i}%`);const l=t.querySelector(".actions");l&&this.updateActionButtons(e,l)}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))}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)}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){if(this.subscribers)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",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/uploadGroups.min.js b/assets/js/min/uploadGroups.min.js
new file mode 100644
index 0000000..5683211
--- /dev/null
+++ b/assets/js/min/uploadGroups.min.js
@@ -0,0 +1 @@
+(()=>{class e{constructor(e){this.manager=e,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.groupElements=new Map,this.groupStoreReady=!1,this.selectors={container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info",display:".group-display",empty:".empty-group"},this.init()}async init(){const{groups:e}=window.jvbStore.register("uploads",[{storeName:"groups",keyPath:"id",indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"pageUrl",keyPath:"pageUrl"}],TTL:6048e5,delayFetch:!0}]);this.groupStore=e,this.groupStore.subscribe((e=>{"data-loaded"===e&&(this.groupStoreReady=!0,this.checkForRecovery())})),this.manager.subscribe(this.handleManagerEvent.bind(this)),this.queue.subscribe(this.handleQueueEvent.bind(this)),this.initListeners(),this.manager.groups=this}handleManagerEvent(e,t){switch(e){case"field-registered":this.enhanceField(t.fieldId);break;case"files-processed":this.onFilesProcessed(t.fieldId);break;case"upload-removed":this.onUploadRemoved(t.uploadId,t.fieldId);break;case"field-state-updated":this.updateGroupCount(t.fieldId)}}handleQueueEvent(e,t){if("uploads/groups"!==t.endpoint)return;const o=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"operation-complete":this.handleOperationComplete(t,o);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,o)}}enhanceField(e){const t=this.manager.fieldElements.get(e);if(!t)return;const{element:o,config:a}=t;if("post_group"!==a.destination)return;const r=o.querySelector(this.selectors.display);r&&(t.ui.groups={display:r,container:o.querySelector(".item-grid.groups"),empty:o.querySelector(this.selectors.empty),groups:new Map}),this.initEmptyGroupDropZone(e),a.maxFiles=20}initEmptyGroupDropZone(e){const t=this.manager.fieldElements.get(e),o=t?.ui?.groups?.empty;o&&!o.sortableInstance&&(o.sortableInstance=new Sortable(o,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:e,pull:!1,put:!0},ghostClass:"sortable-ghost",onEnd:t=>this.handleDrop(t,e)}))}onFilesProcessed(e){const t=this.manager.fieldElements.get(e);t&&("post_group"===t.config.destination&&t.ui.groups?.display&&(t.ui.groups.display.hidden=!1),this.initGroupSortables(e))}initGroupSortables(e){const t=this.manager.fieldElements.get(e);if(!t?.element)return;t.element.querySelectorAll(this.selectors.grid).forEach((t=>{if(t.sortableInstance)return;const o=t.closest(this.selectors.container)?.dataset.groupId;this.createSortableForGrid(t,e,o)}))}async onUploadRemoved(e,t){const o=this.manager.uploadStore.get(e);if(o?.groupId){const t=this.groupStore.get(o.groupId);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.deleteGroup(o.groupId,!1):await this.groupStore.save(t))}}initListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this))}handleClick(e){const t=e.target.closest("[data-action]");if(!t)return;const o=t.dataset.action,a=this.manager.getFieldIdFromElement(t);switch(o){case"add-to-group":this.handleAddToGroup(a);break;case"delete-group":this.handleDeleteGroup(t);break;case"remove-from-group":this.handleRemoveFromGroup(t);break;case"upload":const o=this.manager.fieldElements.get(a);"post_group"===o?.config.destination&&(e.stopImmediatePropagation(),this.submitGroupedUploads(a))}}handleChange(e){const t=this.manager.getFieldIdFromElement(e.target);if(!t)return;const o=this.manager.fieldElements.get(t);if("post_group"!==o?.config.destination)return;const a=e.target.closest(this.selectors.container);a&&this.handleGroupMetaChange(e.target,a)}createSortableForGrid(e,t,o=null){if(!e||e.sortableInstance)return;const a=new Sortable(e,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:t,pull:!0,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleDrop(e,t),onSelect:e=>this.manager.syncCheckboxToSortable(e.item,!0),onDeselect:e=>this.manager.syncCheckboxToSortable(e.item,!1),onAdd:e=>this.manager.updateSortableState(e.to),onRemove:e=>this.manager.updateSortableState(e.from)});e.sortableInstance=a;const r=o?`${t}-group-${o}`:`${t}-preview`;return this.manager.sortableInstances.set(r,a),a}async handleDrop(e,t){const o=e.to,a=e.from,r=e.items?.length>0?e.items:[e.item],s=r.map((e=>e.dataset.uploadId));if(o===a)return void this.handleReorder(e);const i=this.getDropTargetType(o);try{switch(i){case"empty-group":const e=await this.createGroup(t);if(!e)throw new Error("Group creation failed");for(const t of s)await this.addToGroup(t,e.grid);break;case"preview":for(const e of s)await this.removeFromGroup(e);break;case"group":for(const e of s)await this.addToGroup(e,o);break;default:throw new Error("Unknown drop target")}this.finalizeDrop(t,r.length,i)}catch(e){console.error("Drop error:",e);const o=this.manager.fieldElements.get(t);o?.ui?.preview&&r.forEach((e=>o.ui.preview.appendChild(e))),this.a11y.announce("An error occurred. Items returned to preview.")}this.manager.updateSortableState(o),a!==o&&this.manager.updateSortableState(a)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}finalizeDrop(e,t,o){const a={"empty-group":t>1?`Created group with ${t} items`:"Created group with item",preview:t>1?`Moved ${t} items to preview`:"Moved item to preview",group:t>1?`Moved ${t} items to group`:"Moved item to group"};this.a11y.announce(a[o]||"Items moved"),this.manager.selectionHandlers.get(e)?.clearSelection()}handleReorder(e){const t=e.to,o=t.dataset.groupId;if(o){const e=this.groupStore.get(o);if(e){const o=Array.from(t.querySelectorAll(".item:not(.sortable-ghost)")).map((e=>e.dataset.uploadId)).filter(Boolean);e.uploads=o,this.groupStore.save(e)}}this.a11y.announce("Item reordered")}getFieldGroups(e){return this.groupStore.getAll().filter((t=>t.fieldId===e))}async createGroup(e,t=null){const o=this.manager.fieldElements.get(e);if(!o)return null;const a={id:t=t||this.manager.generateId("group"),fieldId:e,pageUrl:window.location.href,uploads:[],fields:{}};await this.groupStore.save(a);const r=this.createGroupElement(t,e);if(!r)return null;const s=r.querySelector(this.selectors.grid);return this.groupElements.set(t,{element:r,grid:s,fieldId:e}),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),this.addGroupSelectionHandler(e,t),s&&this.createSortableForGrid(s,e,t),this.updateGroupCount(e),{id:t,element:r,grid:s}}createGroupElement(e,t){const o=window.getTemplate("imageGroup");if(!o)return null;o.dataset.groupId=e,o.dataset.fieldId=t;const a=window.getTemplate("groupMetadata"),r=o.querySelector(".fields");if(r&&a){r.append(a);const s=r.querySelector('[name="post_title"]'),i=r.querySelector('[name="post_excerpt"]');s&&(s.id=`${e}_title`,s.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const n=this.manager.fieldElements.get(t);if(n?.config.content){const e=o.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else o.querySelector("details")?.remove();const s=o.querySelector(this.selectors.grid);return s&&(s.dataset.groupId=e),o}async deleteGroup(e,t=!0){const o=this.groupElements.get(e);if(!o)return;const a=this.groupStore.get(e);if(t&&a?.uploads)for(const e of a.uploads)await this.removeFromGroup(e);await this.groupStore.delete(e),o.element?.remove(),this.groupElements.delete(e);const r=`${o.fieldId}-group-${e}`;this.manager.sortableInstances.get(r)?.destroy(),this.manager.sortableInstances.delete(r);const s=`${o.fieldId}_${e}`;this.manager.selectionHandlers.get(s)?.destroy(),this.manager.selectionHandlers.delete(s),this.updateGroupCount(o.fieldId),this.a11y.announce("Group removed")}async addToGroup(e,t){const o=this.manager.uploadStore.get(e),a=this.manager.uploadElements.get(e);if(!o||!a)return;const r=t.dataset.groupId,s=this.groupStore.get(r);if(o.groupId&&o.groupId!==r){const t=this.groupStore.get(o.groupId);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.deleteGroup(o.groupId,!1):await this.groupStore.save(t))}o.groupId=r,s&&!s.uploads.includes(e)&&(s.uploads.push(e),await this.groupStore.save(s)),await this.manager.uploadStore.save(o),t.appendChild(a.element),a.location=t;const i=a.element.querySelector('[name="featured"]');i&&(i.hidden=!1,i.name=`${r}_featured`);const n=a.element.querySelector('[name*="select-item"]');n&&(n.checked=!1),this.manager.updateSortableState(t)}async removeFromGroup(e){const t=this.manager.uploadStore.get(e),o=this.manager.uploadElements.get(e);if(!t||!o)return;const a=this.manager.fieldElements.get(t.fieldId);if(!a?.ui?.preview)return;if(t.groupId){const o=this.groupStore.get(t.groupId);o&&(o.uploads=o.uploads.filter((t=>t!==e)),0===o.uploads.length?await this.deleteGroup(t.groupId,!1):await this.groupStore.save(o)),t.groupId=null,await this.manager.uploadStore.save(t)}a.ui.preview.appendChild(o.element),o.location=a.ui.preview;const r=o.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.manager.updateSortableState(a.ui.preview)}async handleGroupMetaChange(e,t){const o=t.dataset.groupId,a=this.groupStore.get(o);if(!a)return;let r=e.name;r.includes(o)&&(r=r.replace(`${o}_`,"").replace(`${o}[`,"").replace("]","")),a.fields[r]=e.value,await this.groupStore.save(a)}updateGroupCount(e){const t=this.manager.fieldElements.get(e);if(!t)return;const o=this.getFieldGroups(e).length;t.element.dataset.hasGroups=o>0}async handleAddToGroup(e){const t=this.manager.selected.get(e);if(t&&0!==t.size){const o=await this.createGroup(e);if(!o)return;for(const e of t)await this.addToGroup(e,o.grid);this.manager.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`)}else await this.createGroup(e)}async handleDeleteGroup(e){const t=e.closest(this.selectors.container);t&&confirm("Delete this group? Items will be moved back to the upload area.")&&await this.deleteGroup(t.dataset.groupId,!0)}async handleRemoveFromGroup(e){const t=e.closest("[data-upload-id]");t&&(await this.removeFromGroup(t.dataset.uploadId),this.a11y.announce("Item moved to preview"))}addGroupSelectionHandler(e,t){const o=`${e}_${t}`;if(this.manager.selectionHandlers.has(o))return;const a=this.groupElements.get(t);if(!a?.element)return;const r=new window.jvbHandleSelection({container:a.element,ui:{selectAll:a.element.querySelector(this.selectors.selectAll),bulkControls:a.element.querySelector(this.selectors.actions),count:a.element.querySelector(this.selectors.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});r.subscribe(((t,o)=>{["item-selected","item-deselected","range-selected"].includes(t)&&this.manager.selected.set(e,o.selectedItems)})),this.manager.selectionHandlers.set(o,r)}async submitGroupedUploads(e){const t=this.manager.getFieldUploads(e),o=this.getFieldGroups(e),a=this.manager.fieldElements.get(e);if(0===t.length||!a)return;a.element.closest("details")?.removeAttribute("open"),document.body.classList.add("uploading");const r=new FormData,s=[],i=[];for(const e of o){const o=t.filter((t=>t.groupId===e.id)),a={images:[],fields:{...e.fields}};for(const e of o){const t=await this.manager.getBlobData(e.id);if(t){r.append("files[]",t),a.images.push({upload_id:e.id,index:i.length}),i.push(e.id);const o=this.manager.uploadElements.get(e.id),s=o?.element?.querySelector('[name*="featured"]');s?.checked&&(a.fields.featured=e.id)}}a.images.length>0&&s.push(a)}const n=t.filter((e=>!e.groupId));for(const e of n){const t=await this.manager.getBlobData(e.id);t&&(r.append("files[]",t),s.push({images:[{upload_id:e.id,index:i.length}],fields:{}}),i.push(e.id))}r.append("content",a.config.content),r.append("user",a.config.itemID),r.append("fieldId",e),r.append("posts",JSON.stringify(s)),r.append("upload_ids",JSON.stringify(i));const d={endpoint:"uploads/groups",method:"POST",data:r,title:`Creating ${s.length} ${a.config.content}${s.length>1?"s":""}...`,popup:`Creating ${s.length} post${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(d);for(const o of t)o.operationId=e,o.status="queued",await this.manager.uploadStore.save(o),this.manager.updateUploadUI(o.id);return this.a11y.announce(`Creating ${s.length} post${s.length>1?"s":""}`),e}catch(t){throw this.error.log(t,{component:"UploadGroups",action:"submitGroupedUploads",fieldId:e}),t}}async handleOperationComplete(e,t){const o=e.result?.data||e.serverData?.data||[];for(const e of o){const t=this.manager.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",await this.manager.uploadStore.save(t),this.manager.updateUploadUI(e.upload_id))}if(!t)return;const a=this.manager.getFieldUploads(t),r=new Set;for(const e of a)"completed"===e.status&&(e.groupId&&r.add(e.groupId),await this.manager.clearUpload(e.id));for(const e of r)await this.groupStore.delete(e),this.groupElements.get(e)?.element?.remove(),this.groupElements.delete(e);this.manager.updateFieldState(t),this.a11y.announce("All uploads completed successfully"),document.body.classList.remove("uploading")}async handleOperationFailed(e,t){const o=e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data?.upload_ids||[],a="operation-failed-permanent"===e.status?"failed_permanent":"failed";for(const e of o)await this.manager.updateUploadStatus(e,a);t&&this.manager.updateFieldState(t),document.body.classList.remove("uploading")}checkForRecovery(){if(!this.manager.storesReady||!this.groupStoreReady)return;if(this.hasCheckedForRecovery)return;const e=this.manager.uploadStore.getAll();if(!e.some((e=>e.groupId))&&this.manager.hasCheckedForRecovery)return void(this.hasCheckedForRecovery=!0);this.hasCheckedForRecovery=!0,this.manager.hasCheckedForRecovery=!0;const t=e.filter((e=>!e.operationId&&["processed","processing","queued"].includes(e.status)));if(0===t.length)return;const o=this.groupUploadsByField(t);this.showRecoveryNotification(o)}groupUploadsByField(e){const t=new Map;return e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,{fieldId:e.fieldId,pageUrl:e.pageUrl,uploads:[],groups:new Map});const o=t.get(e.fieldId);if(o.uploads.push(e),e.groupId){const t=this.groupStore.get(e.groupId);t&&o.groups.set(e.groupId,t)}})),t}async showRecoveryNotification(e){let t=0,o=0;e.forEach((e=>{t+=e.uploads.length,o+=e.groups.size}));const a=window.getTemplate("restoreNotification");if(!a)return;const r=o>0?`${o} group${o>1?"s":""} with ${t} upload${t>1?"s":""} can be restored.`:`${t} upload${t>1?"s":""} can be recovered.`,s=a.querySelector(".restore-details");s&&(s.textContent=r);for(const[t,o]of e){const e=window.getTemplate("restoreField");if(!e)continue;const r=e.querySelector("h3");r&&(r.textContent=t);const s=e.querySelector(".item-grid.restore");for(const e of o.uploads){const o=await this.manager.getBlobData(e.id);if(!o)continue;const a=this.manager.createUploadElement({id:e.id,preview:this.manager.createPreviewUrl(o),meta:e.meta,subtype:this.manager.getSubtypeFromMime(o.type)});a.dataset.fieldId=t,s?.appendChild(a)}a.querySelector(".wrap")?.appendChild(e)}document.querySelector(".field.upload")?.appendChild(a);const i=document.querySelector("dialog.restore-uploads");i&&(this.restoreModal=new window.jvbModal(i),this.restoreSelection=new window.jvbHandleSelection({container:i,ui:{selectAll:i.querySelector("#select-all-restore"),count:i.querySelector(".selection-count")}}),this.restoreModal.handleOpen(),i.addEventListener("click",this.handleRestoreAction.bind(this)))}handleRestoreAction(e){const t=e.target.closest("[data-action]")?.dataset.action;switch(t){case"restore":this.handleRestoreSelected();break;case"restore-all":this.handleRestoreAll();break;case"clear-cache":this.handleClearCache()}}async handleRestoreSelected(){const e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const o=e.closest(".item");o&&t.push({uploadId:o.dataset.uploadId,fieldId:o.dataset.fieldId})})),t.length>0&&await this.restoreUploads(t),this.cleanupRestoreModal()}async handleRestoreAll(){const e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{t.push({uploadId:e.dataset.uploadId,fieldId:e.dataset.fieldId})})),await this.restoreUploads(t),this.cleanupRestoreModal()}async restoreUploads(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,o]of t)await this.restoreFieldUploads(e,o)}async restoreFieldUploads(e,t){let o=this.manager.fieldElements.get(e);if(!o){const t=document.querySelector(`[data-uploader="${e}"]`);t&&(this.manager.registerUploader(t),o=this.manager.fieldElements.get(e),this.enhanceField(e))}if(!o)return void console.warn(`Field ${e} not found for restoration`);o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const a=this.getFieldGroups(e);for(const t of a)await this.restoreGroup(e,t);for(const o of t){const t=this.manager.uploadStore.get(o);t&&await this.restoreUploadElement(e,t)}this.manager.updateFieldState(e),this.manager.refreshSortable(e),this.manager.maybeLockUploads(e)}async restoreGroup(e,t){const o=await this.createGroup(e,t.id);if(o&&t.fields){const e=o.element.querySelector('[name*="post_title"]'),a=o.element.querySelector('[name*="post_excerpt"]');e&&t.fields.post_title&&(e.value=t.fields.post_title),a&&t.fields.post_excerpt&&(a.value=t.fields.post_excerpt)}}async restoreUploadElement(e,t){const o=this.manager.fieldElements.get(e);if(!o)return;const a=await this.manager.getBlobData(t.id);if(!a)return;const r=this.manager.createPreviewUrl(a),s=this.manager.createUploadElement({id:t.id,preview:r,meta:t.meta,subtype:this.manager.getSubtypeFromMime(a.type)});let i;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){i=e.grid;const o=s.querySelector('[name="featured"]');o&&(o.hidden=!1,o.name=`${t.groupId}_featured`)}else i=o.ui.preview}else i=o.ui.preview;i.appendChild(s),this.manager.uploadElements.set(t.id,{element:s,preview:r,location:i}),t.status="processed",await this.manager.uploadStore.save(t)}handleClearCache(){confirm("Discard these uploads?")&&(this.manager.uploadStore.clear(),this.groupStore.clear(),this.cleanupRestoreModal())}cleanupRestoreModal(){this.restoreModal&&(this.restoreModal.handleClose(),this.restoreSelection?.destroy(),this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null,this.restoreSelection=null)}destroy(){this.groupElements.forEach(((e,t)=>{const o=`${e.fieldId}-group-${t}`;this.manager.sortableInstances.get(o)?.destroy(),this.manager.sortableInstances.delete(o)})),this.groupElements.clear()}}document.addEventListener("DOMContentLoaded",(()=>{const t=()=>{window.jvbUploads&&!window.jvbUploadGroups&&(window.jvbUploadGroups=new e(window.jvbUploads))};window.jvbUploads?t():window.auth?.subscribe((e=>{"auth-loaded"===e&&setTimeout(t,50)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 3966c88..467fd2b 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.fieldStoreReady=!1,this.uploadStoreReady=!1,this.hasCheckedForUploads=!1;const{fields:e,uploads:t}=window.jvbStore.register("uploads",[{storeName:"fields",keyPath:"id",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},{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}]);this.fieldStore=e,this.uploadStore=t,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.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.init()}async init(){this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const o=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":o&&this.handleOperationCancelled(o);break;case"operation-status":o&&this.updateFieldStatus(o,t.status);break;case"operation-complete":this.handleOperationComplete(t,o);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,o)}})),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}}}scanFields(e,t){console.log(t,"autoUpload");e.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e,t)))}registerUploader(e,t){const o=this.determineFieldId(e),s=this.extractFieldConfig(e,t),a=this.buildFieldUI(e);console.log(s,"registering with config");const r={id:o,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(r),this.fieldElements.set(o,{element:e,ui:a,config:s}),e.dataset.uploader=o,this.addFieldSelectionHandler(o),"single"!==s.type&&this.initSortable(o),o}extractFieldConfig(e,t){return{autoUpload:t,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")}},o=e.querySelector(".group-display");return o&&(t.groups={display:o,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initSortable(e){if(!window.Sortable)return;!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0);const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((t=>{const o=t.classList.contains("group")?t.closest(".upload-group")?.dataset.groupId:null;this.createSortableForGrid(t,e,o)}));const o=t.element.querySelector(".empty-group");o&&!o.sortableInstance&&(o.sortableInstance=new Sortable(o,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:e,pull:!1,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:t=>this.handleDrop(t,e)}))}syncSortableSelection(e,t){this.sortableInstances.forEach(((o,s)=>{if(s.startsWith(e)){o.el.querySelectorAll(".item").forEach((e=>{const o=e.dataset.uploadId;t.has(o)?Sortable.utils.select(e):Sortable.utils.deselect(e)}))}}))}handleDrop(e,t){const o=e.to,s=e.from,a=e.items?.length>0?e.items:[e.item],r=a.map((e=>e.dataset.uploadId));switch(this.getDropTargetType(o)){case"empty-group":this.handleDropToEmptyGroup(a,r,t);break;case"preview":default:this.handleDropToPreview(a,r,t);break;case"group":this.handleDropToGroup(a,r,o,s,t)}this.updateSortableState(o),s!==o&&this.updateSortableState(s)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}handleDropToGroup(e,t,o,s,a){try{if(o===s)return void this.handleReorder({to:o,items:e});t.forEach((e=>{this.addToGroup(e,o,!1)})),this.schedulePersistance(a);const r=e.length>1?`Moved ${e.length} items to group`:"Moved item to group";this.a11y.announce(r);const i=this.selectionHandlers.get(a);i?.clearSelection()}catch(t){this.handleDropError(e,a,t)}}handleDropToPreview(e,t,o){try{t.forEach((e=>{this.removeFromGroup(e)})),this.schedulePersistance(o);const s=e.length>1?`Moved ${e.length} items to preview`:"Moved item to preview";this.a11y.announce(s);const a=this.selectionHandlers.get(o);a?.clearSelection()}catch(t){this.handleDropError(e,o,t)}}handleDropError(e,t,o,s="An error occurred"){console.error("Drop error:",o);const a=this.fieldElements.get(t);a?.ui?.preview&&e.forEach((e=>a.ui.preview.appendChild(e))),this.a11y.announce(`${s}. Items returned to preview.`)}handleDropToEmptyGroup(e,t,o){try{const s=this.createGroup(o);if(!s)return void this.handleDropError(e,o,new Error("Group creation failed"),"Failed to create group");e.forEach(((e,o)=>{s.grid.appendChild(e),this.addToGroup(t[o],s.grid,!1)})),this.schedulePersistance(o);const a=e.length>1?`Created group with ${e.length} items`:"Created group with item";this.a11y.announce(a);const r=this.selectionHandlers.get(o);r?.clearSelection()}catch(t){this.handleDropError(e,o,t)}}updateSortableState(e){const t=e?.sortableInstance;t&&t.option("disabled",!1)}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,o=t.closest(".field, .upload");if(!o)return;let s=Array.from(t.querySelectorAll(".item:not(.sortable-ghost):not(.sortable-clone)")).map((e=>e.dataset.uploadId)).filter((e=>e)),a=o.querySelector('input[type="hidden"]');a&&s.length>0&&(a.value=s.join(","));const r=this.getFieldIdFromElement(t);if(r){const e=this.getFieldData(r);if(t.classList.contains("group")){const o=t.dataset.groupId,a=e?.groups?.find((e=>e.id===o));a&&(a.uploads=s)}this.schedulePersistance(r)}this.a11y.announce("Item reordered"),o.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex,items:s},bubbles:!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 o=Array.from(e.dataTransfer.files);if(0===o.length)return;const s=this.getFieldIdFromElement(t);s&&(this.processFiles(s,o),this.a11y.announce(`${o.length} file(s) dropped for upload`))}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 o=Array.from(e.target.files);o.length>0&&t&&this.processFiles(t,o)}if(t){const o=this.getFieldData(t);if(!o.config.autoUpload)return;"post_group"===o?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const o=this.getFieldData(e),s=this.fieldElements.get(e);if(!o||!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups?.display&&(s.ui.groups.display.hidden=!1);const a=t.length;let r=0;this.updateUploadProgress(e,0,a,"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}};await this.uploadStore.save(n);const l=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,o.config.subtype):t;this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),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"===o.config.destination);s.ui.preview&&(s.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:l,location:s.ui.preview}));const p=this.uploadStore.get(i);return p&&(p.status="processed",await this.uploadStore.save(p)),o.uploads.add(i),await this.saveFieldData(o),r++,this.updateUploadProgress(e,r,a,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(o){return console.error("Error processing file:",t.name,o),r++,this.updateUploadProgress(e,r,a,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),o.config.autoUpload&&"post_group"!==o.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const o=this.worker.settings.timeout;return new Promise(((s,a)=>{let r,i=!1;r=setTimeout((()=>{i||(i=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),a(new Error(`Processing timeout for ${e.name}`)))}),o),this.worker.tasks.set(t,{file:e,timeoutId:r}),this.handleProcess(e,t).then((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),s(e))})).catch((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),a(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const o=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,o,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,o,.85)}async processOnMainThread(e,t,o){return new Promise(((s,a)=>{const r=new Image,i=document.createElement("canvas"),n=i.getContext("2d");let l=null;const d=()=>{r.onload=null,r.onerror=null,l&&(URL.revokeObjectURL(l),l=null),i.width=1,i.height=1,n.clearRect(0,0,1,1)};r.onload=()=>{try{const{width:l,height:c}=this.calculateOptimalDimensions(r,t);i.width=l,i.height=c,n.imageSmoothingEnabled=!0,n.imageSmoothingQuality="high",n.drawImage(r,0,0,l,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,o);i.toBlob((t=>{if(d(),t){const o=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});s(o)}else a(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),a(new Error(`Canvas processing failed: ${e.message}`))}},r.onerror=()=>{d(),a(new Error(`Failed to load image: ${e.name}`))};try{l=this.createPreviewUrl(e),r.src=l}catch(e){d(),a(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,o,s){return new Promise(((a,r)=>{if(!this.worker.worker)return void r(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 o=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});a(o)}else r(new Error(t.data.error||"Worker processing failed"))},l=e=>{this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),r(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:o,quality:s,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:o,height:s}=e;if(o<=t&&s<=t)return{width:o,height:s};const a=Math.min(t/o,t/s);return{width:Math.round(o*a),height:Math.round(s*a)}}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 o=Array.from(t.uploads);if(0===o.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const s=this.getFieldGroups(e);if(0===s.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const a=[],r=new FormData;let i=[];for(const e of s){const t={images:[],fields:{}};for(let[o,s]of Object.entries(e.changes))t.fields[o]=s;const s=o.filter((t=>{const o=this.uploadStore.get(t);return o?.groupId===e.id}));for(const e of s){const o=await this.getBlobData(e);if(o){r.append("files[]",o);const s={upload_id:e,index:i.length},a=this.uploadElements.get(e),n=a?.element?.querySelector('[name="featured"]');n?.checked&&(t.fields.featured=e),t.images.push(s),i.push(e)}}a.push(t)}const n=o.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of n){const t={images:[],fields:{}},o=await this.getBlobData(e);if(o){r.append("files[]",o);const s={upload_id:e,index:i.length};t.images.push(s),i.push(e)}a.push(t)}r.append("content",t.config.content),r.append("user",t.config.itemID),r.append("posts",JSON.stringify(a)),r.append("upload_ids",JSON.stringify(i));const l={endpoint:"uploads/groups",method:"POST",data:r,title:`Creating ${a.length} ${t.config.content}${a.length>1?"s":""} from uploads...`,popup:`Creating ${a.length} post${a.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(l);return o.forEach((t=>{const o=this.uploadStore.get(t);o&&(o.operationId=e,o.status="queued",this.uploadStore.save(o),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),this.a11y.announce(`Creating ${a.length} post${a.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 o=Array.from(t.uploads),s=this.prepareUploadData(t,o);this.a11y.announce("Queuing for upload");const a={endpoint:"uploads",method:"POST",data:s,title:`Uploading ${o.length} file${o.length>1?"s":""} to server...`,popup:`Uploading ${o.length} file${o.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(a);return o.forEach((t=>{const o=this.uploadStore.get(t);o&&(o.operationId=e,o.status="queued",this.uploadStore.save(o),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async prepareUploadData(e,t){const o=new FormData;o.append("content",e.config.content),o.append("mode",e.config.mode),o.append("field_name",e.config.name),o.append("fieldId",e.id),o.append("field_type",e.config.type),o.append("subtype",e.config.subtype),o.append("item_id",e.config.itemID),o.append("destination",e.config.destination||"meta");let s=[];const a=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const a=await this.getBlobData(e);a&&(o.append("files[]",a),s.push(t.id))}));return await Promise.all(a),o.append("upload_ids",JSON.stringify(s)),o}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),o=this.uploadStore.get(t);if(!o)return;if(!this.getFieldData(o.fieldId))return;let s={};s[e.target.name]=e.target.value,o.meta={...o.meta,...s},await this.uploadStore.save(o);let a={};a[o.attachmentId??o.id]=o.meta;const r={endpoint:"uploads/meta",method:"POST",data:a,title:"Updating meta",canMerge:!0,headers:{action_nonce:window.auth.getNonce("dash")}};try{await this.queue.addToQueue(r)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:o.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 o=this.getFieldData(t);if(!o)return;const s=Array.from(o.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of s)await this.clearUpload(e,!1),o.uploads.delete(e);0===o.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(o),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const o=this.uploadStore.get(t);o&&(o.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(o),this.updateUploadStatus(t,o.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const o=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of o)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 o=e.closest(".item");o&&t.push({uploadId:o.dataset.uploadId,fieldId:o.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,o]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=o,await this.restoreField(t))}}async restoreField(e){const{config:t,context:o,uploads:s,groups:a,id:r}=e;o?.modalType&&await this.openModalForRestore(o);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),a&&a.length>0&&await this.restoreGroups(n,a);const c=s instanceof Set?Array.from(s):Array.isArray(s)?s:[];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),console.log(t),t.autoUpload&&"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(n)}async restoreUpload(e,t){const o=this.fieldElements.get(e),s=this.getFieldData(e);if(!o||!s)return void console.error("Field not found for upload restoration:",e);const a=await this.getBlobData(t.id);if(!a)return void console.warn("Blob data not found for upload:",t.id);const r=this.createPreviewUrl(a),i=this.getSubtypeFromMime(a.type),n=this.createUploadElement({id:t.id,preview:r,meta:t.meta||{originalName:a.name,size:a.size,type:a.type},subtype:i},"post_group"===s.config.destination);let l;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){l=e.grid;const o=s.groups?.find((e=>e.id===t.groupId));o&&(o.uploads||(o.uploads=[]),o.uploads.includes(t.id)||o.uploads.push(t.id))}else l=o.ui.preview,t.groupId=null}else l=o.ui.preview;l?l.appendChild(n):o.ui.preview&&(o.ui.preview.appendChild(n),l=o.ui.preview),this.uploadElements.set(t.id,{element:n,preview:r,location:l}),s.uploads||(s.uploads=new Set),s.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),l&&this.updateSortableState(l)}async restoreGroups(e,t){const o=this.fieldElements.get(e),s=this.getFieldData(e);if(o&&s){for(const o of t){const t=this.createGroup(e,o.id);if(!t){console.warn("Failed to create group:",o.id);continue}const a=s.groups?.find((e=>e.id===o.id));if(a&&(o.changes&&(a.changes={...o.changes}),o.uploads&&(a.uploads=[...o.uploads]),o.changes)){const e=t.element.querySelector('[name*="post_title"]'),s=t.element.querySelector('[name*="post_excerpt"]');e&&o.changes.post_title&&(e.value=o.changes.post_title),s&&o.changes.post_excerpt&&(s.value=o.changes.post_excerpt)}}await this.saveFieldData(s)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:o}=e;let s=null;switch(t){case"create":s=document.querySelector('[data-action="create"]');break;case"edit":o&&(s=document.querySelector(`[data-action="edit"][data-id="${o}"]`));break;case"bulkEdit":s=document.querySelector('[data-action="bulk-edit"]')}s?(s.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 o=t<0?0:t,s=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,s)).toFixed(o))+" "+["Bytes","KB","MB","GB"][s]}async clearUpload(e,t=!0){const o=this.uploadElements.get(e);if(o&&(this.revokePreviewUrl(o.preview),o.element)){const e=o.element.dataset.previewUrl;this.revokePreviewUrl(e),delete o.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),o=this.getFieldData(e);if(!t||!o)return;const s=t.element,a=o.uploads?.size||0,r=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=a>0?"true":"false",s.dataset.uploadCount=a.toString(),s.dataset.hasGroups=r?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${a} item${1!==a?"s":""}`)}updateUploadProgress(e,t,o,s){const a=this.fieldElements.get(e);if(!a?.ui?.progress?.progress)return;const r=a.ui.progress,i=o>0?t/o*100:0;r.fill&&(r.fill.style.width=`${i}%`),r.text&&(r.text.textContent=s),r.count&&(r.count.textContent=`${t}/${o}`),r.progress.hidden=t===o}updateFieldStatus(e,t){const o=this.getFieldData(e);o&&(o.state=t,this.saveFieldData(o))}updateUploadStatus(e,t){const o=this.uploadStore.get(e);o&&(o.status=t,this.uploadStore.save(o),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),o=this.uploadStore.get(e);if(!o||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${o.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(o.status),o.status)}showUploadProgress(e,t=!0){const o=this.uploadElements.get(e);if(!o?.element)return;const s=o.element.querySelector(".progress");s&&(t?(s.style.removeProperty("animation"),s.hidden=!1):(s.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{s.hidden=!0}),300)))}updateUploadItemProgress(e,t,o=null){const s=this.uploadElements.get(e);if(!s?.element)return;const a=s.element.querySelector(".progress");if(!a)return;const r=a.querySelector(".fill"),i=a.querySelector(".details"),n=a.querySelector(".icon");r&&(r.style.width=`${t}%`),o&&i&&(i.textContent=this.getStatusText(o)),o&&n&&(n.innerHTML=this.getStatusIcon(o).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),o=this.getFieldData(e);if(!t?.ui?.dropZone||!o)return;const s=o.uploads?.size||0,a="post_group"===o.config.destination?20:o.config?.maxFiles||999;t.ui.dropZone.hidden=s>=a,t.element.classList.toggle("at-max-uploads",s>=a),"post_group"===o.config.destination&&s>=a&&this.a11y.announce("Maximum of 20 uploads reached. Please submit current uploads before adding more.")}createSortableForGrid(e,t,o=null){if(!e||e.sortableInstance)return;const s=new Sortable(e,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:t,pull:!0,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleDrop(e,t),onSelect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&!t.checked&&(t.checked=!0,t.dispatchEvent(new Event("change",{bubbles:!0})))},onDeselect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&t.checked&&(t.checked=!1,t.dispatchEvent(new Event("change",{bubbles:!0})))},onAdd:e=>this.updateSortableState(e.to),onRemove:e=>this.updateSortableState(e.from)});e.sortableInstance=s;const a=o?`${t}-group-${o}`:`${t}-preview`;return this.sortableInstances.set(a,s),s}createGroup(e,t=null){const o=this.getFieldData(e),s=this.fieldElements.get(e);if(!o||!s)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const a=this.createGroupElement(t,e);if(!a)return null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,a),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(a,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(a);const r=a.querySelector(".item-grid.group");this.groupElements.set(t,{element:a,grid:r,fieldId:e}),o.groups||(o.groups=[]);return o.groups.find((e=>e.id===t))||(o.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(o)),this.addGroupSelectionHandler(e,t),r&&this.createSortableForGrid(r,e,t),{id:t,element:a,grid:r}}createGroupElement(e,t){let o=window.getTemplate("imageGroup");if(!o)return;o.dataset.groupId=e,o.dataset.fieldId=t;let s=window.getTemplate("groupMetadata");const a=o.querySelector(".fields");if(a&&s){a.append(s);const r=a.querySelector('[name="post_title"]'),i=a.querySelector('[name="post_excerpt"]');r&&(r.id=`${e}_title`,r.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=o.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else o.querySelector("details")?.remove();const r=o.querySelector(".item-grid.group");return r&&(r.dataset.groupId=e),o}deleteGroup(e,t=!0){const o=this.groupElements.get(e);if(!o)return;const s=this.getFieldData(o.fieldId);if(!s)return;const a=s.groups?.find((t=>t.id===e));let r=!0;t&&a?.uploads?.length>0&&(r=!window.confirm("Delete uploads in group?")),t&&r&&a?.uploads&&a.uploads.forEach((e=>{this.removeFromGroup(e)})),s.groups&&(s.groups=s.groups.filter((t=>t.id!==e)),this.saveFieldData(s)),o.element&&(o.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${o.fieldId}-group-${e}`,n=this.sortableInstances.get(i);n?.destroy&&n.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(o.fieldId)}addToGroup(e,t=null,o=!0){const s=this.uploadStore.get(e),a=this.uploadElements.get(e);if(!s||!a)return;const r=this.getFieldData(s.fieldId),i=this.fieldElements.get(s.fieldId);if(!r||!i)return;if(!t&&a.location===i.ui.preview||t===a.location)return;if(s.groupId){const t=r.groups?.find((e=>e.id===s.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(s.groupId))}const n=a.element.querySelector('[name*="select-item"]');n&&(n.checked=!1);let l=a.element.querySelector('[name="featured"]');if(l&&(l.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,s.groupId=null;else{const o=t.dataset.groupId;l&&(l.name=o+"_"+l.name);const a=r.groups?.find((e=>e.id===o));a&&(a.uploads||(a.uploads=[]),a.uploads.push(e),s.groupId=o)}a.location=t,t.append(a.element),this.uploadStore.save(s),o&&this.saveFieldData(r),this.updateSortableState(t),a.location&&a.location!==t&&this.updateSortableState(a.location)}removeFromGroup(e){const t=this.uploadStore.get(e),o=this.uploadElements.get(e);if(!t||!o)return;const s=this.getFieldData(t.fieldId),a=this.fieldElements.get(t.fieldId);if(!s||!a)return;if(t.groupId){const o=s.groups?.find((e=>e.id===t.groupId));o&&(o.uploads=o.uploads.filter((t=>t!==e)),0===o.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}a.ui?.preview&&(a.ui.preview.appendChild(o.element),o.location=a.ui.preview);const r=o.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.uploadStore.save(t),this.updateSortableState(a.ui.preview)}removeUpload(e,t){const o=this.getFieldData(e),s=this.uploadStore.get(t),a=this.uploadElements.get(t);if(!o||!s)return;if(o.uploads?.delete(t),s.groupId){const e=o.groups?.find((e=>e.id===s.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(s.groupId))}a?.element?.remove(),this.clearUpload(t),this.saveFieldData(o),this.updateFieldState(e),this.maybeLockUploads(e);const r=this.selectionHandlers.get(e);r&&r.deselect(t),this.a11y.announce("Upload removed")}handleGroupMetaChange(e){const t=this.getGroupFromElement(e);if(!t)return;const o=this.getFieldData(t.fieldId),s=o?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!s)return;s.changes||(s.changes={});let a=e.name;a.includes("group")&&(a=a.replace(`${s.id}_`,"").replace(`${s.id}[`,"").replace("]","")),s.changes[a]=e.value,this.saveFieldData(o),this.schedulePersistance(t.fieldId)}handleAction(e){const t=e.dataset.action,o=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(o);t&&(t.element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(o));break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"restore-all":this.handleRestoreAll().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),o=t?.dataset.uploader;if(!o)return;const s=this.selected.get(o);if(s&&0!==s.size){const e=this.createGroup(o);if(!e)return;s.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(o);t?.clearSelection(),this.a11y.announce(`Created group with ${s.size} items`)}else this.createGroup(o);this.schedulePersistance(o)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const o=t.dataset.groupId,s=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(o),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(s)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const o=t.dataset.uploadId,s=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(s,o),this.a11y.announce("Item removed"),this.schedulePersistance(s))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fieldElements.get(e);if(!t?.element)return;const o=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 o.subscribe(((t,o)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.syncSortableSelection(e,o.selectedItems),this.selected.set(e,o.selectedItems);break;case"select-all":this.handleSelectAll(o.container,o.selected)}})),this.selectionHandlers.set(e,o),o}addGroupSelectionHandler(e,t){const o=`${e}_${t}`;if(this.selectionHandlers.has(o))return this.selectionHandlers.get(o);const s=this.groupElements.get(t);if(!s?.element)return;const a=new window.jvbHandleSelection({container:s.element,ui:{selectAll:s.element.querySelector(this.selectors.groups.selectAll),bulkControls:s.element.querySelector(this.selectors.groups.actions),count:s.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return a.subscribe(((t,o)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,o.selectedItems);break;case"select-all":this.handleSelectAll(o.container,o.selected)}})),this.selectionHandlers.set(o,a),a}handleSelectAll(e,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 o={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 o=this.getFieldData(t.fieldId);return o?.groups?.find((t=>t.id===e))}}},s=o[t];if(!s)return null;const a=e.closest(s.selector);if(!a)return null;const r=a.dataset[s.key];return{...s.getRuntimeData(r),...s.getStoreData(r)}}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}createUploadElement(e,t=!1){let o=window.getTemplate("uploadItem");if(!o)return;o.dataset.uploadId=e.id,o.dataset.subtype=e.subtype||"image";let[s,a,r,i,n]=[o.querySelector('[name="featured"]'),o.querySelector("img"),o.querySelector("video"),o.querySelector("label > span"),o.querySelector("details")];switch(s&&(s.value=e.id),e.subtype){case"image":a&&(a.src=e.preview,a.alt=e.meta?.originalName||""),r?.remove(),i?.remove();break;case"video":r&&(r.src=e.preview),a?.remove(),i?.remove();break;case"document":const t=e.meta?.originalName||"",o=t.split(".").pop()?.toLowerCase()||"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},n=window.getIcon(s[o]||"file");i&&(i.innerText=t,i.prepend(n)),a?.remove(),r?.remove()}if(n){let e=window.getTemplate("uploadMeta");e&&n.append(e)}return o.draggable=t,o.querySelectorAll("input").forEach((t=>{let o=t.id;if(o){let s=o+e.id,a=t.parentNode.querySelector(`label[for="${o}"]`);t.id=s,a&&(a.htmlFor=s)}})),o}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),250)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const o=await t.arrayBuffer(),s=this.uploadStore.get(e)||{id:e};s.blobData={buffer:o,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(s)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const o=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([o],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}async getFilesForForm(e){const t=e.querySelectorAll("[data-upload-field]"),o=[];for(const e of t){const t=this.determineFieldId(e),s=await this.getFilesForField(t);o.push(...s)}return o}async getFilesForField(e){const t=this.getFieldData(e);if(!t?.uploads)return[];const o=[],s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s){const s=this.uploadStore.get(e);if(!s)continue;const a=await this.getBlobData(e);a&&o.push({file:a,uploadId:e,fieldName:t.config.name,meta:s.meta||{}})}return o}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.fieldStoreReady=!0,this.checkIfBothStoresReady()}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.uploadStoreReady=!0,this.checkIfBothStoresReady();break;case"item-saved":this.showSaveIndicator(t.key)}}checkIfBothStoresReady(){this.fieldStoreReady&&this.uploadStoreReady&&!this.hasCheckedForUploads&&(this.hasCheckedForUploads=!0,this.checkForStoredUploads())}async checkForStoredUploads(){const e=this.fieldStore.getAll().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)}))}));0!==e.length&&await this.showRecoveryNotification(e)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),o=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let s,a=window.getTemplate("restoreNotification");if(!a)return void console.error("Restore notification template not found");if(o>0){s=`${o} ${o>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else s=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const r=a.querySelector(".restore-details");r&&(r.textContent=s);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const o=e.querySelector("h3");o&&(o.textContent=t.config.name||"Unnamed Field");const s=e.querySelector(".item-grid.restore");for(let e of t.uploads){const o=this.uploadStore.get(e);let a=window.getTemplate("uploadItem");if(!a)continue;const r=await this.getBlobData(o.id);if(r)try{const e=this.createPreviewUrl(r);let[s,i,n,l,d]=[a.querySelector('[name="featured"]'),a.querySelector("img"),a.querySelector("video"),a.querySelector("label > span"),a.querySelector("details")];a.dataset.uploadId=o.id,a.dataset.fieldId=t.id;let c=this.getSubtypeFromMime(r.type);switch(a.dataset.subtype=c,c){case"image":[i.src,i.alt]=[e,r.name??o.meta?.originalName??""],n.remove(),l.remove();break;case"video":n.src=e,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=o.originalFile.name,l.prepend(t),i.remove(),n.remove()}a.dataset.previewUrl=e}catch(e){console.warn("Failed to create preview for upload:",o.id,e)}const i=a.querySelector("summary span");i&&(i.textContent=o.meta?.originalName||"Unknown file");const n=a.querySelector("details");n&&o.meta&&(n.textContent=`${this.formatBytes(o.meta.size)} • ${o.meta.type}`),a.querySelectorAll("input").forEach((e=>{let t=e.id;if(t){let s=t+o.id,a=e.parentNode.querySelector(`label[for="${t}"]`);e.id=s,a&&(a.htmlFor=s)}})),s&&s.appendChild(a)}a.querySelector(".wrap").appendChild(s)}document.querySelector(".field.upload").appendChild(a),a=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(a),this.restoreSelection=new window.jvbHandleSelection({container:a,ui:{selectAll:a.querySelector("#select-all-restore"),count:a.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())}async handleRestoreAll(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{let o=e.dataset.uploadId,s=e.dataset.fieldId;t.push({uploadId:o,fieldId:s})})),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((o=>{try{o(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",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners()}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),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;if(s)switch(e){case"cancel-operation":this.handleOperationCancelled(s).then((()=>{}));break;case"operation-status":this.handleFieldStatus(s,t).then((()=>{}));break;case"operation-completed":this.handleOperationComplete(t,s).then((()=>{}));break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s).then((()=>{}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery())}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:'[name="select-all-uploads"]',actions:".selection-actions",count:".selection-count",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(!t)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.closest(this.selectors.group.fields);if(!t)return;const s=t.dataset.groupId,r=this.stores.groups.get(s);r&&window.debouncer.schedule(`group-meta-${s}`,(async(e,t)=>{let s=e.name.replace(`${t}_`,"").replace(`${t}[`,"").replace("]","");r.fields[s]=e.value,await this.setGroup(t,r)}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,c;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,c=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,c=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let p=this.sendToQueue(e,s,u,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return p?(r.operationId=p,await this.setBulkUpload(i,"operationId",p),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",p),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),p}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const s={images:[],fields:e.fields??{}},a=t.filter((t=>t.group===e.id));for(const e of a){const t=this.formatFile(e);if(t){o.push(t);const r={upload_id:e.id,index:i.length};let a=this.uploads.get(e.id);a.ui?.featured?.checked&&(s.fields.featured=e.id),s.images.push(r),i.push(e.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}async handleOperationComplete(e,t){const s=e.response;if(s?.data){const e=Array.isArray(s.data)?s.data:Object.values(s.data);for(const t of e)if(t.upload_id&&t.attachment_id){const e=this.stores.uploads.get(t.upload_id);e&&(e.attachmentId=t.attachment_id,e.status="completed",await this.stores.uploads.save(e))}}const r=this.stores.uploads.filterByIndex({field:t}),i=this.stores.groups.filterByIndex({field:t});await Promise.all([...r.filter((e=>"completed"===e.status)).map((e=>this.clearUpload(e.id))),...i.map((e=>this.stores.groups.delete(e.id)))]),this.notify("uploads-complete",{fieldId:t,response:s})}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image"}}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]});if(0===e.length)return;let t=window.getTemplate("restoreNotification");if(!t)return void this.error.log("No restore notification",{component:"UploadManager",src:window.location.href});const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));const r=window.location.href;let i=s.size>1?` across ${s.size} pages`:"",o=e.length>1?"uploads":"upload",a=`${e.length} ${o} can be recovered${i}`,l=t.querySelector(".details");l&&(l.textContent=a);let n=1;for(const[e,i]of s){let s=window.getTemplate("restoreField");if(!s)continue;let o=this.registerField(s,!1,"recovery_"+n),a=this.fields.get(o);n++;let l=e===r,[d,u,c]=[s.querySelector("h3"),s.querySelector("h3 a"),s.querySelector(".item-grid")];s.open=l,l?(u.remove(),d.textContent="From this page:"):[u.href,u.title,u.textContent]=[e,"Navigate to Page and Restore",e];let p=[...new Set(i.map((e=>e.group??"preview")))];for(let e of p){let t="preview"===e||this.stores.groups.get(e);if(!t)continue;let s=await this.createGroupElement(e,a.id),r=s.querySelector(".item-grid"),o=i.filter((t=>t.group===("preview"===e)?null:e));for(const[e,r]of Object.entries(t.fields??{})){let t=s.querySelector(`input[name*="${e}"]`);t&&(t.value=r)}for(let e of o){let t=await this.createUpload(e.id,this.formatFile(e),a.id);r.append(t)}c.append(s)}t.querySelector(".wrap").append(s)}document.body.append(t),t=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(t),this.restoreSelection=new window.jvbHandleSelection({container:t,wrapper:".restore-uploads .wrap",bulkControls:".selection-actions",selectAll:"#select-all-restore",count:".selection-count"}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{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"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let r=window.getTemplate("uploadItem");if(!r)return null;let i=this.fields.get(s);if(!i)return null;r.dataset.uploadId=e;let o=this.getSubtypeFromMime(t.type)||"image";r.dataset.subtype=o;let[a,l,n,d,u]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];switch(a&&(a.value=e),o){case"image":if(l){const e=this.createPreviewUrl(t);l.src=e,l.alt=t.name||"",l.dataset.previewUrl=e}n?.remove(),d?.remove();break;case"video":if(n){const e=this.createPreviewUrl(t);n.src=e,n.dataset.previewUrl=e}l?.remove(),d?.remove();break;case"document":let e=t.name.split(".").pop()?.toLowerCase()??"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},r=window.getIcon(s[e]??"file");d&&(d.innerText=t.name,d.prepend(r)),l?.remove(),n?.remove()}if(u){let e=window.getTemplate("uploadMeta");e&&u.append(e)}return r.draggable="single"!==i.config.type??!1,r.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e,i=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,i&&(i.htmlFor=r)}})),r}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>("status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e))));await Promise.all(r)}async setUploadStatus(e,t){e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&await this.removeGroup(s.id,!1)}await this.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const r=this.createGroupElement(t,e);if(!r)return null;s.groupUI.grid.append(r);const i=r.querySelector(".item-grid");i&&(i.dataset.groupId=t,this.createSortableForGrid(e,i,t));let o=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...o,id:t,field:e}),t}createGroupElement(e,t=null){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,t&&(s.dataset.fieldId=t);const r=s.querySelector("[data-select-all]");if(r){const t=`select-all-${e}`,i=s.querySelector(`label[for="${r.id}"]`);r.id=t,r.name=t,i&&(i.htmlFor=t)}let i=window.getTemplate("groupMetadata"),o=s.querySelector(".fields");if(i&&o){o.append(i);let t=o.querySelector('[name="post_title"]'),s=o.querySelector('[name="post_excerpt"]');t&&(t.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(s.id=`${e}_excerpt`,s.name=`${e}[post_excerpt]`)}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid");return a&&(a.dataset.groupId=e),this.groups.set(e,{element:s,ui:window.uiFromSelectors(this.selectors.group,s)}),s}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&await this.removeGroup(t.id,!1))}if(r.ui.checkbox&&(r.ui.checkbox.checked=!1),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,this.stores.groups.save(i))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:i.ui.grid;o&&o.append(r.element),this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));const o=this.getGroupKey(r.field,e),a=this.sortables.get(o);a?.destroy&&a.destroy(),this.sortables.delete(o),s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all([...t.map((e=>this.removeUpload(e.id))),...s.map((e=>this.removeGroup(e.id,!1)))]),this.a11y.announce("Upload Cancelled")}async handleOperationFailed(e,t){await this.setBulkUpload(this.stores.uploads.filterByIndex({field:t}),"status","failed")}async handleFieldStatus(e,t){let s=t.status,r=this.stores.uploads.filterByIndex({field:e});await this.setBulkUpload(r,"status",s)}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;let r=new window.jvbHandleSelection({container:s.element,item:this.selectors.items.item,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions,checkbox:this.selectors.items.checkbox,selectAll:this.selectors.fields.selectAll,wrapper:`${this.selectors.fields.preview}, ${this.selectors.group.item}`});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),console.log(Array.from(this.selected)),this.syncSortableSelection(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},ghostClass:"ghost",chosenClass:"chosen",dragClass:"dragging",onStart:()=>this.syncSortableSelection(e),onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;await Promise.all(r.map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(t)?.clearSelection()}syncSortableSelection(e){const t=this.selected.get(e)||new Set;for(const[s,r]of this.uploads){const i=this.stores.uploads.get(s);if(!i||i.field!==e)continue;const o=r.element;if(!o)continue;const a=t.has(s);a&&!o.classList.contains("selected")?Sortable.utils.select(o):!a&&o.classList.contains("selected")&&Sortable.utils.deselect(o)}}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.querySelectorAll(this.selectors.items.item+":not(.ghost)")).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.groups.get(t);e&&(e.uploads=r)}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}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(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(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 1f5ffb6..dc0f567 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=e-new Date,i=n<0,o=Math.floor(Math.abs(n)/1e3),r=Math.floor(o/60),s=Math.floor(r/60),a=Math.floor(s/24);if(0===r)return"Just now";let c="";if(o<10)c="a moment";else if(o<60)c="less than a minute";else if(r<5)c="a few minutes";else if(s<24)c=0===s?`${r} ${1===r?"minute":"minutes"}`:`${s} ${1===s?"hour":"hours"}`;else{if(!(a<7))return e.toLocaleDateString();c=`${a} ${1===a?"day":"days"}`}return i?`${c} ago`:`in ${c}`},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.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.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.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-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},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 s=!0;return async function(){for(;s;)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(){s=!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 s=this.map(t[o],r);null!==s&&(s.hasOwnProperty("type")&&s.hasOwnProperty("data")?n[o]=s.data:n[o]=s,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.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()}};document.body;const t=document.documentElement,e=document.querySelector(".scroll-progress .bar");let n=window.scrollY||t.scrollTop||0,i=-1,o=!1,r=0;function s(){r=Math.max(0,t.scrollHeight-window.innerHeight)}function a(t){if(!e)return;const n=r>0?t/r:0,i=Math.max(0,Math.min(1,n));e.style.transform=`scaleX(${i})`}function c(){const e=window.scrollY||t.scrollTop||0;e>n?i=1:e<n&&(i=-1),n=e,document.body.classList.toggle("scroll-up",i<0&&e>0),a(e),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(c))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{s(),a(window.scrollY||t.scrollTop||0)}),20)})),s(),a(n)})();
\ 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=e-new Date,i=n<0,o=Math.floor(Math.abs(n)/1e3),r=Math.floor(o/60),s=Math.floor(r/60),a=Math.floor(s/24);if(0===r)return"Just now";let c="";if(o<10)c="a moment";else if(o<60)c="less than a minute";else if(r<5)c="a few minutes";else if(s<24)c=0===s?`${r} ${1===r?"minute":"minutes"}`:`${s} ${1===s?"hour":"hours"}`;else{if(!(a<7))return e.toLocaleDateString();c=`${a} ${1===a?"day":"days"}`}return i?`${c} ago`:`in ${c}`},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.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.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.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-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},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.generateID=function(t="jvb"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(t,e,n,i="",o=""){const r=e<n;t.progress&&r&&window.fade(t.progress,!0);const s=n>0?e/n*100:0;t.fill&&(t.fill.style.width=`${s}%`),t.details&&(t.details.textContent=i),t.count&&(t.count.textContent=`${e}/${n}`),t.icon&&(t.icon.className=""===o?"icon":"icon icon-"+o),t.progress&&e===n&&window.fade(t.progress,!1)},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 s=!0;return async function(){for(;s;)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(){s=!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 s=this.map(t[o],r);null!==s&&(s.hasOwnProperty("type")&&s.hasOwnProperty("data")?n[o]=s.data:n[o]=s,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.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()}};document.body;const t=document.documentElement,e=document.querySelector(".scroll-progress .bar");let n=window.scrollY||t.scrollTop||0,i=-1,o=!1,r=0;function s(){r=Math.max(0,t.scrollHeight-window.innerHeight)}function a(t){if(!e)return;const n=r>0?t/r:0,i=Math.max(0,Math.min(1,n));e.style.transform=`scaleX(${i})`}function c(){const e=window.scrollY||t.scrollTop||0;e>n?i=1:e<n&&(i=-1),n=e,document.body.classList.toggle("scroll-up",i<0&&e>0),a(e),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(c))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{s(),a(window.scrollY||t.scrollTop||0)}),20)})),s(),a(n)})();
\ No newline at end of file
diff --git a/base/_setup.php b/base/_setup.php
index e8d7871..9e46047 100644
--- a/base/_setup.php
+++ b/base/_setup.php
@@ -18,3 +18,14 @@
 
 $childURL = apply_filters('jvbChildUrl', JVB_URL);
 define('JVB_CHILD_URL', $childURL);
+
+
+function jvbDefaultIcon():string
+{
+	return apply_filters('jvbDefaultIcon', 'arrows-clockwise');
+}
+
+function jvbLogoIcon():string
+{
+	return apply_filters('jvbLogoIcon', 'logo');
+}
diff --git a/build/drawer-menu/block.json b/build/drawer-menu/block.json
new file mode 100644
index 0000000..605e348
--- /dev/null
+++ b/build/drawer-menu/block.json
@@ -0,0 +1,27 @@
+{
+  "$schema": "https://schemas.wp.org/trunk/block.json",
+  "apiVersion": 3,
+  "name": "jvb/drawer-menu",
+  "title": "Drawer Menu",
+  "category": "jvb",
+  "icon": "menu",
+  "version": "1.0.0",
+  "textdomain": "jvb",
+  "supports": {
+    "html": false
+  },
+  "attributes": {
+    "menuId": {
+      "type": "string",
+      "default": ""
+    },
+    "collapsed": {
+      "type": "boolean",
+      "default": true
+    }
+  },
+  "editorScript": "file:./index.js",
+  "editorStyle": "file:./index.css",
+  "style": "file:./style-index.css",
+  "render": "file:./render.php"
+}
\ No newline at end of file
diff --git a/build/drawer-menu/index-rtl.css b/build/drawer-menu/index-rtl.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/build/drawer-menu/index-rtl.css
@@ -0,0 +1 @@
+
diff --git a/build/drawer-menu/index.asset.php b/build/drawer-menu/index.asset.php
new file mode 100644
index 0000000..8180b18
--- /dev/null
+++ b/build/drawer-menu/index.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components'), 'version' => '5613e7295e6abf1d1090');
diff --git a/build/drawer-menu/index.css b/build/drawer-menu/index.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/build/drawer-menu/index.css
@@ -0,0 +1 @@
+
diff --git a/build/drawer-menu/index.js b/build/drawer-menu/index.js
new file mode 100644
index 0000000..687097a
--- /dev/null
+++ b/build/drawer-menu/index.js
@@ -0,0 +1 @@
+(()=>{"use strict";var e,r={582:()=>{const e=window.wp.blocks,r=window.wp.blockEditor,n=window.wp.components,o=window.ReactJSXRuntime;(0,e.registerBlockType)("jvb/drawer-menu",{edit:function({attributes:e,setAttributes:t}){const{menuId:l,collapsed:s}=e,i=(0,r.useBlockProps)();return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(r.InspectorControls,{children:(0,o.jsxs)(n.PanelBody,{title:"Drawer Settings",children:[(0,o.jsx)(n.TextControl,{label:"Menu ID",value:l,onChange:e=>t({menuId:e}),help:"PHP-generated menu identifier"}),(0,o.jsx)(n.ToggleControl,{label:"Start Collapsed",checked:s,onChange:e=>t({collapsed:e})})]})}),(0,o.jsx)("div",{...i,children:(0,o.jsxs)("div",{className:"drawer-menu-preview",children:[(0,o.jsxs)("p",{children:["Drawer Menu: ",l||"Not configured"]}),(0,o.jsxs)("p",{children:["State: ",s?"Collapsed":"Expanded"]})]})})]})},save:function(){return null}})}},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var l=n[e]={exports:{}};return r[e](l,l.exports,o),l.exports}o.m=r,e=[],o.O=(r,n,t,l)=>{if(!n){var s=1/0;for(c=0;c<e.length;c++){for(var[n,t,l]=e[c],i=!0,a=0;a<n.length;a++)(!1&l||s>=l)&&Object.keys(o.O).every((e=>o.O[e](n[a])))?n.splice(a--,1):(i=!1,l<s&&(s=l));if(i){e.splice(c--,1);var d=t();void 0!==d&&(r=d)}}return r}l=l||0;for(var c=e.length;c>0&&e[c-1][2]>l;c--)e[c]=e[c-1];e[c]=[n,t,l]},o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),(()=>{var e={121:0,397:0};o.O.j=r=>0===e[r];var r=(r,n)=>{var t,l,[s,i,a]=n,d=0;if(s.some((r=>0!==e[r]))){for(t in i)o.o(i,t)&&(o.m[t]=i[t]);if(a)var c=a(o)}for(r&&r(n);d<s.length;d++)l=s[d],o.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return o.O(c)},n=globalThis.webpackChunkjvb=globalThis.webpackChunkjvb||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))})();var t=o.O(void 0,[397],(()=>o(582)));t=o.O(t)})();
\ No newline at end of file
diff --git a/build/drawer-menu/render.php b/build/drawer-menu/render.php
new file mode 100644
index 0000000..d0826a3
--- /dev/null
+++ b/build/drawer-menu/render.php
@@ -0,0 +1,39 @@
+<?php
+
+use JVBase\managers\CacheManager;
+use JVBase\ui\Navigation;
+
+$menu_id = $attributes['menuId'] ?? '';
+$collapsed = $attributes['collapsed'] ?? true;
+
+// You'd populate this from options, a filter, or however you store menu data
+$menu_items = apply_filters('jvbDrawerItems', [], $menu_id);
+
+if (empty($menu_items) || empty($menu_id)) {
+	return '<p>Please configure the drawer menu in block settings.</p>';
+}
+
+$cache = CacheManager::for('drawer');
+$cache->clear();
+
+if (!is_front_page()) {
+	$menu_items[] = [
+		'text'	=> 'Home',
+		'url'	=> home_url(),
+		'icon'	=> 'house-simple',
+	];
+}
+$items = array_map(function($item) { return $item['text'];}, $menu_items);
+
+$key = $cache->generateKey($items);
+$menu =  $cache->remember($key,
+function () use ($menu_items, $menu_id, $collapsed) {
+	$menu = new Navigation($menu_id);
+	$menu->asDrawer($collapsed)->populateFromArray($menu_items);
+	return $menu->render();
+});
+
+global $wp;
+
+$current = home_url($wp->request.'/');
+echo str_replace($current.'"', $current.'" class="current" aria-current="page"', $menu);
diff --git a/build/drawer-menu/style-index-rtl.css b/build/drawer-menu/style-index-rtl.css
new file mode 100644
index 0000000..2527c65
--- /dev/null
+++ b/build/drawer-menu/style-index-rtl.css
@@ -0,0 +1 @@
+nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;left:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:var(--base);border-right:1px solid var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(-180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid var(--contrast-200)}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}
diff --git a/build/drawer-menu/style-index.css b/build/drawer-menu/style-index.css
new file mode 100644
index 0000000..1591a7f
--- /dev/null
+++ b/build/drawer-menu/style-index.css
@@ -0,0 +1 @@
+nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;right:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:var(--base);border-left:1px solid var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid var(--contrast-200)}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}
diff --git a/build/feed/style-index-rtl.css b/build/feed/style-index-rtl.css
index 22875c2..ba5c5ec 100644
--- a/build/feed/style-index-rtl.css
+++ b/build/feed/style-index-rtl.css
@@ -1 +1 @@
-.feed-block{grid-column:full}.feed-block .feed-filters{margin:0 auto;max-width:var(--wide);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 [type=radio]{right:var(--offScreen);position:absolute}.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(--fw-b);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)}.item-grid{max-width:none;padding:0 var(--chip)}.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(--trans-base),filter var(--trans-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:rgba(var(--base-rgb),var(--op-3));border-radius:var(--radius);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)}}
+.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);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 [type=radio]{right:var(--offScreen);position:absolute}.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(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.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)}.item-grid{max-width:none;padding:0 var(--chip)}.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 details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));right:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{-o-object-fit:cover;object-fit:cover}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;left:50%;text-align:left}.feed.item[data-timeline] .images span:last-of-type{right:50%;top:0}.feed.item[data-timeline] .images>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 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 bbc8a27..7429b47 100644
--- a/build/feed/style-index.css
+++ b/build/feed/style-index.css
@@ -1 +1 @@
-.feed-block{grid-column:full}.feed-block .feed-filters{margin:0 auto;max-width:var(--wide);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 [type=radio]{left:var(--offScreen);position:absolute}.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(--fw-b);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)}.item-grid{max-width:none;padding:0 var(--chip)}.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(--trans-base),filter var(--trans-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:rgba(var(--base-rgb),var(--op-3));border-radius:var(--radius);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)}}
+.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);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 [type=radio]{left:var(--offScreen);position:absolute}.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(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.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)}.item-grid{max-width:none;padding:0 var(--chip)}.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 details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));left:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{-o-object-fit:cover;object-fit:cover}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;right:50%;text-align:right}.feed.item[data-timeline] .images span:last-of-type{left:50%;top:0}.feed.item[data-timeline] .images>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 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 5f0d1c5..c1f2e24 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '9b079150b16e2c23daab');
+<?php return array('dependencies' => array(), 'version' => '39f29851e15768a69790');
diff --git a/build/feed/view.js b/build/feed/view.js
index a550270..c0a3d9e 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{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-items",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}),this.addTaxonomyPreloadListeners(e,t)})),this.selector.isInitializing=!1,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}addTaxonomyPreloadListeners(e,t){const i=()=>{this.selector.preloadTaxonomy(t)};e.addEventListener("mouseenter",i,{once:!0}),e.addEventListener("pointerdown",i,{once:!0}),e.addEventListener("focus",i,{once:!0})}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(){const e=window.jvbStore.register("feed",{storeName:"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=e.feed,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),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=0===Object.keys(this.taxonomyFilters).length,this.ui.clearFilter.hidden=0===Object.keys(this.taxonomyFilters).length}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,d,h,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,d.textContent,h.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}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}addPlaceholders(){let e=this.contentTypes.length;const t=document.createDocumentFragment();for(let i=0;i<12;i++){let i,s=window.getTemplate("placeholderTemplate"),r=Math.floor(Math.random()*e);i=this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[r])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),s.append(i),t.append(s)}this.ui.grid.append(t)}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.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})):window.addEventListener("resize",(()=>{window.debouncer.schedule("feed-update-images",(()=>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",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(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(){var e;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-items",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=null!==(e=this.ui.filterContainer.querySelectorAll('[name="content"]'))&&void 0!==e&&e,this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content&&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}),this.addTaxonomyPreloadListeners(e,t)})),this.selector.isInitializing=!1,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}addTaxonomyPreloadListeners(e,t){const i=()=>{this.selector.preloadTaxonomy(t)};e.addEventListener("mouseenter",i,{once:!0}),e.addEventListener("pointerdown",i,{once:!0}),e.addEventListener("focus",i,{once:!0})}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(){this.ui.filterContainer&&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(){const e=window.jvbStore.register("feed",{storeName:"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=e.feed,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)}})),this.ui.filterContainer&&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),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.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&&(this.ui.filters.match.hidden=0===Object.keys(this.taxonomyFilters).length),this.ui.clearFilter&&(this.ui.clearFilter.hidden=0===Object.keys(this.taxonomyFilters).length)}createItemElement(e){let t=window.getTemplate(`feedItem${window.uppercaseFirst(e.content)}`);if(Object.hasOwn(t.dataset,"timeline"))return this.createTimelineElement(e,t);for(let[i,s]of Object.entries(e.fields)){let r=t.querySelector(`[data-field="${i}"]`);r&&(Object.keys(e.images).map((e=>parseInt(e))).includes(s)?[r.src,r.srcset,r.alt]=[e.images[s].tiny,`${e.images[s].tiny} 50w, ${e.images[s].small} 300w, ${e.images[s].medium} 1024w`,e.images[s]["image-alt-text"]]:"TIME"===r.tagName?(r.setAttribute("datetime",s),r.textContent=window.formatTimeAgo(s)):r.textContent=s,""===s&&r.remove())}let i=t.querySelector("a");var s;return i&&""!==e.url&&([i.href,i.title]=[e.url,`View ${null!==(s=e.fields.post_title)&&void 0!==s?s:"Item"}`]),t}createTimelineElement(e,t){var i,s;let[r,n,a,o,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,n.href,a.src,a.dataset.small,a.dataset.medium,o.src,o.dataset.small,o.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}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}addPlaceholders(){let e=this.contentTypes.length;const t=document.createDocumentFragment();for(let i=0;i<12;i++){let i,s=window.getTemplate("placeholderTemplate"),r=Math.floor(Math.random()*e);i=this.ui.content&&this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[r])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),s.append(i),t.append(s)}this.ui.grid.append(t)}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.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})):window.addEventListener("resize",(()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}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",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
diff --git a/build/forms/view.asset.php b/build/forms/view.asset.php
index 14684c2..5ac10d4 100644
--- a/build/forms/view.asset.php
+++ b/build/forms/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '918c373929631a4cc439');
+<?php return array('dependencies' => array(), 'version' => '2321e606c22ff3ca6cff');
diff --git a/build/forms/view.js b/build/forms/view.js
index ac99e02..6d7c3b2 100644
--- a/build/forms/view.js
+++ b/build/forms/view.js
@@ -1 +1 @@
-(()=>{class o{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0,autoUpload:!1})})),this.controller.subscribe(((o,r)=>{"form-submit"===o&&this.handleFormSubmission(r)}))}async handleFormSubmission(o){let r=o.formId,e=o.config,t=o.fullData,n=e.element;const s=new FormData;for(const[o,r]of Object.entries(t))"_wpnonce"!==o&&"_wp_http_referer"!==o&&(Array.isArray(r)?r.forEach((r=>s.append(`${o}[]`,r))):"object"==typeof r&&null!==r?s.append(o,JSON.stringify(r)):s.append(o,r));window.jvbUploads&&(await window.jvbUploads.getFilesForForm(n)).forEach((({file:o,fieldName:r,uploadId:e,meta:t})=>{s.append(`${r}[]`,o)}));let l={};n.closest(".jvb-form-block"),this.controller.showFormStatus(r,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",credentials:"same-origin",headers:l,body:s}),e=await o.json();if(!o.ok)return this.controller.showFormStatus(r,"error"),void this.controller.handleFormError(n,e);if(this.controller.showFormStatus(r,"submitted"),this.controller.showSummary(r,".jvb-form-block"),window.jvbUploads){const o=n.querySelectorAll("[data-upload-field]");for(const r of o){const o=window.jvbUploads.determineFieldId(r);await window.jvbUploads.clearFieldFromStores(o)}}}catch(o){console.error("Form submission error:",o),this.controller.showFormStatus(r,"error"),this.controller.handleFormError(n,{message:"Network error. Please check your connection and try again.",code:"network_error"})}finally{this.controller.store.delete(r)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((r=>{"auth-loaded"===r&&new o}))}))})();
\ No newline at end of file
+(()=>{class o{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0,autoUpload:!1})})),this.controller.subscribe(((o,r)=>{"form-submit"===o&&this.handleFormSubmission(r)}))}async handleFormSubmission(o){const{formId:r,config:e,fullData:t}=o,n=e.element,s=new FormData;for(const[o,r]of Object.entries(t))"_wpnonce"!==o&&"_wp_http_referer"!==o&&(Array.isArray(r)?r.forEach((r=>s.append(`${o}[]`,r))):"object"==typeof r&&null!==r?s.append(o,JSON.stringify(r)):s.append(o,r));if(window.jvbUploads)try{(await window.jvbUploads.getFilesForForm(n)).forEach((({file:o,fieldName:r})=>{s.append(`${r}[]`,o)}))}catch(o){console.error("Error getting files:",o)}this.controller.showFormStatus(r,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",credentials:"same-origin",body:s}),e=await o.json();if(!o.ok)return this.controller.showFormStatus(r,"error"),void this.controller.handleFormError(n,e);if(this.controller.showFormStatus(r,"submitted"),this.controller.showSummary(r,".jvb-form-block"),window.jvbUploads){const o=n.querySelectorAll("[data-upload-field]");for(const r of o){const o=window.jvbUploads.determineFieldId(r);await window.jvbUploads.clearFieldFromStores(o)}}}catch(o){console.error("Form submission error:",o),this.controller.showFormStatus(r,"error"),this.controller.handleFormError(n,{message:"Network error. Please check your connection and try again.",code:"network_error"})}finally{await this.controller.store.delete(r)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((r=>{"auth-loaded"===r&&new o}))}))})();
\ No newline at end of file
diff --git a/build/list/render.php b/build/list/render.php
index 2b56c65..f8ef568 100644
--- a/build/list/render.php
+++ b/build/list/render.php
@@ -169,7 +169,7 @@
     $out .= '</section>';
 
     echo $out;
-    echo jvbBuildDirectoryNavigation();
+    echo JVB()->directories()?->renderNavigation();
 
     return ob_get_clean();
 }
diff --git a/build/summary/render.php b/build/summary/render.php
index d6a2040..4c22c7a 100644
--- a/build/summary/render.php
+++ b/build/summary/render.php
@@ -44,7 +44,7 @@
     $artist = jvbContentFromUser((int)$current->post_author);
 
 	$sections = JVB_CONTENT[jvbNoBase($current->post_type)]['sections']??[];
-	jvbDump($sections);
+
 
 
 //    $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type));
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 52b25d1..f83b57d 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -19,7 +19,6 @@
         $this->cache = CacheManager::for('blocks', WEEK_IN_SECONDS);
 		add_filter('render_block', [$this, 'render'], 999, 3);
 
-
         add_action('init', [$this, 'registerBlockStyles']);
     }
 
@@ -126,7 +125,7 @@
      */
 
 
-    protected function render_core_button(array $block):string
+    public function render_core_button(array $block):string
     {
 		preg_match('/href="([^"]*)"/', $block['innerHTML'], $url);
 		preg_match('/>([^<]*)<\/a>/', $block['innerHTML'], $label);
@@ -160,13 +159,13 @@
 		);
     }
 
-    protected function render_core_buttons(array $block):string
+    public function render_core_buttons(array $block):string
     {
         return '<ul'.$this->getClassesAndStyles($block['attrs'], ['buttons','row']).'>'.
                $this->innerBlocks($block).'</ul>';
     }
 
-    protected function render_core_column(array $block):string
+    public function render_core_column(array $block):string
     {
         $styles = (array_key_exists('attrs', $block) &&
                    array_key_exists('width', $block['attrs'])) ?
@@ -177,7 +176,7 @@
                $this->innerBlocks($block).'</div>';
     }
 
-    protected function render_core_columns(array $block):string
+    public function render_core_columns(array $block):string
     {
         return '<section'.
                $this->getClassesAndStyles($block['attrs'], ['columns']).'>'.
@@ -185,7 +184,7 @@
     }
     //core_comment_template
 
-    protected function render_core_group(array $block):string
+    public function render_core_group(array $block):string
     {
         $tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
 
@@ -198,12 +197,12 @@
     //core_more
     //core_nextpage
 
-    protected function render_core_separator(array $block):string
+    public function render_core_separator(array $block):string
     {
         return '<hr'.$this->getClassesAndStyles($block['attrs']).'>';
     }
 
-    protected function render_core_spacer(array $block):string
+    public function render_core_spacer(array $block):string
     {
         return '<div'.$this->getClassesAndStyles($block['attrs'], ['spacer'], ['height:2rem']).
                ' aria-hidden="true"></div>';
@@ -219,7 +218,7 @@
      * Media Blocks
      */
     //core_audio
-    protected function render_core_cover(array $block):string
+    public function render_core_cover(array $block):string
     {
 
         // Extract block attributes
@@ -254,7 +253,7 @@
 
 		// Build classes and styles
 		unset($attrs['url']);
-		$classes = $this->getClassesAndStyles($attrs, ['cover']);
+		$classes = $this->getClassesAndStyles($attrs, ['cover row']);
 
 
 		return '<section' . $classes . '>' .
@@ -266,14 +265,14 @@
 
     //core_file
 
-    protected function render_core_gallery(array $block):string
+    public function render_core_gallery(array $block):string
     {
         return '<ul'.$this->getClassesAndStyles($block['attrs'], ['gallery']).'>'.
                $this->innerBlocks($block,'<li>', '</li>').
                '</ul>';
     }
 
-    protected function render_core_image(array $block):string
+    public function render_core_image(array $block):string
     {
         $ID = $this->imageID('', $block);
         if (!$ID) {
@@ -294,7 +293,7 @@
                $caption.'</figure>';
     }
 
-    protected function render_core_media_text(array $block):string
+    public function render_core_media_text(array $block):string
     {
 
         $ID = $this->imageID('', $block);
@@ -334,7 +333,7 @@
     //render_core_details
     //render_core_footnotes
     //render_core_classic
-    protected function render_core_heading(array $block):string
+    public function render_core_heading(array $block):string
     {
         $level = (array_key_exists('level', $block['attrs'])) ? $block['attrs']['level'] : '2';
 		$content = $this->inside($block);
@@ -344,25 +343,25 @@
                '</h'.$level.'>';
     }
 
-	protected function render_core_list(array $block):string
+	public function render_core_list(array $block):string
 	{
 		$tag = (array_key_exists('ordered', $block['attrs'])) ? 'ol' : 'ul';
 		return '<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.$this->innerBlocks($block).'</'.$tag.'>';
 	}
 
-//	protected function render_core_list_item(array $block):string
+//	public function render_core_list_item(array $block):string
 //	{
 //		return '<li'.$this->getClassesAndStyles($block['attrs']).'>'.$this->inside($block).'</li>';
 //	}
     //render_core_missing
 
-    protected function render_core_paragraph(array $block):string
+    public function render_core_paragraph(array $block):string
     {
         return '<p'.$this->getClassesAndStyles($block['attrs']).'>'.
                $this->inside($block, 'p').
                '</p>';
     }
-	protected function render_core_quote(array $block): string
+	public function render_core_quote(array $block): string
 	{
 		$innerHTML = $block['innerHTML'];
 
@@ -383,7 +382,7 @@
 			$citeHtml.
 			'</blockquote>';
 	}
-	protected function render_core_pullquote(array $block): string
+	public function render_core_pullquote(array $block): string
 	{
 		$innerHTML = $block['innerHTML'];
 
@@ -415,7 +414,7 @@
     //core_loginout
     //core_pattern
 
-    protected function render_core_site_logo(array $block, string $content):string
+    public function render_core_site_logo(array $block, string $content):string
     {
         $open = $close = '';
 
@@ -430,7 +429,7 @@
     }
     //core_site_title_tagline
 
-    protected function render_core_site_title(array $block, string $content):string
+    public function render_core_site_title(array $block, string $content):string
     {
         $tag = (array_key_exists('level', $block['attrs'])) ? $block['attrs']['level'] : 1;
         $tag = ($tag == 0) ? 'p' : 'h'.$tag;
@@ -472,7 +471,7 @@
     /**
      * Theme Navigation Blocks
      */
-    protected function render_core_navigation(array $block, string $content):string
+    public function render_core_navigation(array $block, string $content):string
     {
         $ID = (array_key_exists('ref', $block['attrs'])) ? $block['attrs']['ref'] : false;
 
@@ -513,7 +512,7 @@
 		   '</nav>'.$helpmenu;
     }
 
-    protected function render_core_navigation_link(array $block):string
+    public function render_core_navigation_link(array $block):string
     {
         global $wp;
         $url = (str_starts_with($block['attrs']['url'],'/')) ?
@@ -535,7 +534,7 @@
         return '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].'</a></li>';
     }
 
-    protected function render_core_navigation_submenu(array $block):string
+    public function render_core_navigation_submenu(array $block):string
     {
         global $wp;
         $url = (str_starts_with($block['attrs']['url'],'/')) ?
@@ -605,7 +604,7 @@
     //core_post_author
     //core_post_author_biography
     //core_post_author_name
-    protected function render_core_post_content(array $block, string $content = ''):string
+    public function render_core_post_content(array $block, string $content = ''):string
     {
 
         $tag = (array_key_exists('tagName', $block['attrs'])) ?
@@ -614,6 +613,7 @@
 
         if ($content == '') {
 			global $post;
+
 			$block['innerBlocks'] = parse_blocks($post->post_content);
 			return $this->innerBlocks($block);
         } else {
@@ -621,13 +621,13 @@
         }
     }
     //core_post_date
-	protected function render_core_post_date(array $block):string
+	public function render_core_post_date(array $block):string
 	{
 		$postDate = get_the_date('c');
 		return '<time datetime="'.$postDate.'" itemprop="datePublished"'.$this->getClassesAndStyles($block['attrs']).'>'.get_the_date().'</time>';
 	}
     //core_post_excerpt
-    protected function render_core_post_featured_image(array $block):string
+    public function render_core_post_featured_image(array $block):string
     {
 		global $post;
 		$ID = get_post_thumbnail_id($post->ID);
@@ -644,7 +644,7 @@
     //core_post_navigation_link
     //core_post_template
     //core_post_terms
-	protected function render_core_post_terms(array $block):string
+	public function render_core_post_terms(array $block):string
 	{
 		$terms = get_the_terms(get_the_ID(), $block['attrs']['term']);
 		$out = '';
@@ -664,7 +664,7 @@
 		return $out;
 	}
     //core_post_time_to_read
-    protected function render_core_post_title(array $block):string
+    public function render_core_post_title(array $block):string
     {
         $open = $close = '';
         if (array_key_exists('isLink', $block['attrs'])) {
@@ -689,9 +689,8 @@
                '</h'.$level.'>';
     }
 
-	protected function render_core_query(array $block, string $content):string
+	public function render_core_query(array $block, string $content):string
 	{
-
 		$queryID = $block['attrs']['queryId'];
 		$args = [];
 		$inherit = $block['attrs']['inherit']??false;
@@ -798,7 +797,7 @@
     //core_query_pagination_previous
     //core_query_title
     //core_read_more
-    protected function render_core_template_part(array $block, string $content):string
+    public function render_core_template_part(array $block, string $content):string
     {
 
 		$isHeaderTemplate = (
@@ -877,7 +876,7 @@
     //core_rss
     //core_search
     //core_shortcode
-	protected function render_core_social_link(array $block, string $content):string
+	public function render_core_social_link(array $block, string $content):string
 	{
 		$url = $block['attrs']['url'];
 		$service = $block['attrs']['service'];
@@ -888,7 +887,7 @@
 		}
 		return '<li><a href="'.$url.'" target="_blank" rel="nofollow" title="Find us on '.ucfirst($service).'">'.$icon.'<span class="screen-reader-text">Find us on '.ucfirst($service).'</span></a></li>';
 	}
-	protected function render_core_social_links(array $block, string $content):string
+	public function render_core_social_links(array $block, string $content):string
 	{
 		return '<ul class="socials">'.$this->innerBlocks($block).'</ul>';
 	}
@@ -1514,7 +1513,6 @@
 
             // Background URL (for cover, media blocks)
             case 'url':
-				jvbDump($value);
                 if (!empty($value) && str_starts_with($value, 'http')) {
                     $styles[] = 'background-image: url('.$value.')';
                 }
@@ -1750,5 +1748,3 @@
     }
 
 }
-
-new CustomBlocks();
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 7a5158b..8c630dd 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -21,7 +21,7 @@
 	{
 		// Initialize cache with connections
 		$this->cache = CacheManager::for('feed_block', WEEK_IN_SECONDS);
-
+		$this->cache->clear();
 		// Set up cache connections for all feed content types
 		$this->setupCacheConnections();
 
@@ -76,7 +76,7 @@
 				$config = array_merge($config, $mainConfig['feed']['config']);
 			} else {
 				$config['content'] = $content;
-				$config['icon'] = JVB_CONTENT[$content]['icon']??['logo-triangle'];
+				$config['icon'] = JVB_CONTENT[$content]['icon']??[jvbLogoIcon()];
 			}
 			if (is_singular()) {
 				$config['source'] = $type->ID;
@@ -126,6 +126,7 @@
 
 	public function render(array $attributes, string $content, WP_Block $block)
 	{
+
 		$this->config = $this->buildParams($attributes);
 		return $this->cache->remember(
 			$this->config,
@@ -134,16 +135,48 @@
 			}
 		);
 	}
+	protected function getContext():string|bool
+	{
+		return array_key_exists('context', $this->config)?$this->config['context']:false;
+	}
+	protected function getIDS():array|bool
+	{
+		return (array_key_exists('ids', $this->config) && !empty($this->config['ids'])) ? $this->config['ids'] : false;
+	}
+	protected function getClasses():array|bool
+	{
+		return array_key_exists('classes', $this->config) && !empty($this->config['classes']) ? $this->config['classes'] : false;
+	}
+	protected function getSource():string|bool
+	{
+		return array_key_exists('source', $this->config) ? $this->config['source'] : false;
+	}
+
+	protected function getIcon():string|bool
+	{
+		return array_key_exists('icon', $this->config) ? $this->config['icon'] : false;
+	}
+	protected function isGallery():bool
+	{
+		return (array_key_exists('is_gallery', $this->config) && $this->config['is_gallery']);
+	}
+	protected function getContent():array|bool
+	{
+		return (array_key_exists('content', $this->config) && !empty ($this->config['content'])) ? $this->config['content'] : false;
+	}
 
 	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']).'"' : '';
+		if (is_post_type_archive(BASE.'directory')) {
+			return '';
+		}
+		$ids = ($this->getIDS()) ? ' id="'.implode(' ',$this->getIDS()).'"' : '';
+		$classes = ($this->getClasses()) ? ' class="'.implode(' ',$this->getClasses()).'"' : '';
+		$source = ($this->getSource()) ? ' data-source="'.$this->getSource().'"' : '';
+		$context = ($this->getContext()) ? ' data-context="'.$this->getContext().'"' : '';
+		$icons = ($this->getIcon()) ? ' data-icon="'.$this->getIcon().'"' : ' data-icon="'.jvbLogoIcon().'"';
+		$gallery = $this->isGallery() ? ' data-gallery' : '';
+		$content = ($this->getContent()) ? ' data-content="'.implode(',',$this->getContent()).'"' : '';
 		ob_start();
 		?>
 		<section<?= $ids.$classes ?> class="feed-block"<?= $content.$source.$context.$gallery.$icons ?>>
@@ -166,9 +199,9 @@
 		}
 
 		$feedContent = $this->getFeedContent();
-		$hasMany = count($this->config['content']) > 1;
+		$hasMany = count($this->getContent()) > 1;
 		?>
-		<form class="feed-filters" data-save="feed-<?=$this->config['context']?>">
+		<form class="filters" data-save="feed-<?=$this->getContext()?>">
 			<?php if ($hasMany) {
 				//If we have multiple content, only show the content first
 				?>
@@ -177,7 +210,7 @@
 					<span class="label">SHOWING: </span>
 					<?php
 					$labels = [];
-					foreach ($this->config['content'] as $i => $type) :
+					foreach ($this->getContent() as $i => $type) :
 
 						$checked = $i === 0 ? ' checked' : '';
 						$label = $feedContent[$type]['plural'] ?? ucfirst($type);
@@ -224,8 +257,9 @@
 					<?php endif; ?>
 				<?php if ($hasMany) { ?>
 				</summary>
-				<?php } ?>
-
+				<?php }
+				if (!empty ($this->config['taxonomies'])) {
+				?>
 					<div class="filters">
 						<div class="filter-group row start">
 							<span class="label">FILTER BY:</span>
@@ -243,7 +277,7 @@
 									'feed-'.$tax,
 									$tax,
 									[
-										'icon'			=> $taxConfig['icon']??'logo-triangle',
+										'icon'			=> $taxConfig['icon']??jvbLogoIcon(),
 										'update' 		=> '.selected-items-section .selected-items',
 										'types' 		=> $contentForTax,
 										'autocomplete'	=> false,
@@ -266,7 +300,7 @@
 							</div>
 						</div>
 					</div>
-
+				<?php } ?>
 					<div class="row btw nowrap">
 						<div class="order-by filter-group row start w-full">
 							<span class="label">ORDER BY:</span>
@@ -319,11 +353,11 @@
 		?>
 		<div class="item-grid">
 			<?php
-			$total = count($this->config['content']) - 1;
+			$total = count($this->getContent()) - 1;
 			for ($i = 1; $i <= 36; $i++) {
 				$rand = rand(0, $total);
-				$config = Features::getConfig($this->config['content'][$rand]);
-				$icon = jvbIcon($config['icon']??'logo-triangle');
+				$config = Features::getConfig($this->getContent()[$rand]);
+				$icon = jvbIcon($config['icon']??jvbLogoIcon());
 				?>
 				<div class="placeholder"><?=apply_filters('jvbFeedPlaceholder', $icon) ?></div>
 				<?php
@@ -351,47 +385,99 @@
 
 	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']).'
-                </button>
-                <div class="feed-images">
-                    <a>
-                        <img width="300px" height="300px" loading="lazy" decoding="async">
-                    </a>
-                </div>
-            </summary>
+		if ($this->getContent()) {
+			foreach ($this->getContent() as $content) {
+				echo $this->getDefaultTemplate($content);
+			}
+		}
 
-            <div class="item-info">
-                <h3><a></a></h3>
-                <div class="item">
-                    <span class="label"></span>
-                    <a></a>
-                    <p></p>
-                </div>
-                <div class="item-list">
-                    <span class="label"></span>
-                        <ul>
-                            <li>
-                                <a></a>
-                            </li>
-                        </ul>
-                    </div>
-                </div>
-            </div>
-        </details>', $this->config).'</template>';
 
-		echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="feed-empty-state">
-                <h3>NOTHING HERE...</h3>
+		echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="empty-state">
+                <h3>'.jvbIcon($this->getIcon()).'NOTHING HERE'.jvbIcon($this->getIcon()).'</h3>
                 <p>Try tweaking those filters a bit.</p>
-                <p>Edmonton\'s got talent - let\'s find it.</p>
             </div>', $this->config). '</template>';
-		echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon('logo-triangle')).'</div></template>';
+		echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon(jvbLogoIcon())).'</div></template>';
 	}
 
+	protected function getFavouritesButton(string $content):string
+	{
+		if (!Features::forSite()->has('favourites') && !Features::forContent($content)->has('favouritable')) {
+			return '';
+		}
+		return '<button class="favourite" type="button" title="Add to favourites" data-action="favourite">
+			'.jvbIcon('heart')
+			.jvbIcon('heart', ['style'=>'fill']).'
+		</button>';
+	}
+	protected function getUpvotesButton(string $content):string
+	{
+		if (!Features::forSite()->has('karma') && !Features::forContent($content)->has('karma')){
+			return '';
+		}
+		return '<div class="karma row">
+			<button type="button" class="vote" data-action="upvote">
+				'.jvbIcon('arrow-fat-up')
+				.jvbIcon('arrow-fat-up', ['style'=>'fill']).
+			'</button>
+			<button type="button" class="vote" data-action="downvote">
+				'.jvbIcon('arrow-fat-down')
+				.jvbIcon('arrow-fat-down', ['style'=>'fill']).
+			'</button>
+			<span class="score"></span>
+		</div>';
+	}
+	protected function getDefaultTemplate(string $content): string
+	{
+		$config = JVB_CONTENT[$content]??[];
+		$hasConfig = array_key_exists('feed', $config);
+		$images = ($hasConfig && array_key_exists('images', $config['feed']) ? $config['feed']['images']:['post_thumbnail']);
+		$images = (is_array($images)) ? $images : [$images];
+		$fields = ($hasConfig && array_key_exists('fields', $config['feed']) ? $config['feed']['fields']:['post_title', 'post_date']);
+		$fields = array_filter($fields, function($field) use($images) {
+			return !in_array($field, $images);
+		});
+		$template = '<div class="feed item col '.$content.'">'.$this->getFavouritesButton($content).$this->getUpvotesButton($content);
+
+		//Add all defined images, but allow for filtering
+		$imageTemplate = '<a>';
+		foreach ($images as $image) {
+			$imageTemplate .= '<img data-field="'.$image.'" width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">';
+		}
+		$imageTemplate .= '</a>';
+		$template .= '<div class="images">'.apply_filters('jvbFeedImages', $imageTemplate, $content, $images).'</div>';
+
+		//Output default fields, but allow for filtering
+		$template .= '<details>
+		<summary>'.apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content).'</summary>';
+
+		$fieldsTemplate = '';
+
+		foreach ($fields as $fieldName) {
+			$fieldType = JVB_CONTENT[$content][$fieldName]['type']??'text';
+			$fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($fieldType, $fieldName), $content, $fieldName, $fieldType);
+		}
+		$template .= '<div class="item-info">'.apply_filters('jvbFeedItemFields', $fieldsTemplate, $content, $fields).'</div>';
+		$template .= '</details></div>';
+
+		return '<template class="feedItem'.ucfirst($content).'">'.apply_filters('jvbFeedItem', $template, $content).'</template>';
+	}
+
+	protected function defaultFieldTemplate(string $fieldType, string $fieldName):string
+	{
+		$data = ' data-field="'.$fieldName.'"';
+		switch ($fieldName) {
+			case 'post_title':
+				return '<h3'.$data.'></h3>';
+			case 'post_date':
+			case 'post_modified':
+				return '<time'.$data.'></time>';
+		}
+		return match($fieldType) {
+			'upload'	=> '<img'.$data.' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
+			'taxonomy'	=> '<ul'.$data.'><li><a><i></i></a></li></ul>',
+			default	=>  '<p'.$data.'></p>',
+		};
+	}
 	/**
 	 * Get feed content using Features instead of get_option
 	 * Returns array of slug => config for types that show in feed
diff --git a/inc/blocks/SummaryBlock.php b/inc/blocks/SummaryBlock.php
index 527e523..6fc6da2 100644
--- a/inc/blocks/SummaryBlock.php
+++ b/inc/blocks/SummaryBlock.php
@@ -54,7 +54,9 @@
     {
         $this->config = $this->getConfig();
         $key = $this->generateKey();
+		$this->cache->clear();
         $cache = $this->cache->get($key);
+
         if ($cache) {
             return $cache;
         }
@@ -177,7 +179,7 @@
         if (empty($this->type)) {
             $this->type = match (true) {
                 is_tax()                => jvbNoBase(get_queried_object()->taxonomy),
-                is_post_type_archive()  => jvbNoBase(get_post_type()),
+                is_post_type_archive()  => jvbNoBase(get_queried_object()->name),
                 default                 => jvbNoBase(get_queried_object()->post_type),
             };
         }
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
index 2f0bd4a..c16416b 100644
--- a/inc/blocks/TimelineBlock.php
+++ b/inc/blocks/TimelineBlock.php
@@ -96,7 +96,7 @@
 						$many = count($terms) > 1;
 					?>
 					<li class="<?=$slug?>">
-						<?=jvbIcon($config['icon']??'triangle')?>
+						<?=jvbIcon($config['icon']??jvbDefaultIcon())?>
 
 						<?php
 						if ($many) { echo '<ul>'; }
@@ -152,7 +152,7 @@
 				?>
 				<div class="info">
 					<header>
-						<h2><?=jvbIcon('logo-triangle')?><?= $title?></h2>
+						<h2><?=jvbIcon(jvbLogoIcon())?><?= $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>' : '' ?>
diff --git a/inc/blocks/_setup.php b/inc/blocks/_setup.php
index 07d0e9c..c6955cf 100644
--- a/inc/blocks/_setup.php
+++ b/inc/blocks/_setup.php
@@ -78,5 +78,8 @@
 			]
 		);
 	}
+	register_block_type(
+		JVB_DIR . '/build/drawer-menu',
+	);
 }
 add_action('init', 'jvbRegisterBlocks');
diff --git a/inc/forms/TaxonomySelector.php b/inc/forms/TaxonomySelector.php
index e79834d..ab3d7d5 100644
--- a/inc/forms/TaxonomySelector.php
+++ b/inc/forms/TaxonomySelector.php
@@ -32,6 +32,7 @@
 		$this->id = sanitize_key($id);
 		$this->taxonomy = jvbCheckBase($taxonomy);
 		$this->name = jvbNoBase($taxonomy);
+
 		$this->title = JVB_TAXONOMY[$this->name]['plural'];
 		$this->base = $config['base']??'';
 
diff --git a/inc/helpers/breadcrumbs.php b/inc/helpers/breadcrumbs.php
index 020a700..6c6e1f9 100644
--- a/inc/helpers/breadcrumbs.php
+++ b/inc/helpers/breadcrumbs.php
@@ -112,12 +112,11 @@
 
 	if (is_singular(BASE.'directory')) {
 		$type = get_post_meta(get_the_ID(), BASE.'for_type_slug', true);
-		return jvbDirectories()[$type] ?? [];
+		return JVB()->directories()?->getDirectoryList()[$type] ?? [];
 	}
 
 	$obj = get_queried_object();
-	$directories = jvbDirectories();
-
+	$directories = JVB()->directories()?->directories();
 	if (is_tax()) {
 		$tax = jvbNoBase($obj->taxonomy);
 		return array_key_exists($tax, $directories) ? $directories[$tax] : [];
diff --git a/inc/helpers/directory.php b/inc/helpers/directory.php
index c67f442..178c72a 100644
--- a/inc/helpers/directory.php
+++ b/inc/helpers/directory.php
@@ -6,57 +6,6 @@
 
 function jvbIsDirectory():bool
 {
-    return (is_post_type_archive(BASE.'directory') || is_singular(BASE.'directory'));
-}
 
-function jvbDirectoryIds():array
-{
-    return array_values(get_option(BASE.'directory_ids'));
-}
-
-function jvbBuildDirectoryNavigation():string
-{
-    $nav = get_option(BASE.'directory_nav');
-    if ($nav === false) {
-        $IDs = jvbGlobalDirectoryInfo();
-        $nav = '<nav class="directory-list alignwide" id="directory-list"><ul><li class="title">More Lists:</li>';
-        foreach ($IDs as $ID) {
-            $bit = (array_key_exists('slug', $ID)) ? $ID['slug'] : $ID['title'];
-            $nav .= '<li id="directory-'.$bit.'"><a href="'.$ID['url'].'">[ '.$ID['title'].' ]</a></li>';
-        }
-        $nav .= '</ul></nav>';
-        update_option(BASE.'directory_nav', $nav);
-    }
-    $ID = get_the_ID();
-    return str_replace('directory-'.$ID.'"', 'directory-'.$ID.'" class="current"', $nav);
-}
-
-
-function jvbDirectories($search = 'all'):array
-{
-    $get = jvbGlobalDirectoryInfo();
-//    jvbDump($get);
-
-    if ($search == 'all') {
-        return $get;
-    } else {
-        return $get[$search]??[];
-    }
-}
-
-function jvbIndexedDirectories():array
-{
-
-    $out = get_transient(BASE.'indexed_directories');
-    if (!$out) {
-        $get = jvbGlobalDirectoryInfo();
-        $out = [];
-        foreach ($get as $g) {
-            $temp = $g;
-            unset($temp['ID']);
-            $out[$g['ID']] = $temp;
-        }
-        set_transient(BASE.'indexed_directories', $out, WEEK_IN_SECONDS);
-    }
-    return $out;
+	return JVB()->directories() && JVB()->directories()->isDirectory();
 }
diff --git a/inc/helpers/media.php b/inc/helpers/media.php
index 6a6f6fe..80c33c7 100644
--- a/inc/helpers/media.php
+++ b/inc/helpers/media.php
@@ -36,3 +36,4 @@
     </dialog>
     <?php
 }
+
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 859d67e..3f83bff 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -316,17 +316,34 @@
     $out = ($label === '') ? '' : '<h2 class="inline">'.$label.'</h2>';
     $out .= '<ul class="term-list '.jvbNoBase($terms[array_key_first($terms)]->taxonomy).'">';
     foreach ($terms as $term) {
-        $out .= '<li>
-            <a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="'.$term->name.'">'.
-                $term->name.
-            '</a>
-        </li>';
+        $out .= '<li>'.jvbGetTermLink($term).'</li>';
     }
     $out .= '</ul>';
 
     return $out;
 }
 
+function jvbGetTermLink(int|WP_Term $term, string $taxonomy = ''):string
+{
+	if (is_int($term)){
+		$term = get_term($term, jvbCheckBase($taxonomy));
+		if (is_wp_error($term)) {
+			return '';
+		}
+	}
+	$cache = CacheManager::for($term->taxonomy);
+	$key = $term->term_id.'-link';
+	return $cache->remember(
+		$key,
+		function() use ($term) {
+			return '<a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="'.$term->name.'">'.
+			$term->name.
+			'</a>';
+		}
+	);
+}
+
+
 add_action('wp_footer', 'jvbOutputImageTemplates');
 
 function jvbOutputImageTemplates() {
@@ -367,15 +384,15 @@
 				<div class="group-header">
 					<div class="selected">
 						<div class="field">
-							<input type="checkbox" id="select-all-group" name="select-all-group">
-							<label for="select-all-group">
+							<input type="checkbox" id="select-all" name="select-all" data-selects="item-grid" data-select-all>
+							<label for="select-all">
 								Select All In Group
 							</label>
 						</div>
 						<div class="info" hidden>
 						</div>
 					</div>
-					<div class="group-actions">
+					<div class="selection-actions">
 						<button type="button" data-action="add-to-group" title="Add selected uploads to this group">
 							<?= jvbIcon('plus-square') ?>
 							Add Here
@@ -393,8 +410,8 @@
 					<div class="fields"></div>
 				</details>
 				<div class="group-content col">
+					<p class="hint count"></p>
 					<div class="item-grid group"></div>
-					<p class="hint group-count"></p>
 				</div>
 			</div>
 
@@ -460,10 +477,10 @@
 				<div class="wrap">
 					<div class="restore-message">
 						<h4>Looks like we left things hanging</h4>
-						<p class="restore-details"></p>
+						<p class="details"></p>
 						<p class="hint">If you'd rather start over, you can clear this information.</p>
 					</div>
-					<div class="restore-actions">
+					<div class="selection-actions">
 						<div class="selected">
 							<div class="field">
 								<input type="checkbox" id="select-all-restore" name="select-all-restore">
@@ -493,10 +510,10 @@
 			</dialog>
 		</template>
 		<template class="restoreField">
-			<div class="restore-field">
-				<h3></h3>
+			<details class="restore-field">
+				<summary><h3><a></a></h3></summary>
 				<div class="item-grid restore"></div>
-			</div>
+			</details>
 		</template>
 
 		<template class="startOverConfirmation">
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index cbfaf3b..bbd289a 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -254,6 +254,13 @@
     return $image->formatImage($imgID, $start, $end, $addLink, $postSlug);
 }
 
+function jvbImageCaption(int $imgID, string $start = 'tiny', string $end = 'large', bool $addLink = true, ?string $postSlug = null):string
+{
+	$caption = wp_get_attachment_caption($imgID);
+	$caption = ($caption && $caption !== '') ? '<figcaption>'.apply_filters('the_content', $caption).'</figcaption>' : '';
+	return '<figure>'.jvbFormatImage($imgID, $start, $end, $addLink, $postSlug).$caption.'</figure>';
+}
+
 /**
  * Outputs the notification container in the footer
  * @return void
@@ -425,8 +432,11 @@
 		<div class="bar">
 			<div class="fill"></div>
 		</div>
-		<div class="details row btw">
-			<?=$inside?>
+		<div class="row btw">
+			<i class="icon"></i>
+			<div class="details">
+				<?=$inside?>
+			</div>
 		</div>
 	</div>
 	<?php
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index 498c2b8..b0b037d 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -157,6 +157,10 @@
 	 */
 	protected function registerAdditionalHooks(): void
 	{
+		$this->ensureInitialized();
+		if (!$this->isSetUp()) {
+			return;
+		}
 		// User login tracking for security
 		add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
 
@@ -527,6 +531,10 @@
 	 */
 	public function enqueueScripts(): void
 	{
+		$this->ensureInitialized();
+		if (!$this->isSetUp()) {
+			return;
+		}
 		// Helcim JS SDK
 		$sdk_url = $this->is_test_mode
 			? 'https://helcim-js-sandbox.helcim.com/v1/helcim.js'
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 12b73ec..b1958bc 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -761,6 +761,12 @@
 		array  $options = []
 	): array|WP_Error
 	{
+		if (!$this->is_healthy) {
+			$this->logDebug('Skipping request - integration is unhealthy', [
+				'consecutive_errors' => $this->error_stats['consecutive_errors'],
+				'last_success' => $this->error_stats['last_success']
+			]);
+		}
 		$this->ensureInitialized();
 		if (!$this->isSetUp()){
 			$this->logError('Connection not setup for '.$this->service_name, [
@@ -1920,7 +1926,6 @@
 			return false;
 		}
 
-		// Build refresh request data
 		$request_data = [
 			'client_id' => $this->credentials['client_id'],
 			'client_secret' => $this->credentials['client_secret'],
@@ -1928,12 +1933,24 @@
 			'grant_type' => 'refresh_token'
 		];
 
-		// Use centralized OAuth request method
 		$response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data);
 
 		if (is_wp_error($response)) {
+			$error_message = $response->get_error_message();
+
+			if (str_contains($error_message, 'invalid_grant')) {
+				$this->logError('OAuth refresh token is invalid - user must re-authorize', [
+					'error' => $error_message
+				], 'critical');
+
+				// Mark unhealthy immediately
+				$this->error_stats['consecutive_errors'] = $this->error_threshold;
+				$this->is_healthy = false;
+				$this->saveErrorStats();
+			}
+
 			$this->logError('Failed to refresh OAuth token for '.$this->service_name, [
-				'error' => $response->get_error_message()
+				'error' => $error_message
 			]);
 			return false;
 		}
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index a8b7a43..6f81f6f 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -5,28 +5,36 @@
 	exit;
 }
 
+use JVBase\utility\Features;
 use WP_Block;
 use WP_Query;
 
 class DirectoryManager
 {
     protected array $directories;
+    protected array $directoryPageIDs;
+	protected array $directoryList;
     protected static string $type = BASE.'for_type';
     protected static string $slug = BASE.'for_type_slug';
     protected CacheManager $cache;
 
     public function __construct()
     {
-        $this->directories = jvbGlobalDirectoryInfo();
-        if (empty(jvbGlobalDirectories())) {
+        $this->directories = $this->getDirectories();
+        if (empty($this->directories)) {
             return;
         }
         $this->cache = CacheManager::for('directory', WEEK_IN_SECONDS);
+		foreach(['content','taxonomy','user'] as $key) {
+			if (array_key_exists($key, $this->directories)) {
+				$this->cache->connectTo($key);
+			}
+		}
 
         add_action('init', [$this, 'registerDirectories']);
         jvb_register_do_once('directories_registered', [$this, 'activate']);
 
-        add_action('render_block', [$this, 'renderBlock'], 99, 3);
+        add_action('render_block', [$this, 'renderBlock'], 99999, 3);
     }
 
     public function registerDirectories()
@@ -62,81 +70,121 @@
         ));
     }
 
+	public function getDirectories():array
+	{
+		$directories = get_option(BASE.'directories');
+		if (!$directories) {
+			$directories = [];
+			//content
+			if(Features::anyContentHas('show_directory')) {
+				foreach (JVB_CONTENT as $key => $config) {
+					if (Features::forContent($key)->has('show_directory')) {
+						$directories[$key] = 'content';
+					}
+				}
+			}
+			if(Features::anyTaxonomyHas('show_directory')) {
+				foreach (JVB_TAXONOMY as $key=>$config) {
+					if (Features::forTaxonomy($key)->has('show_directory')) {
+						$directories[$key] = 'taxonomy';
+					}
+				}
+			}
+			if (Features::anyUserHas('show_directory')) {
+				foreach(JVB_USER as $key=>$config) {
+					if (Features::forUser($key)->has('show_directory')) {
+						$directories[$key] = 'user';
+					}
+				}
+			}
+
+			update_option(BASE.'directories', $directories);
+		}
+		return $directories;
+	}
+	protected function getConfigFromType(string $type):array
+	{
+		if (!array_key_exists($type, $this->directories)) {
+			return [];
+		}
+		return match ($this->directories[$type]) {
+			'content' => JVB_CONTENT[$type],
+			'taxonomy' => JVB_TAXONOMY[$type],
+			'user' => JVB_USER[$type],
+			default => [],
+		};
+
+	}
+
     public function activate()
     {
         $created = [];
         $directories = [];
 
-        foreach (jvbGlobalDirectories() as $directory => $type) {
-            switch ($type) {
-                case 'content':
-                    $config = JVB_CONTENT;
-                    break;
-                case 'tax':
-                    $config = JVB_TAXONOMY;
-                    break;
-                case 'user':
-                    $config = JVB_USER;
-                    break;
-            }
-            $title = $config[$directory]['directory']??$config[$directory]['plural'];
-            $excerpt = implode(' ', $config[$directory]['description']??[]);
-            $ID = wp_insert_post([
-                'post_type' => BASE.'directory',
-                'post_title'=> $title,
-                'post_status'=> 'publish',
-                'post_excerpt' => $excerpt,
-                'slug'      => sanitize_title($title)
-            ]);
-            if (!is_wp_error($ID)) {
-                add_post_meta($ID, self::$type, $type);
-                add_post_meta($ID, self::$slug, $directory);
-                $created[$directory] = (int)$ID;
-                $slug = sanitize_title($title);
-                $directories[$directory] = [
-                    'slug'  => $slug,
-                    'title' => $title,
-                    'ID'    => $ID,
-                    'url'   => get_home_url(null, '/directory/'.$slug),
-                    'page'  => $title,
-                    'description'    =>$config[$directory]['description']??[],
-                    'type'  => $type,
-                    'extra'    => $config[$directory]['directory_extra'] ??[],
-                ];
-            }
+		foreach($this->directories as $directory => $type) {
+			$config = $this->getConfigFromType($directory);
+			$title = $config['directory']??$config['plural'];
+			$excerpt = implode(' ', $config['description']??[]);
+			$ID = wp_insert_post([
+				'post_type'	=> BASE.'directory',
+				'post_title'	=> $title,
+				'post_status'	=> 'publish',
+				'post_excerpt'	=> $excerpt,
+				'slug'			=> sanitize_title($title)
+			]);
+			if (!is_wp_error($ID)) {
+				add_post_meta($ID, self::$type, $type);
+				add_post_meta($ID, self::$slug, $directory);
+				$created[$directory] = (int)$ID;
+				$slug = sanitize_title($title);
+				$directories[$directory] = [
+					'slug'  => $slug,
+					'title' => $title,
+					'ID'    => $ID,
+					'url'   => get_home_url(null, '/directory/'.$slug),
+					'page'  => $title,
+					'description'    =>$config[$directory]['description']??[],
+					'type'  => $type,
+					'extra'    => $config[$directory]['directory_extra'] ??[],
+				];
+			}
+			$isGrouped = match ($type) {
+				'content' => Features::forContent($directory)->has('isGrouped'),
+				'taxonomy' => Features::forTaxonomy($directory)->has('isGrouped'),
+				'user' => Features::forUser($directory)->has('isGrouped'),
+				default => false,
+			};
+			if ($isGrouped) {
+				$title = $title.', but Grouped';
+				$slug = sanitize_title($title).'-grouped';
+				$excerpt = $config['groupedDescription']??'Too many options? This is grouped by type.';
+				$ID = wp_insert_post([
+					'post_type'	=> BASE.'directory',
+					'post_title'	=> $title,
+					'post_status'	=> 'publish',
+					'post_excerpt'	=> $excerpt,
+					'slug'			=> $slug,
+				]);
+				if (!is_wp_error($ID)) {
+					add_post_meta($ID, self::$type, $type);
+					add_post_meta($ID, self::$slug, $directory.'-grouped');
+					add_post_meta($ID, BASE.'grouped_directory', 'yup');
+					$created[$directory.'-grouped'] = (int)$ID;
+					$directories[$directory.'-grouped'] = [
+						'slug'  => $slug,
+						'title' => $title,
+						'ID'    => $ID,
+						'url'   => get_home_url(null, '/directory/'.$slug),
+						'page'  => $title,
+						'description'    =>$config[$directory]['description']??[],
+						'type'  => $type,
+						'extra'    => $config[$directory]['directory_extra'] ??[],
+					];
+				}
+			}
+		}
 
-
-            if (jvbCheck('isGrouped', $config[$directory])) {
-                $title = $title. ', but Grouped';
-                $excerpt = 'Too many options in the list? This is grouped by type, nested deep, man.';
-				$slug = sanitize_title(str_replace(', but', '', $title));
-                $ID = wp_insert_post([
-                    'post_type' => BASE.'directory',
-                    'post_title'=> $title,
-                    'post_status'=> 'publish',
-                    'post_excerpt' => $excerpt,
-                    'slug'      => $slug
-                ]);
-                if (!is_wp_error($ID)) {
-                    add_post_meta($ID, self::$type, $type);
-                    add_post_meta($ID, self::$slug, $directory.'-grouped');
-                    add_post_meta($ID, BASE.'grouped_directory', 'yup');
-                    $created[$directory.'-grouped'] = (int)$ID;
-                    $directories[$directory.'-grouped'] = [
-                        'slug'  => $slug,
-                        'title' => $title,
-                        'ID'    => $ID,
-                        'url'   => get_home_url(null, '/directory/'.$slug),
-                        'page'  => $title,
-                        'description'    =>$config[$directory]['description']??[],
-                        'type'  => $type,
-                        'extra'    => $config[$directory]['directory_extra'] ??[],
-                    ];
-                }
-            }
-
-        }
-        if (jvbCheck('has_map', JVB_SITE)) {
+        if (Features::forSite()->has('has_map')) {
             $ID = wp_insert_post([
                 'post_type'     => BASE.'directory',
                 'post_title'    => 'Map',
@@ -165,20 +213,32 @@
         }
     }
 
+	public function getDirectoryPageIDs():array
+	{
+		if (empty($this->directoryPageIDs)) {
+			$this->directoryPageIDs = get_option(BASE.'directory_ids', []);
+		}
+		return $this->directoryPageIDs;
+	}
+	public function getDirectoryList():array
+	{
+		if (empty($this->directoryList)) {
+			$this->directoryList = get_option(BASE.'directory_list', []);
+		}
+		return $this->directoryList;
+	}
+
     public static function getConfig(int $ID):array
     {
         $type = get_post_meta($ID, self::$type, true);
         $slug = get_post_meta($ID, self::$slug, true);
-        switch ($type) {
-            case 'content':
-                return JVB_CONTENT[$slug];
-            case 'taxonomy':
-                return JVB_TAXONOMY[$slug];
-            case 'user':
-                return JVB_USER[$slug];
-        }
-        return [];
-    }
+		return match ($type) {
+			'content' => JVB_CONTENT[$slug],
+			'taxonomy' => JVB_TAXONOMY[$slug],
+			'user' => JVB_USER[$slug],
+			default => [],
+		};
+	}
 
     public function letters():array
     {
@@ -212,7 +272,7 @@
         ];
     }
 
-    protected function alphabetizeMe(
+    public function alphabetizeMe(
         array $list,
         string $name = '',
         string $url = '',
@@ -239,19 +299,35 @@
         return $list;
     }
 
+	public function directories(string $search = 'all'):array
+	{
+		$directories = $this->getDirectories();
+		if ($search === 'all') {
+			return $directories;
+		}
+		return $directories[$search]??[];
+	}
+
+	public function isDirectory():bool
+	{
+		return (is_post_type_archive(BASE.'directory') || is_singular(BASE.'directory'));
+	}
+
     private function renderArchive(): string
     {
+		$this->getDirectoryList();
 		return $this->cache->remember(
 			'archive',
 			function() {
 				$cache = '<h1>Directory of Directories</h1>
                 <p>You like lists? We\'ve got \'em!</p>
                 <section class="directories item-grid">';
-				foreach ($this->directories as $slug => $directory) {
+				foreach ($this->directoryList as $slug => $directory) {
+					$config = $this->getConfigFromType($slug);
 					$aOpen = '<a href="'.$directory['url'].'" title="See our list of '.$directory['title'].'">';
 					$aClose = '</a>';
 					$cache .= '<div class="directory col start">
-                '.$aOpen.jvbIcon($slug).$aClose.
+                '.$aOpen.jvbIcon($config['icon']).$aClose.
 						'<h2>'.$aOpen.$directory['title'].$aClose.'</h2>';
 					if (!empty($directory['description'])) {
 						$cache .= '<div class="description">';
@@ -273,11 +349,14 @@
 		$cache = $this->cache->remember(
 			'index',
 			function() {
-				$cache = '<nav class="directory"><ul>';
-				foreach (jvbDirectories() as $slug => $directory) {
+				$cache = '<nav class="directory condensed"><ul>';
+				foreach ($this->getDirectoryList() as $slug => $directory) {
+					$actualSlug = str_replace('-grouped', '', $slug);
+					$config = $this->getConfigFromType($actualSlug);
+					$icon = jvbIcon($config['icon']??'');
 					$cache .= '<li id="'.$slug.'">
-                    <a href="'.$directory['url'].'">'.
-						jvbIcon(str_replace('-grouped', '', $slug)).$directory['title'].'
+                    <a href="'.$directory['url'].'" class="'.$actualSlug.'">'.
+						$icon.$directory['title'].'
                     </a>
                 </li>';
 				}
@@ -285,7 +364,7 @@
 				return $cache;
 			}
 		);
-        if ($current !== '' && array_key_exists($current, jvbDirectories())) {
+        if ($current !== '' && array_key_exists($current, $this->directories())) {
             $open = ($open) ? ' open' : '';
             $cache = '<details'.$open.'><summary class="row btw">Other Directories:</summary>'.
                      str_replace('id="'.$current.'"', 'id="'.$current.'" class="current"', $cache)
@@ -301,21 +380,23 @@
         if ($slug === '') {
             return '';
         }
+		$this->directories();
 		return $this->cache->remember(
 			$slug,
 			function() use ($slug) {
-				$out = '<h1>'.$this->directories[$slug]['title'].'</h1>';
+				$config = $this->getConfigFromType($slug);
+				$out = '<h1>'.$config['directory'].'</h1>';
 				$out .= '<div class="description">';
 
-				foreach ($this->directories[$slug]['description']??[] as $p) {
+				foreach ($config[$slug]['description']??[] as $p) {
 					$out .= '<p>'.$p.'</p>';
 				}
 				$out .= '</div>';
 				$out .= $this->renderIndex($slug);
 
-				$data = $this->directories[$slug];
+				$type = $this->directories[$slug];
 				$list = [];
-				switch ($data['type']) {
+				switch ($type) {
 					case 'content':
 						$get = new WP_Query([
 							'post_type'            => jvbCheckBase($slug),
@@ -324,37 +405,40 @@
 							'order'                => 'ASC'
 						]);
 
+						$hasExtra = Features::forContent($slug)->has('directory_extra');
 						if ($get->have_posts()) {
 							while ( $get->have_posts() ) {
 								$get->the_post();
 								$extra = [];
-								foreach ( $data['extra'] as $item ) {
-									$item = jvbCheckBase( $item );
+								if ($hasExtra) {
+									foreach ($config['directory_extra'] as $item ) {
+										$item = jvbCheckBase( $item );
 
-									$terms = get_the_terms( get_the_ID(), jvbCheckBase( $item ) );
-									if ( $terms && ! is_wp_error( $terms ) ) {
-										$term    = $terms[0];
-										$extra[] = [
-											'name' => (get_term_meta( $term->term_id, BASE . 'singular', true ) !== '') ? get_term_meta( $term->term_id, BASE . 'singular', true ) : $term->name,
-											'url'  => get_term_link( $term->term_id, $item ),
-											'id'   => $term->term_id,
-											'type' => $item,
-										];
+										$terms = get_the_terms( get_the_ID(), jvbCheckBase( $item ) );
+										if ( $terms && ! is_wp_error( $terms ) ) {
+											$term    = $terms[0];
+											$extra[] = [
+												'name' => (get_term_meta( $term->term_id, BASE . 'singular', true ) !== '') ? get_term_meta( $term->term_id, BASE . 'singular', true ) : $term->name,
+												'url'  => get_term_link( $term->term_id, $item ),
+												'id'   => $term->term_id,
+												'type' => $item,
+											];
 
+										}
 									}
-									$list = $this->alphabetizeMe(
-										$list,
-										get_the_title(),
-										get_the_permalink(),
-										get_the_ID(),
-										$extra
-									);
 								}
+								$list = $this->alphabetizeMe(
+									$list,
+									get_the_title(),
+									get_the_permalink(),
+									get_the_ID(),
+									$extra
+								);
 							}
 						}
 						wp_reset_postdata();
 						break;
-					case 'tax':
+					case 'taxonomy':
 						$get = get_terms([
 							'taxonomy'            => jvbCheckBase($slug),
 							'hide_empty'        => true,
@@ -363,7 +447,7 @@
 						]);
 
 						if ($get && !is_wp_error($get)) {
-							$extra = false;
+							$extra = [];
 							foreach ($get as $term) {
 
 								$list = $this->alphabetizeMe(
@@ -384,9 +468,6 @@
 						]);
 
 						break;
-					default:
-						$list = [];
-						break;
 				}
 
 				$out .= '<section class="directory-list '.$slug.'">';
@@ -412,7 +493,7 @@
         }
 		return $this->cache->remember(
 			$slug.'_group',
-			function() {
+			function() use ($slug){
 				$out = '<h1>'.$this->directories[$slug]['title'].'</h1>';
 				$out .= '<div class="description">';
 
@@ -493,7 +574,7 @@
 
         $out = '<ul class="list-none">';
         foreach ($list as $letter => $items) {
-            $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw"><h3>'.strtoupper($letter).'</h3><ul>';
+            $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>';
             foreach ($items as $item) {
                 $extra = '';
                 if (!empty($item['extra'])) {
@@ -525,15 +606,13 @@
             return $content;
         }
 
-
         // For archive page
         if (is_post_type_archive(BASE.'directory') && $block['blockName'] === 'core/group') {
-            return ($block['attrs']['tagName']??false === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content;
+            return ($block['attrs']['tagName']??'' === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content;
         }
 
         // For single directory posts
         if ($block['blockName'] === 'core/group') {
-
             switch (get_post_meta(get_the_ID(), BASE.'grouped_directory', true)) {
                 case '':
                     return '<main>' . $this->renderDirectory() . '</main>';
@@ -545,15 +624,3 @@
         return $content;
     }
 }
-
-new DirectoryManager();
-
-function jvbDirectoryConfig():array
-{
-    $ID = get_the_ID();
-    if ($ID) {
-        return DirectoryManager::getConfig($ID);
-    }
-    return [];
-}
-
diff --git a/inc/managers/IconsManager.php b/inc/managers/IconsManager.php
index 383e687..d244340 100644
--- a/inc/managers/IconsManager.php
+++ b/inc/managers/IconsManager.php
@@ -43,7 +43,6 @@
 	{
 		$this->source = $source;
 		$this->cache = CacheManager::for('icons_' . $source, WEEK_IN_SECONDS);
-
 		$this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles))
 			? JVB_SITE['icons']
 			: 'regular';
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
index 538a5e8..32e91a6 100644
--- a/inc/managers/SEO/BreadcrumbManager.php
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -24,6 +24,7 @@
 	private function __construct()
 	{
 		$this->cache = CacheManager::for('breadcrumbs', MONTH_IN_SECONDS)->connectTo('all');
+//		$this->cache->clear();
 	}
 
 	public static function getInstance(): self
@@ -93,7 +94,8 @@
 			$crumbs = $this->addTaxonomyCrumbs($crumbs, $obj);
 		} elseif (is_singular()) {
 			$crumbs = $this->addArchiveCrumbs($crumbs, $obj);
-			$crumbs = $this->addSingularCrumbs($crumbs, $obj);
+			$hierarchy = $this->addSingularCrumbs($crumbs, $obj);
+			$crumbs = $crumbs + $hierarchy;
 		} elseif (is_post_type_archive() && !is_post_type_archive(BASE.'dash')) {
 			$crumbs = $this->addArchiveCrumbs($crumbs, $obj);
 		}
@@ -124,7 +126,7 @@
 
 		// Add directory if exists
 		if (Features::forTaxonomy($tax)->has('directory')) {
-			$directory = jvbDirectories($tax);
+			$directory = JVB()->directories()?->directories($tax);
 			$crumbs[] = [
 				'name' => $directory['title'],
 				'url'  => $directory['url']
@@ -143,23 +145,26 @@
 	private function addSingularCrumbs(array $crumbs, WP_Post $post): array
 	{
 		// Add directory if exists
-		$directory = jvbDirectories(jvbNoBase($post->post_type));
-		if (!empty($directory)) {
-			$crumbs[] = [
-				'name' => $directory['title'],
-				'url'  => $directory['url']
-			];
+		$content = jvbNoBase($post->post_type);
+		if(Features::forContent($content)->has('show_directory')) {
+			$directory = JVB()->directories()->getDirectoryList()[$content]??[];
+			if (!empty($directory)) {
+				$crumbs[] = [
+					'name'	=> $directory['title'],
+					'url'	=>$directory['url']
+				];
+			}
 		}
 
 		// Handle directory posts specially
-		if (jvbIsDirectory()) {
+		if (JVB()->directories()->isDirectory()) {
 			$pos = jvbGetDirectoryInfo();
 			if (!empty($pos)) {
 				// Special case for map
 				if ($pos['title'] == 'Map') {
 					$crumbs[] = [
 						'name' => 'Tattoo Shops',
-						'url'  => jvbDirectories(BASE.'shop')['url']
+						'url'  => JVB()->directories()?->directories(BASE.'shop')['url']
 					];
 				}
 
@@ -169,6 +174,10 @@
 				];
 			}
 		} else {
+			$name = jvbNoBase($post->post_type);
+			if (Features::forContent($name)->has('addCrumb')) {
+				$this->addTaxToCrumbs($crumbs, JVB_CONTENT[$name]['addCrumb']);
+			}
 			// Add post hierarchy
 			$crumbs = array_merge($crumbs, $this->buildPostHierarchy($post));
 		}
@@ -183,7 +192,13 @@
 	{
 		$type = is_singular() ? $obj->post_type : $obj->name;
 		$name = jvbNoBase($type);
-		if (array_key_exists($name, JVB_CONTENT)) {
+
+		if (Features::forSite()->has('is_directory') && $name === 'directory') {
+			$crumbs[] = [
+				'name'	=> 'Directory',
+				'url'	=> get_post_type_archive_link($type)
+			];
+		} elseif ((is_post_type_archive() || !Features::forContent($name)->has('show_directory')) && array_key_exists($name, JVB_CONTENT)) {
 			$crumbs[] = [
 				'name' => JVB_CONTENT[$name]['breadcrumb'] ?? JVB_CONTENT[$name]['plural'],
 				'url'  => get_post_type_archive_link($type)
@@ -339,4 +354,30 @@
 			$this->cache->clear();
 		}
 	}
+
+	public function addTaxToCrumbs(array $crumbs, string $taxonomy):array
+	{
+		$ID = get_the_ID();
+		$taxonomy = jvbCheckBase($taxonomy);
+		$terms = get_the_terms($ID, $taxonomy);
+		if ($terms && !is_wp_error($terms)) {
+			$term = $terms[0];
+			$ancestors = get_ancestors($term->term_id, $taxonomy, 'taxonomy');
+			$ancestors = array_reverse($ancestors);
+			foreach ($ancestors as $ancestor) {
+				$aTerm = get_term($ancestor, $taxonomy);
+				if ($aTerm && !is_wp_error($aTerm)) {
+					$crumbs[] = [
+						'name' => $aTerm->name,
+						'url'   => get_term_link($ancestor, $taxonomy)
+					];
+				}
+			}
+			$crumbs[] = [
+				'name' => $term->name,
+				'url'   => get_term_link($term, $taxonomy)
+			];
+		}
+		return $crumbs;
+	}
 }
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index e86780a..f59cdfd 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -343,6 +343,7 @@
 	);
 
 
+
 	//Notifications
 	wp_register_script(
 		'jvb-notifications',
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 733e141..aa537c0 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -818,7 +818,7 @@
 			//Processing Options
 			'max_size' => null, // Override default size limits
 			'convert' => 'webp', // Image conversion format
-			'quality' => 80, // Conversion quality
+			'quality' => 90, // Conversion quality
 			'create_thumbnails' => true,
 		];
 		$config = array_merge($defaultConfig, $field);
@@ -917,6 +917,7 @@
 					<?php endif; ?>
 					<div class="file-error"></div>
 				</div>
+				<?php jvbRenderProgressBar(); ?>
 			</div>
 
 
@@ -927,7 +928,7 @@
 						<div class="selection-controls">
 							<div class="selected">
 								<div class="field">
-									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
+									<input type="checkbox" id="select-all-uploads" data-select-all data-selects="item-grid" name="select-all-uploads">
 									<label for="select-all-uploads">
 										Select All
 									</label>
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index 95745c4..118111c 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -44,7 +44,25 @@
 		$this->userIDType = $this->getColumnType($this->userTable, 'ID');
 		$this->termIDType = $this->getColumnType($this->wpdb->terms, 'term_id');
 		$this->postIDType = $this->getColumnType($this->wpdb->posts, 'ID');
+
+		error_log("JVB DEBUG: userTable = " . $this->userTable);
+		error_log("JVB DEBUG: Users table exists = " . ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->userTable}'") ? 'yes' : 'no'));
+		error_log("JVB DEBUG: Users engine = " . $this->wpdb->get_var("SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}'"));
+		error_log("JVB DEBUG: MySQL version = " . $this->wpdb->db_version());
+		$usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->userTable}'");
+		$termsStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->terms}'");
+		error_log("JVB DEBUG: wp_users collation = " . $usersStatus->Collation);
+		error_log("JVB DEBUG: wp_terms collation = " . $termsStatus->Collation);
+		error_log("JVB DEBUG: Our charset_collate = " . $this->wpdb->get_charset_collate());
+
 		error_log("JVB FK Types: users.ID={$this->userIDType}, terms.term_id={$this->termIDType}, posts.ID={$this->postIDType}");
+
+		$usersDb = $this->wpdb->get_var(
+			"SELECT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}' LIMIT 1"
+		);
+		error_log("JVB DEBUG: wp_users is in database = " . $usersDb);
+		error_log("JVB DEBUG: Current DB_NAME = " . DB_NAME);
+		error_log("JVB DEBUG: Tables being created in = " . $this->wpdb->dbname);
 	}
 
 	protected function getMultisiteUsersTable():string
@@ -368,73 +386,70 @@
 		}
 	}
 
-    public function createTables(array $tables)
-    {
-        $charset_collate = $this->wpdb->get_charset_collate();
-        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+	public function createTables(array $tables)
+	{
+		require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
 
-        $errors = [];
-        $created = [];
+		// Match collation from existing WP tables for FK compatibility
+		$usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->users}'");
+		$parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci';
+		$charset_collate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}";
 
-        foreach ($tables as $name => $schema) {
-            $table_name = $this->prefix . BASE . $name;
-            $full_schema = "CREATE TABLE IF NOT EXISTS {$table_name} {$schema} {$charset_collate};";
+		error_log("JVB: Using charset_collate: " . $charset_collate);
 
-            // Clear any previous errors
-            $this->wpdb->flush();
+		$errors = [];
+		$created = [];
 
-            try {
-                $result = dbDelta($full_schema);
+		foreach ($tables as $name => $schema) {
+			$table_name = $this->prefix . BASE . $name;
 
-                // Check for SQL errors
-                if ($this->wpdb->last_error) {
-                    $error_msg = "SQL Error creating table {$table_name}: " . $this->wpdb->last_error;
-                    error_log($error_msg);
-                    error_log("Failed SQL Query: " . $full_schema);
-                    $errors[] = $error_msg;
+			// Skip if exists
+			if ($this->wpdb->get_var("SHOW TABLES LIKE '{$table_name}'")) {
+				$created[] = $table_name . ' (exists)';
+				continue;
+			}
 
-                    // Don't throw exception, just log and continue
-                    continue;
-                }
+			$full_schema = "CREATE TABLE IF NOT EXISTS {$table_name} {$schema} {$charset_collate}";
 
-                // Verify table was actually created
-                $table_exists = $this->wpdb->get_var("SHOW TABLES LIKE '{$table_name}'");
-                if ($table_exists) {
-                    $created[] = $table_name;
-                    error_log("Successfully created table: {$table_name}");
-                } else {
-                    $error_msg = "Table {$table_name} was not created (no error reported)";
-                    error_log($error_msg);
-                    error_log("Schema used: " . $full_schema);
-                    $errors[] = $error_msg;
-                }
+			$this->wpdb->flush();
 
-            } catch (Exception $e) {
-                $error_msg = "Exception creating table {$table_name}: " . $e->getMessage();
-                error_log($error_msg);
-                error_log("Exception SQL Query: " . $full_schema);
-                $errors[] = $error_msg;
-            }
-        }
+			// Use direct query - dbDelta mangles FK constraints
+			$hasForeignKey = stripos($schema, 'FOREIGN KEY') !== false;
 
-        // Log summary
-        if (!empty($created)) {
-            error_log("JVB Tables Created Successfully: " . implode(', ', $created));
-        }
+			if ($hasForeignKey) {
+				$result = $this->wpdb->query($full_schema);
+				$success = ($result !== false && !$this->wpdb->last_error);
+			} else {
+				dbDelta($full_schema . ';');
+				$success = !$this->wpdb->last_error;
+			}
 
-        if (!empty($errors)) {
-            error_log("JVB Table Creation Errors (" . count($errors) . " total):");
-            foreach ($errors as $error) {
-                error_log("  - " . $error);
-            }
+			if (!$success) {
+				$error_msg = "SQL Error creating table {$table_name}: " . $this->wpdb->last_error;
+				error_log($error_msg);
+				error_log("Failed SQL Query: " . $full_schema);
+				$errors[] = $error_msg;
+			} elseif ($this->wpdb->get_var("SHOW TABLES LIKE '{$table_name}'")) {
+				$created[] = $table_name;
+				error_log("Successfully created table: {$table_name}");
+			}
+		}
 
-            // Optionally store errors for admin display
-            update_option(BASE . 'table_creation_errors', $errors);
-        } else {
-            // Clear any previous errors
-            delete_option(BASE . 'table_creation_errors');
-        }
-    }
+		// Log summary
+		if (!empty($created)) {
+			error_log("JVB Tables Created Successfully: " . implode(', ', $created));
+		}
+
+		if (!empty($errors)) {
+			error_log("JVB Table Creation Errors (" . count($errors) . " total):");
+			foreach ($errors as $error) {
+				error_log("  - " . $error);
+			}
+			update_option(BASE . 'table_creation_errors', $errors);
+		} else {
+			delete_option(BASE . 'table_creation_errors');
+		}
+	}
 
     /******************************************************
      * Table Definitions
@@ -575,9 +590,9 @@
             KEY `requires_action` (`owner_id`, `requires_action`, `action_taken`),
             KEY `acting_user_lookup` (`owner_id`, `action_user_id`, `type`, `status`, `created_at`),
             CONSTRAINT `{$this->base}notify_owner` FOREIGN KEY (`owner_id`)
-            REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}action_id` FOREIGN KEY (`action_user_id`)
-            REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )",
 
 
@@ -617,7 +632,7 @@
             UNIQUE KEY `user_content_notif` (`user_id`, `content_notification_id`),
             KEY `user_status` (`user_id`, `status`),
             CONSTRAINT `{$this->base}user_content_user` FOREIGN KEY (`user_id`)
-            REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}user_content_notification` FOREIGN KEY (`content_notification_id`)
             REFERENCES `{$this->prefixed}notifications_content` (`id`) ON DELETE CASCADE
         )",
@@ -637,7 +652,7 @@
             KEY `user_frequency` (`user_id`, `frequency`),
             KEY `frequency_lookup` (`frequency`, `last_sent`),
             CONSTRAINT `{$this->base}notification_pref_user` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )",
 
             // Notification digest scheduling and tracking
@@ -654,7 +669,7 @@
             KEY `scheduled_digests` (`frequency`, `scheduled_at`, `status`),
             KEY `user_digests` (`user_id`, `frequency`),
             CONSTRAINT `{$this->base}digest_user` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )",
 
             // Analytics on notification interactions
@@ -671,9 +686,9 @@
             KEY `user_actions` (`user_id`, `action`),
             KEY `action_analysis` (`action`, `action_source`),
             CONSTRAINT `{$this->base}metrics_user` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}metrics_notification` FOREIGN KEY (`notification_id`)
-                REFERENCES {$this->prefixed}notifications (`id`) ON DELETE CASCADE
+                REFERENCES `{$this->prefixed}notifications` (`id`) ON DELETE CASCADE
         )"
         ];
     }
@@ -702,7 +717,7 @@
                 KEY `status` (`status`),
                 KEY `expiring_requests` (`status`, `expires_at`),
                 CONSTRAINT `{$this->base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
-                    REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
             )";
             $tables['approval_'.$type.'_votes'] = "(
                 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -715,9 +730,9 @@
                 UNIQUE KEY `unique_vote` (`request_id`, `user_id`),
                 KEY `user_votes` (`user_id`),
                 CONSTRAINT `{$this->base}{$type}_user_approval_request` FOREIGN KEY (`request_id`)
-                    REFERENCES {$this->prefixed}approval_{$type}_requests (`id`) ON DELETE CASCADE,
+                    REFERENCES `{$this->prefixed}approval_{$type}_requests` (`id`) ON DELETE CASCADE,
                 CONSTRAINT `{$this->base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`)
-                    REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
             )";
         }
         if (!empty($save)) {
@@ -747,9 +762,9 @@
 		KEY `related_taxonomy` (`related_taxonomy`),
 		UNIQUE KEY `term_relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`),
 		CONSTRAINT `{$this->base}tax_rel_term_id` FOREIGN KEY (`term_id`)
-			REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE,
+			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
 		CONSTRAINT `{$this->base}tax_rel_related_id` FOREIGN KEY (`related_term_id`)
-			REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
 	)"
 		];
 
@@ -767,9 +782,9 @@
 		KEY `user_taxonomy` (`user_id`, `taxonomy`),
 		KEY `taxonomy` (`taxonomy`),
 		CONSTRAINT `{$this->base}user_term_user_fk` FOREIGN KEY (`user_id`)
-			REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+			REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
 		CONSTRAINT `{$this->base}user_term_term_fk` FOREIGN KEY (`term_id`)
-			REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
 	)";
 		}
 
@@ -792,7 +807,7 @@
             KEY `user_type` (`user_id`, `type`),
             KEY `target_type` (`target_id`, `type`),
             CONSTRAINT `{$this->base}favourites_user` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )",
             'favourites_lists'  => "(
             `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -804,7 +819,7 @@
             PRIMARY KEY (`id`),
             KEY `user_lists` (`user_id`),
             CONSTRAINT `{$this->base}list_user` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )",
             'favourites_list_items' => "(
             `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -818,9 +833,9 @@
             KEY `list_items` (`list_id`),
             KEY `favourite_id` (`favourite_id`),
             CONSTRAINT `{$this->base}list_items` FOREIGN KEY (`list_id`)
-                REFERENCES {$this->prefixed}favourites_lists (`id`) ON DELETE CASCADE,
+                REFERENCES 	`{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}list_favourite` FOREIGN KEY (`favourite_id`)
-                REFERENCES {$this->prefixed}favourites (`id`) ON DELETE SET NULL
+                REFERENCES `{$this->prefixed}favourites` (`id`) ON DELETE SET NULL
         )",
             'favourites_list_shares' => "(
             `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -838,9 +853,9 @@
             KEY `list_shares` (`list_id`),
             KEY `status_index` (`status`),
             CONSTRAINT `{$this->base}share_list` FOREIGN KEY (`list_id`)
-                REFERENCES {$this->prefixed}favourites_lists (`id`) ON DELETE CASCADE,
+                REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}share_user` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )",
             'favourites_list_stats'    => "(
             `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -872,11 +887,11 @@
                 KEY `user_id` (`user_id`),
                 KEY `artist_id` (`artist_id`),
             CONSTRAINT `{$this->base}nr_shop_news_shop` FOREIGN KEY (`shop_id`)
-                REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE,
+                REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}nr_shop_news_user` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}nr_shop_news_artist` FOREIGN KEY (`artist_id`)
-                REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE SET NULL
+                REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL
         )"
         ];
     }
@@ -904,9 +919,9 @@
             KEY `parent_child` (`parent_id`),
             KEY `karma_order` (`karma`),
             CONSTRAINT `{$this->base}re_responses_news` FOREIGN KEY (`item_id`)
-                REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE CASCADE,
+                REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}re_responses_parent` FOREIGN KEY (`parent_id`)
-                REFERENCES {$this->prefixed}responses (`id`) ON DELETE SET NULL
+                REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE SET NULL
         )",
             'karma_response' => "(
                 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -919,9 +934,9 @@
                 KEY `item_id` (`item_id`),
                 KEY `user_id` (`user_id`),
             CONSTRAINT `{$this->base}_response_item_id` FOREIGN KEY (`item_id`)
-                REFERENCES {$this->prefixed}responses (`id`) ON DELETE CASCADE,
+                REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}_response_user_id` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
                 )"
         ];
     }
@@ -972,9 +987,9 @@
                 KEY `item_id` (`item_id`),
                 KEY `user_id` (`user_id`),
                 CONSTRAINT `{$this->base}kt_{$type}_item_id` FOREIGN KEY (`item_id`)
-                    REFERENCES {$reference_table} ({$reference_column}) ON DELETE CASCADE,
+                    REFERENCES `{$reference_table}` (`{$reference_column}`) ON DELETE CASCADE,
                 CONSTRAINT `{$this->base}kt_{$type}_user_id` FOREIGN KEY (`user_id`)
-                    REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
             )";
 
         }
@@ -1051,13 +1066,13 @@
                 KEY `rsvp_events` (`rsvp_required`, `rsvp_deadline`),
 
                 CONSTRAINT `{$this->base}cal_{$type}_type` FOREIGN KEY (`event_type`)
-                    REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE,
+                    REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
                 CONSTRAINT `{$this->base}cal_{$type}_post` FOREIGN KEY (`post_id`)
-                    REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE CASCADE,
+                    REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE,
                 CONSTRAINT `{$this->base}cal_{$type}_shop` FOREIGN KEY (`shop_id`)
-                    REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE SET NULL,
+                    REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE SET NULL,
                 CONSTRAINT `{$this->base}cal_{$type}_user` FOREIGN KEY (`user_id`)
-                    REFERENCES {$this->userTable} (`ID`) ON DELETE SET NULL
+                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE SET NULL
             )";
 
             $tables['calendar_'.$type.'_participants'] = "(
@@ -1069,9 +1084,9 @@
                 PRIMARY KEY (`id`),
                 UNIQUE KEY `event_user` (`event_id`, `user_id`),
                 CONSTRAINT `{$this->base}cal_{$type}_participant_event` FOREIGN KEY (`event_id`)
-                    REFERENCES {$this->prefixed}calendar_{$type} (`id`) ON DELETE CASCADE,
+                    REFERENCES `{$this->prefixed}calendar_{$type}` (`id`) ON DELETE CASCADE,
                 CONSTRAINT `{$this->base}cal_{$type}_participant_user` FOREIGN KEY (`user_id`)
-                    REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
             )";
 
             $tables['calendar_'.$type.'_recurrence_exceptions'] = "(
@@ -1117,9 +1132,9 @@
             KEY `user_idx` (`user_id`),
             KEY `owner_idx` (`owner_id`),
             CONSTRAINT `{$this->base}umami_user_id_link` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
             CONSTRAINT `{$this->base}umami_owner_id_link` FOREIGN KEY (`owner_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )",
             'stats_performance' => "(
             `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -1137,7 +1152,7 @@
             PRIMARY KEY (`id`),
             KEY `user_date_idx` (`user_id`, `date`),
             CONSTRAINT `{$this->base}performance_user_id_link` FOREIGN KEY (`user_id`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
         )"
         ];
     }
@@ -1170,7 +1185,7 @@
             )";
 			foreach($config['to_terms']??[] as $term) {
 				$definitions .= "CONSTRAINT `{$this->base}_{$term}_link` FOREIGN KEY (`to_{$term}`)
-                REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE";
+                REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE";
 			}
 
 			$tables['invitations_'.$role] = $definitions;
@@ -1199,9 +1214,9 @@
                     UNIQUE KEY `content_term` (`content_id`, `term_id`),
                     KEY content_role (`term_id`, `role`),
                     CONSTRAINT `{$this->base}{$content}_{$type}_history_user` FOREIGN KEY (`user_id`)
-                        REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+                        REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
                     CONSTRAINT `{$this->base}{$content}_{$type}_history_term` FOREIGN KEY (`term_id`)
-                        REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+                        REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
                 )";
             }
         }
@@ -1228,9 +1243,9 @@
                     PRIMARY KEY (`id`),
                     UNIQUE KEY `{$this->base}content_term` (`content_id`, `term_id`),
                     CONSTRAINT `{$this->base}{$content}_{$type}_request_user` FOREIGN KEY (`user_id`)
-                        REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+                        REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
                     CONSTRAINT `{$this->base}{$content}_{$type}_request_term` FOREIGN KEY (`term_id`)
-                        REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+                        REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
                 )";
             }
         }
@@ -1271,9 +1286,9 @@
 	KEY `date_idx` (`referred_at`),
 	KEY `consult_idx` (`consulted_at`),
 	CONSTRAINT `{$this->base}referral_referrer_fk` FOREIGN KEY (`referrer_id`)
-		REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
 	CONSTRAINT `{$this->base}referral_referee_fk` FOREIGN KEY (`referee_id`)
-		REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
 )";
 
 		// Create the main referrals table first
@@ -1297,7 +1312,7 @@
 	KEY `user_idx` (`user_id`),
 	KEY `email_idx` (`email`),
 	CONSTRAINT `{$this->base}jane_client_user_fk` FOREIGN KEY (`user_id`)
-		REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
 )";
 
 		// Third: referral_treatments (depends on referrals AND wp_users)
@@ -1317,9 +1332,9 @@
 	KEY `date_idx` (`treatment_date`),
 	KEY `type_idx` (`treatment_type`),
 	CONSTRAINT `{$this->base}treatment_referral_fk` FOREIGN KEY (`referral_id`)
-		REFERENCES {$this->prefixed}referrals (`id`) ON DELETE CASCADE,
+		REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
 	CONSTRAINT `{$this->base}treatment_user_fk` FOREIGN KEY (`user_id`)
-		REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
 )";
 
 		// Fourth: referral_rewards (depends on referrals AND wp_users)
@@ -1341,9 +1356,9 @@
 	KEY `status_idx` (`status`),
 	KEY `type_idx` (`reward_type`),
 	CONSTRAINT `{$this->base}reward_referral_fk` FOREIGN KEY (`referral_id`)
-		REFERENCES {$this->prefixed}referrals (`id`) ON DELETE CASCADE,
+		REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
 	CONSTRAINT `{$this->base}reward_user_fk` FOREIGN KEY (`user_id`)
-		REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
 )";
 
 		return $dependentTables;
@@ -1465,13 +1480,13 @@
 
 			if ($type === 'taxonomy') {
 				$constraints[] = "CONSTRAINT `{$constraintName}` FOREIGN KEY (`{$fieldName}`)
-                REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE SET NULL";
+                REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE SET NULL";
 			} elseif ($type === 'user') {
 				$constraints[] = "CONSTRAINT `{$constraintName}` FOREIGN KEY (`{$fieldName}`)
-                REFERENCES {$this->userTable} (`ID`) ON DELETE SET NULL";
+                REFERENCES `{$this->userTable}` (`ID`) ON DELETE SET NULL";
 			} elseif ($type === 'image' || $type === 'file') {
 				$constraints[] = "CONSTRAINT `{$constraintName}` FOREIGN KEY (`{$fieldName}`)
-                REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE SET NULL";
+                REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL";
 			}
 		}
 
@@ -1568,7 +1583,7 @@
 				["PRIMARY KEY (`term_id`)"],
 				$indexes,
 				$constraints,
-				["CONSTRAINT `{$base}_{$type}_term` FOREIGN KEY (`term_id`) REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE"]
+				["CONSTRAINT `{$base}_{$type}_term` FOREIGN KEY (`term_id`) REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE"]
 			);
 			$sql = "(\n    " . implode(",\n    ", $allDefinitions) . "\n)";
 			error_log("JVB: Creating content table for type: {$type}");
@@ -1732,7 +1747,7 @@
 
         $constraints = [
             "CONSTRAINT `{$this->base}_{$userType}_stats_user` FOREIGN KEY (`user_id`)
-         REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE"
+         REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE"
         ];
 
         $allDefinitions = array_merge($columns, $indexes, $constraints);
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index e323a70..468a1a7 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -36,7 +36,7 @@
     {
         $this->cache_name = 'user_content_'.get_current_user_id();
         parent::__construct();
-
+		$this->cache->clear();
         $this->action = 'dash-';
         $this->operation_type = 'content_update';
         add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -844,6 +844,7 @@
             $data['images'] = $images;
         }
 
+		error_log('Got Data for post: '.print_r($data, true));
         return $data;
     }
 	protected function extractImages(array $fields = []):array
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index f871502..92375c2 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -32,17 +32,17 @@
 		$this->cache_name = 'feed';
 		$this->cache_ttl = 86400;
 		parent::__construct();
+		$this->cache->clear();
 	}
 
 	public function init():void
 	{
+		$this->cache->clear();
 		$this->checker = Checker::getInstance();
 
 		if (jvbSiteUsesUmami()) {
 			$this->tracker = JVB()->connect('umami');
 		}
-
-		$this->cache->clear();
 		$this->setupCacheConnections();
 	}
 
@@ -111,9 +111,9 @@
 		if (!$post || is_wp_error($post)) {
 			return [];
 		}
-
-		return $cache->remember($postID,
-			function() use ($postID, $type, $metaType, $post, $skip) {
+//
+//		return $cache->remember($postID,
+//			function() use ($postID, $type, $metaType, $post, $skip) {
 				$config = null;
 				switch ($metaType) {
 					case 'post':
@@ -150,10 +150,10 @@
 					return $field['type'] === 'taxonomy';
 				});
 				foreach ($temp as $key => $config) {
-					if (array_key_exists($key, $out) && $out[$key] !== '') {
-						$IDs = array_map('absint', explode(',', $out[$key]));
+					if (array_key_exists($key, $out['fields']) && $out['fields'][$key] !== '') {
+						$IDs = array_map('absint', explode(',', $out['fields'][$key]));
 						$data = [];
-						$icon = JVB_TAXONOMY[$config['taxonomy']]['icon']??'triangle';
+						$icon = JVB_TAXONOMY[$config['taxonomy']]['icon']??jvbDefaultIcon();
 						foreach ($IDs as $ID) {
 							$term = get_term($ID, jvbCheckBase($config['taxonomy']));
 							if ($term && !is_wp_error($term)) {
@@ -179,9 +179,10 @@
 				$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]));
+					if (array_key_exists($key, $out['fields']) && $out['fields'][$key] !== '') {
+						$IDs = array_map('absint', explode(',',$out['fields'][$key]));
 						foreach ($IDs as $ID) {
 							$imgIDs[$ID] = jvbImageData($ID);
 						}
@@ -191,7 +192,7 @@
 
 				$out['id'] = $postID;
 				$out['content'] = $type;
-				$out['icon'] = $config['icon']??'triangle';
+				$out['icon'] = $config['icon']??jvbDefaultIcon();
 
 				if ($this->tracker) {
 					$args = ($metaType === 'post') ? ['owner_id' => $post->post_author] : [];
@@ -214,12 +215,14 @@
 						$out['url'] = get_the_permalink($postID);
 						break;
 				}
-				return $out;
-			}
-		);
-
+//				return $out;
+//			}
+//		);
+		return $out;
 	}
 
+
+
 	protected function initTimelineFields(string $content):void
 	{
 		$content = jvbNoBase($content);
@@ -336,7 +339,6 @@
 	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'] :
@@ -362,7 +364,6 @@
 		$args = $this->applyDateFilters($args, $data);
 
 		$args = $this->applyFavouritesFilter($args, $data);
-		error_log('Final Args: '.print_r($args, true));
 		return $args;
 	}
 
@@ -438,9 +439,6 @@
 		// 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);
 
@@ -536,7 +534,6 @@
 	 */
 	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;
 		}
@@ -627,7 +624,6 @@
 		if (!array_key_exists('favourites', $filters)) {
 			return $args;
 		}
-		error_log('Proceeding to check for favourites:');
 		global $wpdb;
 
 		// Get post types for the current filter
@@ -637,7 +633,6 @@
 
 		$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)",
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index 5695d64..f1efa39 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -376,7 +376,7 @@
 		}
 
 		$type = $field_config['type'] ?? 'text';
-		$type = $field_config['subType']?:$type;
+		$type = $field_config['subType'] ?? $type;
 
 		switch ($type) {
 			case 'phone':
diff --git a/inc/rest/routes/NotificationsRoutes.php b/inc/rest/routes/NotificationsRoutes.php
index ea6fa2c..a6edcb1 100644
--- a/inc/rest/routes/NotificationsRoutes.php
+++ b/inc/rest/routes/NotificationsRoutes.php
@@ -198,7 +198,6 @@
      */
     protected function getItemLink(int $ID, string $content):string
     {
-        error_log('Type: '.print_r($content, true));
         switch ($content) {
             case BASE.'artist':
             case BASE.'artwork':
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 742e295..6052ab5 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -1092,10 +1092,6 @@
 			$args = $this->buildUploadArgs($request);
 			$data = $request->get_params();
 
-
-
-			error_log('[UploadRoutes]:handleGroupingRequest: data'.print_r($data, true));
-			error_log('[UploadRoutes]:handleGroupingRequest: args'.print_r($args, true));
 			error_log('get_file_params: '.print_r($files, true));
 			global $_FILES;
 			error_log('Global Files: '.print_r($_FILES, true));
@@ -1121,7 +1117,7 @@
 			}
 
 			// Queue file upload operation
-			$operation_type = $this->determineOperationType($secured_data['files'][0] ?? []);
+			$operation_type = $this->determineOperationType($secured_files['files'][0] ?? []);
 			$chunkSize = 5;
 			if ($operation_type === 'video') {
 				$chunkSize = 1;
@@ -1230,11 +1226,12 @@
 			$created_posts = [];
 			$used_upload_ids = [];
 
+			error_log('Processing Group Data: '.print_r($data, true));
 			// Create posts from groups
 			foreach ($data['posts'] as $index => $post) {
 				$post_title = !empty($post['fields']['post_title'])
 					? sanitize_text_field($post['fields']['post_title'])
-					: 'New ' . JVB_CONTENT[$content]['singular'] . ' ' . ($index + 1);
+					: 'New ' . JVB_CONTENT[$data['content']]['singular'] . ' ' . ($index + 1);
 
 				$post_excerpt = !empty($post['fields']['post_excerpt'])
 					? sanitize_textarea_field($post['fields']['post_excerpt'])
@@ -1282,7 +1279,7 @@
 						set_post_thumbnail($new_post_id, (int)$gallery_attachment_ids[0]);
 						array_shift($gallery_attachment_ids);
 					}
-
+					error_log('Remaining Gallery images: '.print_r($gallery_attachment_ids, true));
 					// Set gallery images
 					if (!empty($gallery_attachment_ids)) {
 						$meta = new MetaManager($new_post_id, 'post');
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index 10b30cb..e8c0478 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -36,7 +36,7 @@
 	protected string $dataType = '';
 	protected string $singular = '';
 	protected string $plural = '';
-	protected string $icon = 'triangle';
+	protected string $icon;
 
 	// Capabilities
 	protected array $caps = [];
@@ -134,6 +134,7 @@
 	protected array $additionalClasses = [];
 
 	public function __construct() {
+		$this->icon = jvbDefaultIcon();
 		$this->user = wp_get_current_user();
 		$this->user_id = $this->user->ID;
 	}
diff --git a/inc/ui/Navigation.php b/inc/ui/Navigation.php
index 7988917..16a7e66 100644
--- a/inc/ui/Navigation.php
+++ b/inc/ui/Navigation.php
@@ -30,6 +30,8 @@
 	private bool $hasToggle = false;
 	protected array $defaultItemClasses = [];
 	private int $counter = 0;
+	private bool $isDrawer = false;
+	private bool $drawerCollapsed = true;
 
 	public function __construct(string $id = '') {
 		$this->id = $id ?: 'menu-' . uniqid();
@@ -124,11 +126,19 @@
 		if ($this->isNav) {
 			$html = '<nav id="' . esc_attr($this->id).'"' . $classStr.'>';
 
-			if ($this->hasToggle) {
+			// Drawer toggle or regular toggle
+			if ($this->isDrawer) {
+				$html .= '<button class="toggle main" type="button"
+                aria-expanded="' . ($this->drawerCollapsed ? 'false' : 'true') . '"
+                aria-controls="' . esc_attr($this->id) . '-list">
+                ' . jvbIcon('caret-left') . '
+                <span class="screen-reader-text">Toggle Menu</span>
+            </button>';
+			} elseif ($this->hasToggle) {
 				$html .= '<button class="toggle main" type="button" aria-expanded="false" aria-controls="' . esc_attr($this->id) . '">
-				' . jvbIcon('list') . '
-				<span class="screen-reader-text">Toggle Menu</span>
-			</button>';
+                ' . jvbIcon('list') . '
+                <span class="screen-reader-text">Toggle Menu</span>
+            </button>';
 			}
 		}
 		if (!$this->isNav) {
@@ -154,6 +164,95 @@
 		echo $html;
 		return $html;
 	}
+
+	/**
+	 * Configure as a drawer-style menu
+	 *
+	 * @param bool $collapsed Initial state
+	 * @return self
+	 */
+	public function asDrawer(bool $collapsed = true): self {
+		$this->isDrawer = true;
+		$this->drawerCollapsed = $collapsed;
+		$this->addClass('drawer');
+		if (!$collapsed) {
+			$this->addClass('open');
+		}
+		return $this;
+	}
+
+	public function isDrawer(): bool {
+		return $this->isDrawer;
+	}
+	/**
+	 * Add a section header
+	 *
+	 * @param string $title
+	 * @return MenuSection
+	 */
+	public function addSection(string $title): MenuSection {
+		$section = new MenuSection($title, ++$this->counter);
+		$this->items[] = $section;
+
+		if (!empty($this->defaultItemClasses)) {
+			foreach ($this->defaultItemClasses as $class) {
+				$section->addItemClass($class);
+			}
+		}
+
+		return $section;
+	}
+
+	/**
+	 * Populate menu from array structure
+	 *
+	 * @param array $items Array of menu items
+	 * @return self
+	 */
+	public function populateFromArray(array $items): self {
+		foreach ($items as $item) {
+			// Handle sections
+			if (!empty($item['section'])) {
+				$section = $this->addSection($item['section']);
+				if (!empty($item['items'])) {
+					$this->populateSection($section, $item['items']);
+				}
+				continue;
+			}
+
+			// Handle regular items
+			$menuItem = $this->addItem($item['text'] ?? '', $item['icon'] ?? '');
+
+			if (!empty($item['class'])) {
+				$menuItem->addClass($item['class']);
+			}
+			if (!empty($item['url'])) {
+				$menuItem->url($item['url']);
+			}
+
+			if (!empty($item['submenu'])) {
+				$submenu = $menuItem->submenu();
+				$submenu->populateFromArray($item['submenu']);
+			}
+		}
+
+		return $this;
+	}
+
+	private function populateSection(MenuSection $section, array $items): void {
+		foreach ($items as $item) {
+			$menuItem = $section->addItem($item['text'] ?? '', $item['icon'] ?? null);
+
+			if (!empty($item['url'])) {
+				$menuItem->url($item['url']);
+			}
+
+			if (!empty($item['submenu'])) {
+				$submenu = $menuItem->submenu();
+				$submenu->populateFromArray($item['submenu']);
+			}
+		}
+	}
 }
 
 /**
@@ -324,3 +423,66 @@
 		return $html;
 	}
 }
+
+/**
+ * Menu section with header and items
+ */
+class MenuSection {
+	private int $id;
+	private string $title;
+	private array $items = [];
+	private array $classes = [];
+	private array $defaultItemClasses = [];
+	private int $counter = 0;
+
+	public function __construct(string $title, int $id) {
+		$this->title = $title;
+		$this->id = $id;
+	}
+
+	public function addItem(?string $text = null, ?string $icon = null): MenuItem {
+		$item = new MenuItem(++$this->counter);
+		$this->items[] = $item;
+
+		if ($text) $item->text($text);
+		if ($icon) $item->icon($icon);
+
+		if (!empty($this->defaultItemClasses)) {
+			foreach ($this->defaultItemClasses as $class) {
+				$item->addClass($class);
+			}
+		}
+
+		return $item;
+	}
+
+	public function addClass(string $class): self {
+		$this->classes[] = $class;
+		return $this;
+	}
+
+	public function addItemClass(string $class): self {
+		$this->defaultItemClasses[] = $class;
+		return $this;
+	}
+
+	public function render(): string {
+		if (empty($this->items)) {
+			return '';
+		}
+
+		$classStr = !empty($this->classes) ? ' class="menu-section ' . esc_attr(implode(' ', $this->classes)) . '"' : ' class="menu-section"';
+
+		$html = '<li' . $classStr . '>';
+		$html .= '<span class="section-title">' . esc_html($this->title) . '</span>';
+		$html .= '<ul class="section-items">';
+
+		foreach ($this->items as $item) {
+			$html .= $item->render();
+		}
+
+		$html .= '</ul></li>';
+
+		return $html;
+	}
+}
diff --git a/inc/utility/Validator.php b/inc/utility/Validator.php
index d6505b2..f77dd52 100644
--- a/inc/utility/Validator.php
+++ b/inc/utility/Validator.php
@@ -342,7 +342,7 @@
 			$validTypes = [
 				'text', 'textarea', 'number', 'email', 'url', 'select',
 				'radio', 'checkbox', 'true_false', 'date', 'time',
-				'datetime', 'color', 'image', 'file', 'gallery',
+				'datetime', 'color', 'upload', 'image', 'file', 'gallery',
 				'repeater', 'location', 'user', 'taxonomy', 'set'
 			];
 
diff --git a/package-lock.json b/package-lock.json
index 180947c..dfb64fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
             "version": "0.1.0",
             "license": "GPL-2.0-or-later",
             "devDependencies": {
-                "@wordpress/scripts": "^30.26.0"
+                "@wordpress/scripts": "^31.2.0"
             }
         },
         "node_modules/@ampproject/remapping": {
@@ -26,16 +26,37 @@
                 "node": ">=6.0.0"
             }
         },
-        "node_modules/@babel/code-frame": {
-            "version": "7.26.2",
-            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
-            "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+        "node_modules/@asamuzakjp/css-color": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+            "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@babel/helper-validator-identifier": "^7.25.9",
+                "@csstools/css-calc": "^2.1.3",
+                "@csstools/css-color-parser": "^3.0.9",
+                "@csstools/css-parser-algorithms": "^3.0.4",
+                "@csstools/css-tokenizer": "^3.0.3",
+                "lru-cache": "^10.4.3"
+            }
+        },
+        "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+            "version": "10.4.3",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+            "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+            "dev": true,
+            "license": "ISC"
+        },
+        "node_modules/@babel/code-frame": {
+            "version": "7.27.1",
+            "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+            "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@babel/helper-validator-identifier": "^7.27.1",
                 "js-tokens": "^4.0.0",
-                "picocolors": "^1.0.0"
+                "picocolors": "^1.1.1"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -336,9 +357,9 @@
             }
         },
         "node_modules/@babel/helper-validator-identifier": {
-            "version": "7.27.1",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
-            "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+            "version": "7.28.5",
+            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+            "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
             "dev": true,
             "license": "MIT",
             "engines": {
@@ -371,27 +392,27 @@
             }
         },
         "node_modules/@babel/helpers": {
-            "version": "7.26.7",
-            "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
-            "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
+            "version": "7.28.4",
+            "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+            "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@babel/template": "^7.25.9",
-                "@babel/types": "^7.26.7"
+                "@babel/template": "^7.27.2",
+                "@babel/types": "^7.28.4"
             },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/parser": {
-            "version": "7.26.7",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
-            "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
+            "version": "7.28.5",
+            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+            "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@babel/types": "^7.26.7"
+                "@babel/types": "^7.28.5"
             },
             "bin": {
                 "parser": "bin/babel-parser.js"
@@ -1873,28 +1894,25 @@
             }
         },
         "node_modules/@babel/runtime": {
-            "version": "7.25.7",
-            "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
-            "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+            "version": "7.28.4",
+            "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+            "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
             "dev": true,
             "license": "MIT",
-            "dependencies": {
-                "regenerator-runtime": "^0.14.0"
-            },
             "engines": {
                 "node": ">=6.9.0"
             }
         },
         "node_modules/@babel/template": {
-            "version": "7.25.9",
-            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
-            "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+            "version": "7.27.2",
+            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+            "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@babel/code-frame": "^7.25.9",
-                "@babel/parser": "^7.25.9",
-                "@babel/types": "^7.25.9"
+                "@babel/code-frame": "^7.27.1",
+                "@babel/parser": "^7.27.2",
+                "@babel/types": "^7.27.1"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1920,14 +1938,14 @@
             }
         },
         "node_modules/@babel/types": {
-            "version": "7.28.4",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
-            "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+            "version": "7.28.5",
+            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+            "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "@babel/helper-string-parser": "^7.27.1",
-                "@babel/helper-validator-identifier": "^7.27.1"
+                "@babel/helper-validator-identifier": "^7.28.5"
             },
             "engines": {
                 "node": ">=6.9.0"
@@ -1940,50 +1958,40 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/@cacheable/memoize": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.3.tgz",
-            "integrity": "sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@cacheable/utils": "^2.0.3"
-            }
-        },
         "node_modules/@cacheable/memory": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.3.tgz",
-            "integrity": "sha512-R3UKy/CKOyb1LZG/VRCTMcpiMDyLH7SH3JrraRdK6kf3GweWCOU3sgvE13W3TiDRbxnDKylzKJvhUAvWl9LQOA==",
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.6.tgz",
+            "integrity": "sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@cacheable/memoize": "^2.0.3",
-                "@cacheable/utils": "^2.0.3",
-                "@keyv/bigmap": "^1.0.2",
-                "hookified": "^1.12.1",
-                "keyv": "^5.5.3"
+                "@cacheable/utils": "^2.3.2",
+                "@keyv/bigmap": "^1.3.0",
+                "hookified": "^1.13.0",
+                "keyv": "^5.5.4"
             }
         },
         "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.1.0.tgz",
-            "integrity": "sha512-MX7XIUNwVRK+hjZcAbNJ0Z8DREo+Weu9vinBOjGU1thEi9F6vPhICzBbk4CCf3eEefKRz7n6TfZXwUFZTSgj8Q==",
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.0.tgz",
+            "integrity": "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "hookified": "^1.12.2"
+                "hashery": "^1.2.0",
+                "hookified": "^1.13.0"
             },
             "engines": {
                 "node": ">= 18"
             },
             "peerDependencies": {
-                "keyv": "^5.5.3"
+                "keyv": "^5.5.4"
             }
         },
         "node_modules/@cacheable/memory/node_modules/keyv": {
-            "version": "5.5.3",
-            "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
-            "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+            "version": "5.5.5",
+            "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
+            "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
             "dev": true,
             "license": "MIT",
             "peer": true,
@@ -1992,25 +2000,98 @@
             }
         },
         "node_modules/@cacheable/utils": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.1.0.tgz",
-            "integrity": "sha512-ZdxfOiaarMqMj+H7qwlt5EBKWaeGihSYVHdQv5lUsbn8MJJOTW82OIwirQ39U5tMZkNvy3bQE+ryzC+xTAb9/g==",
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.2.tgz",
+            "integrity": "sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "keyv": "^5.5.3"
+                "hashery": "^1.2.0",
+                "keyv": "^5.5.4"
             }
         },
         "node_modules/@cacheable/utils/node_modules/keyv": {
-            "version": "5.5.3",
-            "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
-            "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+            "version": "5.5.5",
+            "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
+            "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "@keyv/serialize": "^1.1.1"
             }
         },
+        "node_modules/@csstools/color-helpers": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+            "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/csstools"
+                },
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/csstools"
+                }
+            ],
+            "license": "MIT-0",
+            "engines": {
+                "node": ">=18"
+            }
+        },
+        "node_modules/@csstools/css-calc": {
+            "version": "2.1.4",
+            "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+            "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/csstools"
+                },
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/csstools"
+                }
+            ],
+            "license": "MIT",
+            "engines": {
+                "node": ">=18"
+            },
+            "peerDependencies": {
+                "@csstools/css-parser-algorithms": "^3.0.5",
+                "@csstools/css-tokenizer": "^3.0.4"
+            }
+        },
+        "node_modules/@csstools/css-color-parser": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+            "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/csstools"
+                },
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/csstools"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "@csstools/color-helpers": "^5.1.0",
+                "@csstools/css-calc": "^2.1.4"
+            },
+            "engines": {
+                "node": ">=18"
+            },
+            "peerDependencies": {
+                "@csstools/css-parser-algorithms": "^3.0.5",
+                "@csstools/css-tokenizer": "^3.0.4"
+            }
+        },
         "node_modules/@csstools/css-parser-algorithms": {
             "version": "3.0.5",
             "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
@@ -2035,6 +2116,26 @@
                 "@csstools/css-tokenizer": "^3.0.4"
             }
         },
+        "node_modules/@csstools/css-syntax-patches-for-csstree": {
+            "version": "1.0.22",
+            "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.22.tgz",
+            "integrity": "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/csstools"
+                },
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/csstools"
+                }
+            ],
+            "license": "MIT-0",
+            "engines": {
+                "node": ">=18"
+            }
+        },
         "node_modules/@csstools/css-tokenizer": {
             "version": "3.0.4",
             "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
@@ -2101,6 +2202,40 @@
                 "url": "https://github.com/sponsors/JounQin"
             }
         },
+        "node_modules/@emnapi/core": {
+            "version": "1.7.1",
+            "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
+            "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "@emnapi/wasi-threads": "1.1.0",
+                "tslib": "^2.4.0"
+            }
+        },
+        "node_modules/@emnapi/runtime": {
+            "version": "1.7.1",
+            "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
+            "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "tslib": "^2.4.0"
+            }
+        },
+        "node_modules/@emnapi/wasi-threads": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+            "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "tslib": "^2.4.0"
+            }
+        },
         "node_modules/@es-joy/jsdoccomment": {
             "version": "0.41.0",
             "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz",
@@ -2149,9 +2284,9 @@
             }
         },
         "node_modules/@eslint-community/regexpp": {
-            "version": "4.12.1",
-            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
-            "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+            "version": "4.12.2",
+            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+            "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
             "dev": true,
             "license": "MIT",
             "engines": {
@@ -2217,9 +2352,9 @@
             }
         },
         "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+            "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -2519,6 +2654,228 @@
                 "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
             }
         },
+        "node_modules/@jest/environment-jsdom-abstract": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz",
+            "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/environment": "30.2.0",
+                "@jest/fake-timers": "30.2.0",
+                "@jest/types": "30.2.0",
+                "@types/jsdom": "^21.1.7",
+                "@types/node": "*",
+                "jest-mock": "30.2.0",
+                "jest-util": "30.2.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            },
+            "peerDependencies": {
+                "canvas": "^3.0.0",
+                "jsdom": "*"
+            },
+            "peerDependenciesMeta": {
+                "canvas": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
+            "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/fake-timers": "30.2.0",
+                "@jest/types": "30.2.0",
+                "@types/node": "*",
+                "jest-mock": "30.2.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
+            "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/types": "30.2.0",
+                "@sinonjs/fake-timers": "^13.0.0",
+                "@types/node": "*",
+                "jest-message-util": "30.2.0",
+                "jest-mock": "30.2.0",
+                "jest-util": "30.2.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": {
+            "version": "30.0.5",
+            "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+            "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@sinclair/typebox": "^0.34.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
+            "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/pattern": "30.0.1",
+                "@jest/schemas": "30.0.5",
+                "@types/istanbul-lib-coverage": "^2.0.6",
+                "@types/istanbul-reports": "^3.0.4",
+                "@types/node": "*",
+                "@types/yargs": "^17.0.33",
+                "chalk": "^4.1.2"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": {
+            "version": "0.34.41",
+            "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
+            "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": {
+            "version": "13.0.5",
+            "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+            "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+            "dev": true,
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "@sinonjs/commons": "^3.0.1"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+            "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": {
+            "version": "4.3.1",
+            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
+            "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/sibiraj-s"
+                }
+            ],
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
+            "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@babel/code-frame": "^7.27.1",
+                "@jest/types": "30.2.0",
+                "@types/stack-utils": "^2.0.3",
+                "chalk": "^4.1.2",
+                "graceful-fs": "^4.2.11",
+                "micromatch": "^4.0.8",
+                "pretty-format": "30.2.0",
+                "slash": "^3.0.0",
+                "stack-utils": "^2.0.6"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
+            "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/types": "30.2.0",
+                "@types/node": "*",
+                "jest-util": "30.2.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
+            "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/types": "30.2.0",
+                "@types/node": "*",
+                "chalk": "^4.1.2",
+                "ci-info": "^4.2.0",
+                "graceful-fs": "^4.2.11",
+                "picomatch": "^4.0.2"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+            "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
+            "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/schemas": "30.0.5",
+                "ansi-styles": "^5.2.0",
+                "react-is": "^18.3.1"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
         "node_modules/@jest/expect": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
@@ -2580,6 +2937,30 @@
                 "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
             }
         },
+        "node_modules/@jest/pattern": {
+            "version": "30.0.1",
+            "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
+            "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*",
+                "jest-regex-util": "30.0.1"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/@jest/pattern/node_modules/jest-regex-util": {
+            "version": "30.0.1",
+            "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
+            "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
         "node_modules/@jest/reporters": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
@@ -2837,6 +3218,19 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/@napi-rs/wasm-runtime": {
+            "version": "0.2.12",
+            "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
+            "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "@emnapi/core": "^1.4.3",
+                "@emnapi/runtime": "^1.4.3",
+                "@tybys/wasm-util": "^0.10.0"
+            }
+        },
         "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
             "version": "5.1.1-v1",
             "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3464,9 +3858,9 @@
             }
         },
         "node_modules/@opentelemetry/semantic-conventions": {
-            "version": "1.37.0",
-            "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz",
-            "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==",
+            "version": "1.38.0",
+            "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
+            "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
             "dev": true,
             "license": "Apache-2.0",
             "peer": true,
@@ -3825,14 +4219,14 @@
             }
         },
         "node_modules/@playwright/test": {
-            "version": "1.56.1",
-            "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
-            "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
+            "version": "1.57.0",
+            "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
+            "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
             "dev": true,
             "license": "Apache-2.0",
             "peer": true,
             "dependencies": {
-                "playwright": "1.56.1"
+                "playwright": "1.57.0"
             },
             "bin": {
                 "playwright": "cli.js"
@@ -3954,9 +4348,9 @@
             "license": "MIT"
         },
         "node_modules/@sentry/core": {
-            "version": "9.46.0",
-            "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.46.0.tgz",
-            "integrity": "sha512-it7JMFqxVproAgEtbLgCVBYtQ9fIb+Bu0JD+cEplTN/Ukpe6GaolyYib5geZqslVxhp2sQgT+58aGvfd/k0N8Q==",
+            "version": "9.47.1",
+            "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.47.1.tgz",
+            "integrity": "sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==",
             "dev": true,
             "license": "MIT",
             "engines": {
@@ -3964,9 +4358,9 @@
             }
         },
         "node_modules/@sentry/node": {
-            "version": "9.46.0",
-            "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.46.0.tgz",
-            "integrity": "sha512-pRLqAcd7GTGvN8gex5FtkQR5Mcol8gOy1WlyZZFq4rBbVtMbqKOQRhohwqnb+YrnmtFpj7IZ7KNDo077MvNeOQ==",
+            "version": "9.47.1",
+            "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.47.1.tgz",
+            "integrity": "sha512-CDbkasBz3fnWRKSFs6mmaRepM2pa+tbZkrqhPWifFfIkJDidtVW40p6OnquTvPXyPAszCnDZRnZT14xyvNmKPQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -4000,9 +4394,9 @@
                 "@opentelemetry/sdk-trace-base": "^1.30.1",
                 "@opentelemetry/semantic-conventions": "^1.34.0",
                 "@prisma/instrumentation": "6.11.1",
-                "@sentry/core": "9.46.0",
-                "@sentry/node-core": "9.46.0",
-                "@sentry/opentelemetry": "9.46.0",
+                "@sentry/core": "9.47.1",
+                "@sentry/node-core": "9.47.1",
+                "@sentry/opentelemetry": "9.47.1",
                 "import-in-the-middle": "^1.14.2",
                 "minimatch": "^9.0.0"
             },
@@ -4011,14 +4405,14 @@
             }
         },
         "node_modules/@sentry/node-core": {
-            "version": "9.46.0",
-            "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.46.0.tgz",
-            "integrity": "sha512-XRVu5pqoklZeh4wqhxCLZkz/ipoKhitctgEFXX9Yh1e1BoHM2pIxT52wf+W6hHM676TFmFXW3uKBjsmRM3AjgA==",
+            "version": "9.47.1",
+            "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.47.1.tgz",
+            "integrity": "sha512-7TEOiCGkyShJ8CKtsri9lbgMCbB+qNts2Xq37itiMPN2m+lIukK3OX//L8DC5nfKYZlgikrefS63/vJtm669hQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@sentry/core": "9.46.0",
-                "@sentry/opentelemetry": "9.46.0",
+                "@sentry/core": "9.47.1",
+                "@sentry/opentelemetry": "9.47.1",
                 "import-in-the-middle": "^1.14.2"
             },
             "engines": {
@@ -4035,13 +4429,13 @@
             }
         },
         "node_modules/@sentry/opentelemetry": {
-            "version": "9.46.0",
-            "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.46.0.tgz",
-            "integrity": "sha512-w2zTxqrdmwRok0cXBoh+ksXdGRUHUZhlpfL/H2kfTodOL+Mk8rW72qUmfqQceXoqgbz8UyK8YgJbyt+XS5H4Qg==",
+            "version": "9.47.1",
+            "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.47.1.tgz",
+            "integrity": "sha512-STtFpjF7lwzeoedDJV+5XA6P89BfmFwFftmHSGSe3UTI8z8IoiR5yB6X2vCjSPvXlfeOs13qCNNCEZyznxM8Xw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@sentry/core": "9.46.0"
+                "@sentry/core": "9.47.1"
             },
             "engines": {
                 "node": ">=18"
@@ -4400,16 +4794,6 @@
                 "url": "https://github.com/sponsors/gregberge"
             }
         },
-        "node_modules/@tootallnate/once": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
-            "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 10"
-            }
-        },
         "node_modules/@tootallnate/quickjs-emscripten": {
             "version": "0.23.0",
             "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
@@ -4427,6 +4811,17 @@
                 "node": ">=10.13.0"
             }
         },
+        "node_modules/@tybys/wasm-util": {
+            "version": "0.10.1",
+            "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+            "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "tslib": "^2.4.0"
+            }
+        },
         "node_modules/@types/babel__core": {
             "version": "7.20.5",
             "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4638,9 +5033,9 @@
             }
         },
         "node_modules/@types/jsdom": {
-            "version": "20.0.1",
-            "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
-            "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
+            "version": "21.1.7",
+            "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
+            "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -5219,6 +5614,275 @@
             "dev": true,
             "license": "ISC"
         },
+        "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
+            "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
+            "cpu": [
+                "arm"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "android"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-android-arm64": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
+            "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "android"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-darwin-arm64": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
+            "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "darwin"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-darwin-x64": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
+            "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "darwin"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-freebsd-x64": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
+            "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "freebsd"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
+            "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
+            "cpu": [
+                "arm"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
+            "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
+            "cpu": [
+                "arm"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
+            "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
+            "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
+            "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
+            "cpu": [
+                "ppc64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
+            "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
+            "cpu": [
+                "riscv64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
+            "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
+            "cpu": [
+                "riscv64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
+            "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
+            "cpu": [
+                "s390x"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
+            "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
+            "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "linux"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
+            "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
+            "cpu": [
+                "wasm32"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "dependencies": {
+                "@napi-rs/wasm-runtime": "^0.2.11"
+            },
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
+            "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "win32"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
+            "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
+            "cpu": [
+                "ia32"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "win32"
+            ]
+        },
+        "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
+            "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "license": "MIT",
+            "optional": true,
+            "os": [
+                "win32"
+            ]
+        },
         "node_modules/@webassemblyjs/ast": {
             "version": "1.14.1",
             "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@@ -5428,9 +6092,9 @@
             }
         },
         "node_modules/@wordpress/babel-preset-default": {
-            "version": "8.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.33.0.tgz",
-            "integrity": "sha512-zi+TfLm7w8UmC/IE1b6/z+GIRMvv9s6yQ7+2a3XUEFriAiLwVM2cRXTcauaKkcos3BDi35M0V8x0T7980RwTlQ==",
+            "version": "8.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.37.0.tgz",
+            "integrity": "sha512-tPLa2YFAau+gT2zKovRK5DRiJchsKgNFiuElIWeKYkLytkyws9fQAgjF/AkDFFxRMdDocNfCscpKWhp79+tIFA==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
@@ -5440,8 +6104,8 @@
                 "@babel/plugin-transform-runtime": "7.25.7",
                 "@babel/preset-env": "7.25.7",
                 "@babel/preset-typescript": "7.25.7",
-                "@wordpress/browserslist-config": "^6.33.0",
-                "@wordpress/warning": "^3.33.0",
+                "@wordpress/browserslist-config": "^6.37.0",
+                "@wordpress/warning": "^3.37.0",
                 "browserslist": "^4.21.10",
                 "core-js": "^3.31.0",
                 "react": "^18.3.0"
@@ -5616,9 +6280,9 @@
             }
         },
         "node_modules/@wordpress/base-styles": {
-            "version": "6.9.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-6.9.0.tgz",
-            "integrity": "sha512-z3WCO0EdVWrXkEn6QXlFQZoKyPxplIctOWTqG8KPLtdHa0gqXhF+gaNxwGg6Ao2ac4sqoFSBcKPhXgE/08jK7g==",
+            "version": "6.13.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-6.13.0.tgz",
+            "integrity": "sha512-+APLd5GqzzJ/atVVs3LGPcCRRy8mVfVQi1QY+cseNAQbRe4LvsDarLbzkblWEwuksxgUGmVGDC3fDNxrwszJ2A==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "engines": {
@@ -5627,9 +6291,9 @@
             }
         },
         "node_modules/@wordpress/browserslist-config": {
-            "version": "6.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.33.0.tgz",
-            "integrity": "sha512-4plw8mLKjcd1beuJzmjT4GNBk+R02qu/og6h/BuGMY8dxfqovfGB0Z2w7C85ILmjY2qnvsU7gelDcSXNgwuwxQ==",
+            "version": "6.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.37.0.tgz",
+            "integrity": "sha512-L//FRak9bo+sDLAignC4QkhITHgeFVlL0C4lWI/AM+AIHKGrHT4LOdwwNpYWMkkztW9rxHRptGkF/JDxuCakxQ==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "engines": {
@@ -5638,9 +6302,9 @@
             }
         },
         "node_modules/@wordpress/dependency-extraction-webpack-plugin": {
-            "version": "6.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.33.0.tgz",
-            "integrity": "sha512-uGvJrak1wpi6XAfIvSXedXgfxvavpzVlj7ypAedAqQ26eFLHCPzK9S2TRp+jw4BglUE3mR2NXD8/glorbGwq+g==",
+            "version": "6.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.37.0.tgz",
+            "integrity": "sha512-N9DTLr07AYV9vo/kFVSKYp/+wg2wBHL4ekDldwx2rGNtOHjoSi1oo+iBmStSaKMRtCQX2Xq8xVFH04+53T0yXA==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
@@ -5662,14 +6326,13 @@
             "license": "BSD"
         },
         "node_modules/@wordpress/e2e-test-utils-playwright": {
-            "version": "1.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.33.0.tgz",
-            "integrity": "sha512-OuxF/5TeHh2k58jsKRG2AtFhoRgAFKUrOjcrBLaNew3Y6RepwvLLgSq1LXqUrR1nhJU90AaH6AqFrJ2s+lmFUw==",
+            "version": "1.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.37.0.tgz",
+            "integrity": "sha512-H77He9+sPNchzbEeFk2YSuYW5JNk3oHnlXJ1EVPUHpIZTr6gM80AGBuxP/W4WtWdLn0A5/TxCmTAxsN1M8YVvQ==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
                 "change-case": "^4.1.2",
-                "form-data": "^4.0.0",
                 "get-port": "^5.1.1",
                 "lighthouse": "^12.2.2",
                 "mime": "^3.0.0",
@@ -5684,19 +6347,20 @@
             }
         },
         "node_modules/@wordpress/eslint-plugin": {
-            "version": "22.19.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.19.0.tgz",
-            "integrity": "sha512-J24RZ6U4Ref0ix8uhmc3XJGkJLdi/V+JOQjjRwB0uLpsSHio4+LhAJrBlovkZCf+0HsRKiJHuIdli0EKW5gl3g==",
+            "version": "23.0.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-23.0.0.tgz",
+            "integrity": "sha512-fdgBWc7jC0JKi8j16lt51F3Sp02n7emSU3af5UFnQ5feXyJSO3mGcVJzZDpehL3ts/Clhu3II+CYOlUeek4H8g==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
                 "@babel/eslint-parser": "7.25.7",
                 "@typescript-eslint/eslint-plugin": "^6.4.1",
                 "@typescript-eslint/parser": "^6.4.1",
-                "@wordpress/babel-preset-default": "^8.33.0",
-                "@wordpress/prettier-config": "^4.33.0",
+                "@wordpress/babel-preset-default": "^8.37.0",
+                "@wordpress/prettier-config": "^4.37.0",
                 "cosmiconfig": "^7.0.0",
                 "eslint-config-prettier": "^8.3.0",
+                "eslint-import-resolver-typescript": "^4.4.4",
                 "eslint-plugin-import": "^2.25.2",
                 "eslint-plugin-jest": "^27.4.3",
                 "eslint-plugin-jsdoc": "^46.4.6",
@@ -5774,9 +6438,9 @@
             }
         },
         "node_modules/@wordpress/jest-console": {
-            "version": "8.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.33.0.tgz",
-            "integrity": "sha512-G9mJYPpGokk+G5MCM2xMQzHqmZY2DNTFDxtJnmH4ISHm4+2S2OTsHovTNuOM+n8QqaaB2En4uuBfYykpRQfNlw==",
+            "version": "8.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.37.0.tgz",
+            "integrity": "sha512-RTNy0qg1sQLRunqsWjCAR8Nw2RZ62DSIaf09J6xzmuYdFjdvsmeQWs0OSs9e6mVQftxqDgJ/V4NQ1ctLxZM06w==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
@@ -5791,13 +6455,13 @@
             }
         },
         "node_modules/@wordpress/jest-preset-default": {
-            "version": "12.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.33.0.tgz",
-            "integrity": "sha512-TI3FHvMyWeC36IBz7lGaADLIHrSow9Yj80jwisWZ1uppWkAh1wwnJuGnMUn6dSydUolCGitLcMBjA/kGx3uPLw==",
+            "version": "12.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.37.0.tgz",
+            "integrity": "sha512-AaT02lS2a/FnD/arYSSc1TkbIlLd1zXY40YqGtpYz1/MqJKoje6th/RZGOMQKC1pyXt5KKBzSFMD4Av39CxThw==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
-                "@wordpress/jest-console": "^8.33.0",
+                "@wordpress/jest-console": "^8.37.0",
                 "babel-jest": "29.7.0"
             },
             "engines": {
@@ -5810,9 +6474,9 @@
             }
         },
         "node_modules/@wordpress/npm-package-json-lint-config": {
-            "version": "5.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.33.0.tgz",
-            "integrity": "sha512-XejRL8yPGoBVY44gvfH2A2STzFDUjzT7inxhsqzZWYgpMtDNjgdrRN6fgA1GP1nyQx0iRg28r/vapjFCWCA+5w==",
+            "version": "5.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.37.0.tgz",
+            "integrity": "sha512-Ond/mR+fw+8JMlcMmj1MBjVgQKD7GVYlL3Ww1n1/yBpRS9OP+UECF4vES3rdEqvL8TU+O/cvoWFpNY2DWO4P8w==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "engines": {
@@ -5824,13 +6488,13 @@
             }
         },
         "node_modules/@wordpress/postcss-plugins-preset": {
-            "version": "5.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.33.0.tgz",
-            "integrity": "sha512-VBmXyBpjq96L58ox5Fmhc2lMKuLZafqkz8im34gQOthjw8PwkHXDCcC/q5ue5SzYXvX07UTZnGGuc7V6ARrHLg==",
+            "version": "5.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.37.0.tgz",
+            "integrity": "sha512-6+/aFPjr3AhmiBQEZs/huG3pkHPUHwwqwS8IIUqwDFuXnX3RkqRJvIoLdfDyR04QaFamyk1IoRv01zU0pZB8YA==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
-                "@wordpress/base-styles": "^6.9.0",
+                "@wordpress/base-styles": "^6.13.0",
                 "autoprefixer": "^10.4.20",
                 "postcss-import": "^16.1.1"
             },
@@ -5843,9 +6507,9 @@
             }
         },
         "node_modules/@wordpress/prettier-config": {
-            "version": "4.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.33.0.tgz",
-            "integrity": "sha512-PRNb10ouWjg52yeWHTXlaZqkuHMSHlKq9Risg368f5fWU7akDJgZboiD6jVdtv+iGXdFRlI5oRF31wqArzNykA==",
+            "version": "4.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.37.0.tgz",
+            "integrity": "sha512-5l+5q07DXpoMhGWxVjJqb60RFyspaD7twCnnVWzqfFNUfKWptytMj3KYV3oO9+4ONWehsEzRF9zl3ai+S6ztvg==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "engines": {
@@ -5857,25 +6521,25 @@
             }
         },
         "node_modules/@wordpress/scripts": {
-            "version": "30.26.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.26.0.tgz",
-            "integrity": "sha512-RpyF41xHtA4ktOP0JBBb6/MkoB7/H/emqQnO3t+dZFs56jCP/8141MicDl7Ne9PY29D4NaB0LgbcmthK5Msk1Q==",
+            "version": "31.2.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-31.2.0.tgz",
+            "integrity": "sha512-0BKF/aWGX2MegnsgxNkoKQCIlZzvzc/w9noC0hCsNCQqgTolP+idtr8sBA3J2E5gP3ieGk4DCr9CbAtLjkP23g==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "dependencies": {
                 "@babel/core": "7.25.7",
                 "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
                 "@svgr/webpack": "^8.0.1",
-                "@wordpress/babel-preset-default": "^8.33.0",
-                "@wordpress/browserslist-config": "^6.33.0",
-                "@wordpress/dependency-extraction-webpack-plugin": "^6.33.0",
-                "@wordpress/e2e-test-utils-playwright": "^1.33.0",
-                "@wordpress/eslint-plugin": "^22.19.0",
-                "@wordpress/jest-preset-default": "^12.33.0",
-                "@wordpress/npm-package-json-lint-config": "^5.33.0",
-                "@wordpress/postcss-plugins-preset": "^5.33.0",
-                "@wordpress/prettier-config": "^4.33.0",
-                "@wordpress/stylelint-config": "^23.25.0",
+                "@wordpress/babel-preset-default": "^8.37.0",
+                "@wordpress/browserslist-config": "^6.37.0",
+                "@wordpress/dependency-extraction-webpack-plugin": "^6.37.0",
+                "@wordpress/e2e-test-utils-playwright": "^1.37.0",
+                "@wordpress/eslint-plugin": "^23.0.0",
+                "@wordpress/jest-preset-default": "^12.37.0",
+                "@wordpress/npm-package-json-lint-config": "^5.37.0",
+                "@wordpress/postcss-plugins-preset": "^5.37.0",
+                "@wordpress/prettier-config": "^4.37.0",
+                "@wordpress/stylelint-config": "^23.29.0",
                 "adm-zip": "^0.5.9",
                 "babel-jest": "29.7.0",
                 "babel-loader": "9.2.1",
@@ -5894,7 +6558,7 @@
                 "filenamify": "^4.2.0",
                 "jest": "^29.6.2",
                 "jest-dev-server": "^10.1.4",
-                "jest-environment-jsdom": "^29.6.2",
+                "jest-environment-jsdom": "^30.2.0",
                 "jest-environment-node": "^29.6.2",
                 "json2php": "^0.0.9",
                 "markdownlint-cli": "^0.31.1",
@@ -5931,7 +6595,7 @@
                 "npm": ">=8.19.2"
             },
             "peerDependencies": {
-                "@playwright/test": "^1.55.0",
+                "@playwright/test": "^1.57.0",
                 "@wordpress/env": "^10.0.0",
                 "react": "^18.0.0",
                 "react-dom": "^18.0.0"
@@ -5943,9 +6607,9 @@
             }
         },
         "node_modules/@wordpress/stylelint-config": {
-            "version": "23.25.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.25.0.tgz",
-            "integrity": "sha512-GefqayI9kSohIwYW6xkK8jZTF62k71ALdMSVgktMXru567gUDpb1Ci79CIY4iTK3fq/OpJW3uAM4AfXYNH8+3Q==",
+            "version": "23.29.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.29.0.tgz",
+            "integrity": "sha512-TTT2oc9Fw2rdnCs4bzI2EHRXgODT3YggjcewqxbBcioPsiAOff9E9gp6NZhe2VlZjVsDcwabUs6m4RIgbBSKaQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -5963,9 +6627,9 @@
             }
         },
         "node_modules/@wordpress/warning": {
-            "version": "3.33.0",
-            "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.33.0.tgz",
-            "integrity": "sha512-LzYgKfxgK5YEpTu4zHPCDzw+kH5hYCrKRK/joK8S9booy5ERvzRCPrISMwrmAKTD9esYF82+IEHhW0/qsjxPsw==",
+            "version": "3.37.0",
+            "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.37.0.tgz",
+            "integrity": "sha512-oXWyKiYJIa9SuPRNEJiOWn2Qk0RzfxOsDqXcus1OL44swCRtSM+ypm16CJpRhZpMUcsJ6d23PBxTC97C/iiJpQ==",
             "dev": true,
             "license": "GPL-2.0-or-later",
             "engines": {
@@ -6033,17 +6697,6 @@
                 "node": ">=0.4.0"
             }
         },
-        "node_modules/acorn-globals": {
-            "version": "7.0.1",
-            "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
-            "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "acorn": "^8.1.0",
-                "acorn-walk": "^8.0.2"
-            }
-        },
         "node_modules/acorn-import-attributes": {
             "version": "1.9.5",
             "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
@@ -6088,16 +6741,13 @@
             }
         },
         "node_modules/agent-base": {
-            "version": "6.0.2",
-            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
-            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "version": "7.1.4",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+            "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
             "dev": true,
             "license": "MIT",
-            "dependencies": {
-                "debug": "4"
-            },
             "engines": {
-                "node": ">= 6.0.0"
+                "node": ">= 14"
             }
         },
         "node_modules/ajv": {
@@ -6547,19 +7197,20 @@
             "license": "MIT"
         },
         "node_modules/atomically": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz",
-            "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==",
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz",
+            "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "stubborn-fs": "^1.2.5",
-                "when-exit": "^2.1.1"
+                "stubborn-fs": "^2.0.0",
+                "when-exit": "^2.1.4"
             }
         },
         "node_modules/autoprefixer": {
-            "version": "10.4.21",
-            "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
-            "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+            "version": "10.4.23",
+            "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+            "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
             "dev": true,
             "funding": [
                 {
@@ -6577,10 +7228,9 @@
             ],
             "license": "MIT",
             "dependencies": {
-                "browserslist": "^4.24.4",
-                "caniuse-lite": "^1.0.30001702",
-                "fraction.js": "^4.3.7",
-                "normalize-range": "^0.1.2",
+                "browserslist": "^4.28.1",
+                "caniuse-lite": "^1.0.30001760",
+                "fraction.js": "^5.3.4",
                 "picocolors": "^1.1.1",
                 "postcss-value-parser": "^4.2.0"
             },
@@ -6621,14 +7271,14 @@
             }
         },
         "node_modules/axios": {
-            "version": "1.7.9",
-            "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
-            "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+            "version": "1.13.2",
+            "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+            "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "follow-redirects": "^1.15.6",
-                "form-data": "^4.0.0",
+                "form-data": "^4.0.4",
                 "proxy-from-env": "^1.1.0"
             }
         },
@@ -6926,6 +7576,16 @@
             ],
             "license": "MIT"
         },
+        "node_modules/baseline-browser-mapping": {
+            "version": "2.9.11",
+            "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
+            "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "bin": {
+                "baseline-browser-mapping": "dist/cli.js"
+            }
+        },
         "node_modules/basic-ftp": {
             "version": "5.0.5",
             "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
@@ -7063,9 +7723,9 @@
             }
         },
         "node_modules/browserslist": {
-            "version": "4.24.4",
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
-            "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+            "version": "4.28.1",
+            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+            "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
             "dev": true,
             "funding": [
                 {
@@ -7084,10 +7744,11 @@
             "license": "MIT",
             "peer": true,
             "dependencies": {
-                "caniuse-lite": "^1.0.30001688",
-                "electron-to-chromium": "^1.5.73",
-                "node-releases": "^2.0.19",
-                "update-browserslist-db": "^1.1.1"
+                "baseline-browser-mapping": "^2.9.0",
+                "caniuse-lite": "^1.0.30001759",
+                "electron-to-chromium": "^1.5.263",
+                "node-releases": "^2.0.27",
+                "update-browserslist-db": "^1.2.0"
             },
             "bin": {
                 "browserslist": "cli.js"
@@ -7172,24 +7833,23 @@
             }
         },
         "node_modules/cacheable": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.1.1.tgz",
-            "integrity": "sha512-LmF4AXiSNdiRbI2UjH8pAp9NIXxeQsTotpEaegPiDcnN0YPygDJDV3l/Urc0mL72JWdATEorKqIHEx55nDlONg==",
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.1.tgz",
+            "integrity": "sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@cacheable/memoize": "^2.0.3",
-                "@cacheable/memory": "^2.0.3",
-                "@cacheable/utils": "^2.1.0",
-                "hookified": "^1.12.2",
-                "keyv": "^5.5.3",
-                "qified": "^0.5.0"
+                "@cacheable/memory": "^2.0.6",
+                "@cacheable/utils": "^2.3.2",
+                "hookified": "^1.14.0",
+                "keyv": "^5.5.5",
+                "qified": "^0.5.3"
             }
         },
         "node_modules/cacheable/node_modules/keyv": {
-            "version": "5.5.3",
-            "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
-            "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+            "version": "5.5.5",
+            "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
+            "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -7322,9 +7982,9 @@
             }
         },
         "node_modules/caniuse-lite": {
-            "version": "1.0.30001731",
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
-            "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
+            "version": "1.0.30001761",
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
+            "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
             "dev": true,
             "funding": [
                 {
@@ -7670,9 +8330,9 @@
             }
         },
         "node_modules/compression": {
-            "version": "1.7.5",
-            "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
-            "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
+            "version": "1.8.1",
+            "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+            "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -7680,7 +8340,7 @@
                 "compressible": "~2.0.18",
                 "debug": "2.6.9",
                 "negotiator": "~0.6.4",
-                "on-headers": "~1.0.2",
+                "on-headers": "~1.1.0",
                 "safe-buffer": "5.2.1",
                 "vary": "~1.1.2"
             },
@@ -7873,9 +8533,9 @@
             }
         },
         "node_modules/core-js": {
-            "version": "3.46.0",
-            "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
-            "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
+            "version": "3.47.0",
+            "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
+            "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
             "dev": true,
             "hasInstallScript": true,
             "license": "MIT",
@@ -7952,9 +8612,9 @@
             "license": "Python-2.0"
         },
         "node_modules/cosmiconfig/node_modules/js-yaml": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+            "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -8252,33 +8912,20 @@
             "dev": true,
             "license": "CC0-1.0"
         },
-        "node_modules/cssom": {
-            "version": "0.5.0",
-            "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
-            "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
-            "dev": true,
-            "license": "MIT"
-        },
         "node_modules/cssstyle": {
-            "version": "2.3.0",
-            "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
-            "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+            "version": "4.6.0",
+            "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+            "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "cssom": "~0.3.6"
+                "@asamuzakjp/css-color": "^3.2.0",
+                "rrweb-cssom": "^0.8.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">=18"
             }
         },
-        "node_modules/cssstyle/node_modules/cssom": {
-            "version": "0.3.8",
-            "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
-            "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
-            "dev": true,
-            "license": "MIT"
-        },
         "node_modules/cwd": {
             "version": "0.10.0",
             "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz",
@@ -8311,18 +8958,17 @@
             }
         },
         "node_modules/data-urls": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
-            "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+            "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "abab": "^2.0.6",
-                "whatwg-mimetype": "^3.0.0",
-                "whatwg-url": "^11.0.0"
+                "whatwg-mimetype": "^4.0.0",
+                "whatwg-url": "^14.0.0"
             },
             "engines": {
-                "node": ">=12"
+                "node": ">=18"
             }
         },
         "node_modules/data-view-buffer": {
@@ -8449,9 +9095,9 @@
             "license": "MIT"
         },
         "node_modules/dedent": {
-            "version": "1.7.0",
-            "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
-            "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==",
+            "version": "1.7.1",
+            "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
+            "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==",
             "dev": true,
             "license": "MIT",
             "peerDependencies": {
@@ -8711,20 +9357,6 @@
             ],
             "license": "BSD-2-Clause"
         },
-        "node_modules/domexception": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
-            "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
-            "deprecated": "Use your platform's native DOMException instead",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "webidl-conversions": "^7.0.0"
-            },
-            "engines": {
-                "node": ">=12"
-            }
-        },
         "node_modules/domhandler": {
             "version": "5.0.3",
             "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
@@ -8826,9 +9458,9 @@
             "license": "MIT"
         },
         "node_modules/electron-to-chromium": {
-            "version": "1.5.91",
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.91.tgz",
-            "integrity": "sha512-sNSHHyq048PFmZY4S90ax61q+gLCs0X0YmcOII9wG9S2XwbVr+h4VW2wWhnbp/Eys3cCwTxVF292W3qPaxIapQ==",
+            "version": "1.5.267",
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+            "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
             "dev": true,
             "license": "ISC"
         },
@@ -8967,9 +9599,9 @@
             }
         },
         "node_modules/es-abstract": {
-            "version": "1.24.0",
-            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
-            "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
+            "version": "1.24.1",
+            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
+            "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -9056,27 +9688,27 @@
             }
         },
         "node_modules/es-iterator-helpers": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
-            "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
+            "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "call-bind": "^1.0.8",
-                "call-bound": "^1.0.3",
+                "call-bound": "^1.0.4",
                 "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.6",
+                "es-abstract": "^1.24.1",
                 "es-errors": "^1.3.0",
-                "es-set-tostringtag": "^2.0.3",
+                "es-set-tostringtag": "^2.1.0",
                 "function-bind": "^1.1.2",
-                "get-intrinsic": "^1.2.6",
+                "get-intrinsic": "^1.3.0",
                 "globalthis": "^1.0.4",
                 "gopd": "^1.2.0",
                 "has-property-descriptors": "^1.0.2",
                 "has-proto": "^1.2.0",
                 "has-symbols": "^1.1.0",
                 "internal-slot": "^1.1.0",
-                "iterator.prototype": "^1.1.4",
+                "iterator.prototype": "^1.1.5",
                 "safe-array-concat": "^1.1.3"
             },
             "engines": {
@@ -9285,6 +9917,31 @@
                 "eslint": ">=7.0.0"
             }
         },
+        "node_modules/eslint-import-context": {
+            "version": "0.1.9",
+            "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz",
+            "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "get-tsconfig": "^4.10.1",
+                "stable-hash-x": "^0.2.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint-import-context"
+            },
+            "peerDependencies": {
+                "unrs-resolver": "^1.0.0"
+            },
+            "peerDependenciesMeta": {
+                "unrs-resolver": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/eslint-import-resolver-node": {
             "version": "0.3.9",
             "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@@ -9307,6 +9964,41 @@
                 "ms": "^2.1.1"
             }
         },
+        "node_modules/eslint-import-resolver-typescript": {
+            "version": "4.4.4",
+            "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz",
+            "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "debug": "^4.4.1",
+                "eslint-import-context": "^0.1.8",
+                "get-tsconfig": "^4.10.1",
+                "is-bun-module": "^2.0.0",
+                "stable-hash-x": "^0.2.0",
+                "tinyglobby": "^0.2.14",
+                "unrs-resolver": "^1.7.11"
+            },
+            "engines": {
+                "node": "^16.17.0 || >=18.6.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint-import-resolver-typescript"
+            },
+            "peerDependencies": {
+                "eslint": "*",
+                "eslint-plugin-import": "*",
+                "eslint-plugin-import-x": "*"
+            },
+            "peerDependenciesMeta": {
+                "eslint-plugin-import": {
+                    "optional": true
+                },
+                "eslint-plugin-import-x": {
+                    "optional": true
+                }
+            }
+        },
         "node_modules/eslint-module-utils": {
             "version": "2.12.1",
             "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
@@ -9341,6 +10033,7 @@
             "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@rtsao/scc": "^1.1.0",
                 "array-includes": "^3.1.9",
@@ -9928,9 +10621,9 @@
             }
         },
         "node_modules/eslint/node_modules/js-yaml": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+            "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -10695,14 +11388,16 @@
             }
         },
         "node_modules/form-data": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
-            "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+            "version": "4.0.5",
+            "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+            "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "asynckit": "^0.4.0",
                 "combined-stream": "^1.0.8",
+                "es-set-tostringtag": "^2.1.0",
+                "hasown": "^2.0.2",
                 "mime-types": "^2.1.12"
             },
             "engines": {
@@ -10727,16 +11422,16 @@
             "license": "MIT"
         },
         "node_modules/fraction.js": {
-            "version": "4.3.7",
-            "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
-            "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+            "version": "5.3.4",
+            "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+            "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
             "dev": true,
             "license": "MIT",
             "engines": {
                 "node": "*"
             },
             "funding": {
-                "type": "patreon",
+                "type": "github",
                 "url": "https://github.com/sponsors/rawify"
             }
         },
@@ -10966,6 +11661,19 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/get-tsconfig": {
+            "version": "4.13.0",
+            "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
+            "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "resolve-pkg-maps": "^1.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+            }
+        },
         "node_modules/get-uri": {
             "version": "6.0.5",
             "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
@@ -11024,9 +11732,9 @@
             "license": "BSD-2-Clause"
         },
         "node_modules/glob/node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "version": "1.1.12",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+            "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -11286,6 +11994,19 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/hashery": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.3.0.tgz",
+            "integrity": "sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "hookified": "^1.13.0"
+            },
+            "engines": {
+                "node": ">=20"
+            }
+        },
         "node_modules/hasown": {
             "version": "2.0.2",
             "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -11324,9 +12045,9 @@
             }
         },
         "node_modules/hookified": {
-            "version": "1.12.2",
-            "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.2.tgz",
-            "integrity": "sha512-aokUX1VdTpI0DUsndvW+OiwmBpKCu/NgRsSSkuSY0zq8PY6Q6a+lmOfAFDXAAOtBqJELvcWY9L1EVtzjbQcMdg==",
+            "version": "1.14.0",
+            "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.14.0.tgz",
+            "integrity": "sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==",
             "dev": true,
             "license": "MIT"
         },
@@ -11417,16 +12138,16 @@
             }
         },
         "node_modules/html-encoding-sniffer": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
-            "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+            "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "whatwg-encoding": "^2.0.0"
+                "whatwg-encoding": "^3.1.1"
             },
             "engines": {
-                "node": ">=12"
+                "node": ">=18"
             }
         },
         "node_modules/html-entities": {
@@ -11523,24 +12244,23 @@
             }
         },
         "node_modules/http-proxy-agent": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
-            "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+            "version": "7.0.2",
+            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+            "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@tootallnate/once": "2",
-                "agent-base": "6",
-                "debug": "4"
+                "agent-base": "^7.1.0",
+                "debug": "^4.3.4"
             },
             "engines": {
-                "node": ">= 6"
+                "node": ">= 14"
             }
         },
         "node_modules/http-proxy-middleware": {
-            "version": "2.0.7",
-            "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
-            "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
+            "version": "2.0.9",
+            "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
+            "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -11563,17 +12283,17 @@
             }
         },
         "node_modules/https-proxy-agent": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
-            "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+            "version": "7.0.6",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+            "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "agent-base": "6",
+                "agent-base": "^7.1.2",
                 "debug": "4"
             },
             "engines": {
-                "node": ">= 6"
+                "node": ">= 14"
             }
         },
         "node_modules/human-signals": {
@@ -11657,9 +12377,9 @@
             }
         },
         "node_modules/ignore-walk/node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "version": "1.1.12",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+            "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -11995,6 +12715,29 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/is-bun-module": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
+            "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "semver": "^7.7.1"
+            }
+        },
+        "node_modules/is-bun-module/node_modules/semver": {
+            "version": "7.7.3",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+            "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+            "dev": true,
+            "license": "ISC",
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/is-callable": {
             "version": "1.2.7",
             "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -12795,26 +13538,23 @@
             }
         },
         "node_modules/jest-environment-jsdom": {
-            "version": "29.7.0",
-            "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
-            "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz",
+            "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "@jest/environment": "^29.7.0",
-                "@jest/fake-timers": "^29.7.0",
-                "@jest/types": "^29.6.3",
-                "@types/jsdom": "^20.0.0",
+                "@jest/environment": "30.2.0",
+                "@jest/environment-jsdom-abstract": "30.2.0",
+                "@types/jsdom": "^21.1.7",
                 "@types/node": "*",
-                "jest-mock": "^29.7.0",
-                "jest-util": "^29.7.0",
-                "jsdom": "^20.0.0"
+                "jsdom": "^26.1.0"
             },
             "engines": {
-                "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
             },
             "peerDependencies": {
-                "canvas": "^2.5.0"
+                "canvas": "^3.0.0"
             },
             "peerDependenciesMeta": {
                 "canvas": {
@@ -12822,6 +13562,200 @@
                 }
             }
         },
+        "node_modules/jest-environment-jsdom/node_modules/@jest/environment": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
+            "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/fake-timers": "30.2.0",
+                "@jest/types": "30.2.0",
+                "@types/node": "*",
+                "jest-mock": "30.2.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
+            "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/types": "30.2.0",
+                "@sinonjs/fake-timers": "^13.0.0",
+                "@types/node": "*",
+                "jest-message-util": "30.2.0",
+                "jest-mock": "30.2.0",
+                "jest-util": "30.2.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": {
+            "version": "30.0.5",
+            "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+            "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@sinclair/typebox": "^0.34.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/@jest/types": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
+            "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/pattern": "30.0.1",
+                "@jest/schemas": "30.0.5",
+                "@types/istanbul-lib-coverage": "^2.0.6",
+                "@types/istanbul-reports": "^3.0.4",
+                "@types/node": "*",
+                "@types/yargs": "^17.0.33",
+                "chalk": "^4.1.2"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": {
+            "version": "0.34.41",
+            "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
+            "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": {
+            "version": "13.0.5",
+            "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+            "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+            "dev": true,
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "@sinonjs/commons": "^3.0.1"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/ansi-styles": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+            "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/ci-info": {
+            "version": "4.3.1",
+            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
+            "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/sibiraj-s"
+                }
+            ],
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/jest-message-util": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
+            "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@babel/code-frame": "^7.27.1",
+                "@jest/types": "30.2.0",
+                "@types/stack-utils": "^2.0.3",
+                "chalk": "^4.1.2",
+                "graceful-fs": "^4.2.11",
+                "micromatch": "^4.0.8",
+                "pretty-format": "30.2.0",
+                "slash": "^3.0.0",
+                "stack-utils": "^2.0.6"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/jest-mock": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
+            "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/types": "30.2.0",
+                "@types/node": "*",
+                "jest-util": "30.2.0"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/jest-util": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
+            "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/types": "30.2.0",
+                "@types/node": "*",
+                "chalk": "^4.1.2",
+                "ci-info": "^4.2.0",
+                "graceful-fs": "^4.2.11",
+                "picomatch": "^4.0.2"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/picomatch": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+            "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/jest-environment-jsdom/node_modules/pretty-format": {
+            "version": "30.2.0",
+            "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
+            "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@jest/schemas": "30.0.5",
+                "ansi-styles": "^5.2.0",
+                "react-is": "^18.3.1"
+            },
+            "engines": {
+                "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+            }
+        },
         "node_modules/jest-environment-node": {
             "version": "29.7.0",
             "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
@@ -13244,9 +14178,9 @@
             "license": "MIT"
         },
         "node_modules/js-yaml": {
-            "version": "3.14.1",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
-            "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+            "version": "3.14.2",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+            "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -13268,44 +14202,39 @@
             }
         },
         "node_modules/jsdom": {
-            "version": "20.0.3",
-            "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
-            "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+            "version": "26.1.0",
+            "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+            "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
-                "abab": "^2.0.6",
-                "acorn": "^8.8.1",
-                "acorn-globals": "^7.0.0",
-                "cssom": "^0.5.0",
-                "cssstyle": "^2.3.0",
-                "data-urls": "^3.0.2",
-                "decimal.js": "^10.4.2",
-                "domexception": "^4.0.0",
-                "escodegen": "^2.0.0",
-                "form-data": "^4.0.0",
-                "html-encoding-sniffer": "^3.0.0",
-                "http-proxy-agent": "^5.0.0",
-                "https-proxy-agent": "^5.0.1",
+                "cssstyle": "^4.2.1",
+                "data-urls": "^5.0.0",
+                "decimal.js": "^10.5.0",
+                "html-encoding-sniffer": "^4.0.0",
+                "http-proxy-agent": "^7.0.2",
+                "https-proxy-agent": "^7.0.6",
                 "is-potential-custom-element-name": "^1.0.1",
-                "nwsapi": "^2.2.2",
-                "parse5": "^7.1.1",
+                "nwsapi": "^2.2.16",
+                "parse5": "^7.2.1",
+                "rrweb-cssom": "^0.8.0",
                 "saxes": "^6.0.0",
                 "symbol-tree": "^3.2.4",
-                "tough-cookie": "^4.1.2",
-                "w3c-xmlserializer": "^4.0.0",
+                "tough-cookie": "^5.1.1",
+                "w3c-xmlserializer": "^5.0.0",
                 "webidl-conversions": "^7.0.0",
-                "whatwg-encoding": "^2.0.0",
-                "whatwg-mimetype": "^3.0.0",
-                "whatwg-url": "^11.0.0",
-                "ws": "^8.11.0",
-                "xml-name-validator": "^4.0.0"
+                "whatwg-encoding": "^3.1.1",
+                "whatwg-mimetype": "^4.0.0",
+                "whatwg-url": "^14.1.1",
+                "ws": "^8.18.0",
+                "xml-name-validator": "^5.0.0"
             },
             "engines": {
-                "node": ">=14"
+                "node": ">=18"
             },
             "peerDependencies": {
-                "canvas": "^2.5.0"
+                "canvas": "^3.0.0"
             },
             "peerDependenciesMeta": {
                 "canvas": {
@@ -13582,9 +14511,9 @@
             "license": "Apache-2.0"
         },
         "node_modules/lighthouse/node_modules/@puppeteer/browsers": {
-            "version": "2.10.12",
-            "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.12.tgz",
-            "integrity": "sha512-mP9iLFZwH+FapKJLeA7/fLqOlSUwYpMwjR1P5J23qd4e7qGJwecJccJqHYrjw33jmIZYV4dtiTHPD/J+1e7cEw==",
+            "version": "2.11.0",
+            "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.0.tgz",
+            "integrity": "sha512-n6oQX6mYkG8TRPuPXmbPidkUbsSRalhmaaVAQxvH1IkQy63cwsH+kOjB3e4cpCDHg0aSvsiX9bQ4s2VB6mGWUQ==",
             "dev": true,
             "license": "Apache-2.0",
             "dependencies": {
@@ -13604,18 +14533,18 @@
             }
         },
         "node_modules/lighthouse/node_modules/puppeteer-core": {
-            "version": "24.25.0",
-            "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.25.0.tgz",
-            "integrity": "sha512-8Xs6q3Ut+C8y7sAaqjIhzv1QykGWG4gc2mEZ2mYE7siZFuRp4xQVehOf8uQKSQAkeL7jXUs3mknEeiqnRqUKvQ==",
+            "version": "24.34.0",
+            "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.34.0.tgz",
+            "integrity": "sha512-24evawO+mUGW4mvS2a2ivwLdX3gk8zRLZr9HP+7+VT2vBQnm0oh9jJEZmUE3ePJhRkYlZ93i7OMpdcoi2qNCLg==",
             "dev": true,
             "license": "Apache-2.0",
             "dependencies": {
-                "@puppeteer/browsers": "2.10.12",
-                "chromium-bidi": "9.1.0",
+                "@puppeteer/browsers": "2.11.0",
+                "chromium-bidi": "12.0.1",
                 "debug": "^4.4.3",
-                "devtools-protocol": "0.0.1508733",
+                "devtools-protocol": "0.0.1534754",
                 "typed-query-selector": "^2.12.0",
-                "webdriver-bidi-protocol": "0.3.7",
+                "webdriver-bidi-protocol": "0.3.10",
                 "ws": "^8.18.3"
             },
             "engines": {
@@ -13623,9 +14552,9 @@
             }
         },
         "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/chromium-bidi": {
-            "version": "9.1.0",
-            "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz",
-            "integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==",
+            "version": "12.0.1",
+            "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-12.0.1.tgz",
+            "integrity": "sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg==",
             "dev": true,
             "license": "Apache-2.0",
             "dependencies": {
@@ -13637,9 +14566,9 @@
             }
         },
         "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/devtools-protocol": {
-            "version": "0.0.1508733",
-            "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
-            "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
+            "version": "0.0.1534754",
+            "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz",
+            "integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==",
             "dev": true,
             "license": "BSD-3-Clause",
             "peer": true
@@ -13787,9 +14716,9 @@
             "license": "MIT"
         },
         "node_modules/lodash-es": {
-            "version": "4.17.21",
-            "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
-            "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+            "version": "4.17.22",
+            "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
+            "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
             "dev": true,
             "license": "MIT"
         },
@@ -14038,9 +14967,9 @@
             "license": "Python-2.0"
         },
         "node_modules/markdownlint-cli/node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "version": "1.1.12",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+            "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -14069,9 +14998,9 @@
             }
         },
         "node_modules/markdownlint-cli/node_modules/js-yaml": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+            "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -14522,6 +15451,22 @@
                 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
             }
         },
+        "node_modules/napi-postinstall": {
+            "version": "0.3.4",
+            "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
+            "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
+            "dev": true,
+            "license": "MIT",
+            "bin": {
+                "napi-postinstall": "lib/cli.js"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/napi-postinstall"
+            }
+        },
         "node_modules/natural-compare": {
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -14576,9 +15521,9 @@
             "optional": true
         },
         "node_modules/node-forge": {
-            "version": "1.3.1",
-            "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
-            "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+            "version": "1.3.3",
+            "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
+            "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
             "dev": true,
             "license": "(BSD-3-Clause OR GPL-2.0)",
             "engines": {
@@ -14593,9 +15538,9 @@
             "license": "MIT"
         },
         "node_modules/node-releases": {
-            "version": "2.0.19",
-            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
-            "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+            "version": "2.0.27",
+            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+            "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
             "dev": true,
             "license": "MIT"
         },
@@ -14638,16 +15583,6 @@
                 "node": ">=0.10.0"
             }
         },
-        "node_modules/normalize-range": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
-            "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
         "node_modules/npm-bundled": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
@@ -14778,9 +15713,9 @@
             }
         },
         "node_modules/nwsapi": {
-            "version": "2.2.16",
-            "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz",
-            "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==",
+            "version": "2.2.23",
+            "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+            "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
             "dev": true,
             "license": "MIT"
         },
@@ -14935,9 +15870,9 @@
             }
         },
         "node_modules/on-headers": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
-            "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+            "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
             "dev": true,
             "license": "MIT",
             "engines": {
@@ -15133,44 +16068,6 @@
                 "node": ">= 14"
             }
         },
-        "node_modules/pac-proxy-agent/node_modules/agent-base": {
-            "version": "7.1.4",
-            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
-            "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 14"
-            }
-        },
-        "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": {
-            "version": "7.0.2",
-            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
-            "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "agent-base": "^7.1.0",
-                "debug": "^4.3.4"
-            },
-            "engines": {
-                "node": ">= 14"
-            }
-        },
-        "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": {
-            "version": "7.0.6",
-            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
-            "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "agent-base": "^7.1.2",
-                "debug": "4"
-            },
-            "engines": {
-                "node": ">= 14"
-            }
-        },
         "node_modules/pac-resolver": {
             "version": "7.0.1",
             "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
@@ -15245,18 +16142,31 @@
             }
         },
         "node_modules/parse5": {
-            "version": "7.2.1",
-            "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
-            "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+            "version": "7.3.0",
+            "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+            "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "entities": "^4.5.0"
+                "entities": "^6.0.0"
             },
             "funding": {
                 "url": "https://github.com/inikulin/parse5?sponsor=1"
             }
         },
+        "node_modules/parse5/node_modules/entities": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+            "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+            "dev": true,
+            "license": "BSD-2-Clause",
+            "engines": {
+                "node": ">=0.12"
+            },
+            "funding": {
+                "url": "https://github.com/fb55/entities?sponsor=1"
+            }
+        },
         "node_modules/parseurl": {
             "version": "1.3.3",
             "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -15529,13 +16439,13 @@
             }
         },
         "node_modules/playwright": {
-            "version": "1.56.1",
-            "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
-            "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
+            "version": "1.57.0",
+            "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
+            "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
             "dev": true,
             "license": "Apache-2.0",
             "dependencies": {
-                "playwright-core": "1.56.1"
+                "playwright-core": "1.57.0"
             },
             "bin": {
                 "playwright": "cli.js"
@@ -15548,9 +16458,9 @@
             }
         },
         "node_modules/playwright-core": {
-            "version": "1.56.1",
-            "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
-            "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
+            "version": "1.57.0",
+            "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
+            "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
             "dev": true,
             "license": "Apache-2.0",
             "bin": {
@@ -16327,9 +17237,9 @@
             }
         },
         "node_modules/postgres-bytea": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
-            "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+            "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
             "dev": true,
             "license": "MIT",
             "engines": {
@@ -16522,44 +17432,6 @@
                 "node": ">= 14"
             }
         },
-        "node_modules/proxy-agent/node_modules/agent-base": {
-            "version": "7.1.4",
-            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
-            "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 14"
-            }
-        },
-        "node_modules/proxy-agent/node_modules/http-proxy-agent": {
-            "version": "7.0.2",
-            "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
-            "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "agent-base": "^7.1.0",
-                "debug": "^4.3.4"
-            },
-            "engines": {
-                "node": ">= 14"
-            }
-        },
-        "node_modules/proxy-agent/node_modules/https-proxy-agent": {
-            "version": "7.0.6",
-            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
-            "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "agent-base": "^7.1.2",
-                "debug": "4"
-            },
-            "engines": {
-                "node": ">= 14"
-            }
-        },
         "node_modules/proxy-agent/node_modules/lru-cache": {
             "version": "7.18.3",
             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -16577,19 +17449,6 @@
             "dev": true,
             "license": "MIT"
         },
-        "node_modules/psl": {
-            "version": "1.15.0",
-            "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
-            "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "punycode": "^2.3.1"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/lupomontero"
-            }
-        },
         "node_modules/pump": {
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -16654,13 +17513,13 @@
             "license": "MIT"
         },
         "node_modules/qified": {
-            "version": "0.5.1",
-            "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.1.tgz",
-            "integrity": "sha512-+BtFN3dCP+IaFA6IYNOu/f/uK1B8xD2QWyOeCse0rjtAebBmkzgd2d1OAXi3ikAzJMIBSdzZDNZ3wZKEUDQs5w==",
+            "version": "0.5.3",
+            "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.3.tgz",
+            "integrity": "sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "hookified": "^1.12.2"
+                "hookified": "^1.13.0"
             },
             "engines": {
                 "node": ">=20"
@@ -16682,13 +17541,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/querystringify": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
-            "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
-            "dev": true,
-            "license": "MIT"
-        },
         "node_modules/queue-microtask": {
             "version": "1.2.3",
             "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -17009,13 +17861,6 @@
                 "node": ">=4"
             }
         },
-        "node_modules/regenerator-runtime": {
-            "version": "0.14.1",
-            "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
-            "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
-            "dev": true,
-            "license": "MIT"
-        },
         "node_modules/regenerator-transform": {
             "version": "0.15.2",
             "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
@@ -17218,6 +18063,16 @@
                 "node": ">=8"
             }
         },
+        "node_modules/resolve-pkg-maps": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+            "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+            "dev": true,
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+            }
+        },
         "node_modules/resolve.exports": {
             "version": "2.0.3",
             "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
@@ -17276,6 +18131,13 @@
                 "node": ">=10.0.0"
             }
         },
+        "node_modules/rrweb-cssom": {
+            "version": "0.8.0",
+            "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+            "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/rtlcss": {
             "version": "4.3.0",
             "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
@@ -18137,16 +18999,6 @@
                 "node": ">= 14"
             }
         },
-        "node_modules/socks-proxy-agent/node_modules/agent-base": {
-            "version": "7.1.4",
-            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
-            "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 14"
-            }
-        },
         "node_modules/source-map": {
             "version": "0.7.4",
             "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@@ -18338,6 +19190,16 @@
             "dev": true,
             "license": "BSD-3-Clause"
         },
+        "node_modules/stable-hash-x": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz",
+            "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
         "node_modules/stack-utils": {
             "version": "2.0.6",
             "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -18646,10 +19508,21 @@
             }
         },
         "node_modules/stubborn-fs": {
-            "version": "1.2.5",
-            "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz",
-            "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==",
-            "dev": true
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz",
+            "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "stubborn-utils": "^1.0.1"
+            }
+        },
+        "node_modules/stubborn-utils": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz",
+            "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==",
+            "dev": true,
+            "license": "MIT"
         },
         "node_modules/style-search": {
             "version": "0.1.0",
@@ -18676,9 +19549,9 @@
             }
         },
         "node_modules/stylelint": {
-            "version": "16.25.0",
-            "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz",
-            "integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==",
+            "version": "16.26.1",
+            "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.1.tgz",
+            "integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==",
             "dev": true,
             "funding": [
                 {
@@ -18694,6 +19567,7 @@
             "peer": true,
             "dependencies": {
                 "@csstools/css-parser-algorithms": "^3.0.5",
+                "@csstools/css-syntax-patches-for-csstree": "^1.0.19",
                 "@csstools/css-tokenizer": "^3.0.4",
                 "@csstools/media-query-list-parser": "^4.0.3",
                 "@csstools/selector-specificity": "^5.0.0",
@@ -18706,7 +19580,7 @@
                 "debug": "^4.4.3",
                 "fast-glob": "^3.3.3",
                 "fastest-levenshtein": "^1.0.16",
-                "file-entry-cache": "^10.1.4",
+                "file-entry-cache": "^11.1.1",
                 "global-modules": "^2.0.0",
                 "globby": "^11.1.0",
                 "globjoin": "^0.1.4",
@@ -18787,46 +19661,39 @@
             }
         },
         "node_modules/stylelint-scss": {
-            "version": "6.12.1",
-            "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.12.1.tgz",
-            "integrity": "sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==",
+            "version": "6.13.0",
+            "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.13.0.tgz",
+            "integrity": "sha512-kZPwFUJkfup2gP1enlrS2h9U5+T5wFoqzJ1n/56AlpwSj28kmFe7ww/QFydvPsg5gLjWchAwWWBLtterynZrOw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "css-tree": "^3.0.1",
                 "is-plain-object": "^5.0.0",
-                "known-css-properties": "^0.36.0",
-                "mdn-data": "^2.21.0",
+                "known-css-properties": "^0.37.0",
+                "mdn-data": "^2.25.0",
                 "postcss-media-query-parser": "^0.2.3",
                 "postcss-resolve-nested-selector": "^0.1.6",
-                "postcss-selector-parser": "^7.1.0",
+                "postcss-selector-parser": "^7.1.1",
                 "postcss-value-parser": "^4.2.0"
             },
             "engines": {
                 "node": ">=18.12.0"
             },
             "peerDependencies": {
-                "stylelint": "^16.0.2"
+                "stylelint": "^16.8.2"
             }
         },
-        "node_modules/stylelint-scss/node_modules/known-css-properties": {
-            "version": "0.36.0",
-            "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz",
-            "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==",
-            "dev": true,
-            "license": "MIT"
-        },
         "node_modules/stylelint-scss/node_modules/mdn-data": {
-            "version": "2.24.0",
-            "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.24.0.tgz",
-            "integrity": "sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==",
+            "version": "2.25.0",
+            "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.25.0.tgz",
+            "integrity": "sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==",
             "dev": true,
             "license": "CC0-1.0"
         },
         "node_modules/stylelint-scss/node_modules/postcss-selector-parser": {
-            "version": "7.1.0",
-            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
-            "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+            "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -18926,25 +19793,25 @@
             }
         },
         "node_modules/stylelint/node_modules/file-entry-cache": {
-            "version": "10.1.4",
-            "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz",
-            "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==",
+            "version": "11.1.1",
+            "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.1.tgz",
+            "integrity": "sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "flat-cache": "^6.1.13"
+                "flat-cache": "^6.1.19"
             }
         },
         "node_modules/stylelint/node_modules/flat-cache": {
-            "version": "6.1.18",
-            "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.18.tgz",
-            "integrity": "sha512-JUPnFgHMuAVmLmoH9/zoZ6RHOt5n9NlUw/sDXsTbROJ2SFoS2DS4s+swAV6UTeTbGH/CAsZIE6M8TaG/3jVxgQ==",
+            "version": "6.1.19",
+            "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.19.tgz",
+            "integrity": "sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "cacheable": "^2.1.0",
+                "cacheable": "^2.2.0",
                 "flatted": "^3.3.3",
-                "hookified": "^1.12.0"
+                "hookified": "^1.13.0"
             }
         },
         "node_modules/stylelint/node_modules/global-modules": {
@@ -18986,9 +19853,9 @@
             }
         },
         "node_modules/stylelint/node_modules/js-yaml": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+            "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
@@ -19022,9 +19889,9 @@
             }
         },
         "node_modules/stylelint/node_modules/postcss-selector-parser": {
-            "version": "7.1.0",
-            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
-            "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+            "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
             "dev": true,
             "license": "MIT",
             "peer": true,
@@ -19510,23 +20377,92 @@
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/tinyglobby": {
+            "version": "0.2.15",
+            "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+            "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "fdir": "^6.5.0",
+                "picomatch": "^4.0.3"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/SuperchupuDev"
+            }
+        },
+        "node_modules/tinyglobby/node_modules/fdir": {
+            "version": "6.5.0",
+            "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+            "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=12.0.0"
+            },
+            "peerDependencies": {
+                "picomatch": "^3 || ^4"
+            },
+            "peerDependenciesMeta": {
+                "picomatch": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/tinyglobby/node_modules/picomatch": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+            "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+            "dev": true,
+            "license": "MIT",
+            "peer": true,
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/tldts": {
+            "version": "6.1.86",
+            "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+            "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "tldts-core": "^6.1.86"
+            },
+            "bin": {
+                "tldts": "bin/cli.js"
+            }
+        },
         "node_modules/tldts-core": {
-            "version": "7.0.17",
-            "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz",
-            "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==",
+            "version": "7.0.19",
+            "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
+            "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
             "dev": true,
             "license": "MIT"
         },
         "node_modules/tldts-icann": {
-            "version": "7.0.17",
-            "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.0.17.tgz",
-            "integrity": "sha512-up4oFDoumyz2RscRxoYRxf+2OvIKUHjh7rUvuGWI0PZ/47k35sadoi2JyKR0AIfTw09qcfix8bUxXFQhY1QZIQ==",
+            "version": "7.0.19",
+            "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.0.19.tgz",
+            "integrity": "sha512-PZgda8E2cXMNa7QlBbiZh3vcS8UaPTDRIBmcGPDlujSMtQLrzjvikeJxzQSqWxn3muaMJ7BsC+aL464Yl2I6cA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "tldts-core": "^7.0.17"
+                "tldts-core": "^7.0.19"
             }
         },
+        "node_modules/tldts/node_modules/tldts-core": {
+            "version": "6.1.86",
+            "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+            "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/tmpl": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -19568,32 +20504,29 @@
             }
         },
         "node_modules/tough-cookie": {
-            "version": "4.1.4",
-            "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
-            "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+            "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
             "dev": true,
             "license": "BSD-3-Clause",
             "dependencies": {
-                "psl": "^1.1.33",
-                "punycode": "^2.1.1",
-                "universalify": "^0.2.0",
-                "url-parse": "^1.5.3"
+                "tldts": "^6.1.32"
             },
             "engines": {
-                "node": ">=6"
+                "node": ">=16"
             }
         },
         "node_modules/tr46": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
-            "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+            "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "punycode": "^2.1.1"
+                "punycode": "^2.3.1"
             },
             "engines": {
-                "node": ">=12"
+                "node": ">=18"
             }
         },
         "node_modules/tree-kill": {
@@ -19957,16 +20890,6 @@
                 "node": ">=4"
             }
         },
-        "node_modules/universalify": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
-            "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 4.0.0"
-            }
-        },
         "node_modules/unpipe": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -19977,10 +20900,46 @@
                 "node": ">= 0.8"
             }
         },
+        "node_modules/unrs-resolver": {
+            "version": "1.11.1",
+            "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
+            "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==",
+            "dev": true,
+            "hasInstallScript": true,
+            "license": "MIT",
+            "peer": true,
+            "dependencies": {
+                "napi-postinstall": "^0.3.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/unrs-resolver"
+            },
+            "optionalDependencies": {
+                "@unrs/resolver-binding-android-arm-eabi": "1.11.1",
+                "@unrs/resolver-binding-android-arm64": "1.11.1",
+                "@unrs/resolver-binding-darwin-arm64": "1.11.1",
+                "@unrs/resolver-binding-darwin-x64": "1.11.1",
+                "@unrs/resolver-binding-freebsd-x64": "1.11.1",
+                "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
+                "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
+                "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
+                "@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
+                "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
+                "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
+                "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
+                "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
+                "@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
+                "@unrs/resolver-binding-linux-x64-musl": "1.11.1",
+                "@unrs/resolver-binding-wasm32-wasi": "1.11.1",
+                "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
+                "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
+                "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
+            }
+        },
         "node_modules/update-browserslist-db": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
-            "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+            "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
             "dev": true,
             "funding": [
                 {
@@ -20085,17 +21044,6 @@
                 "url": "https://opencollective.com/webpack"
             }
         },
-        "node_modules/url-parse": {
-            "version": "1.5.10",
-            "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
-            "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "querystringify": "^2.1.1",
-                "requires-port": "^1.0.0"
-            }
-        },
         "node_modules/util-deprecate": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -20181,16 +21129,16 @@
             }
         },
         "node_modules/w3c-xmlserializer": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
-            "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+            "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "xml-name-validator": "^4.0.0"
+                "xml-name-validator": "^5.0.0"
             },
             "engines": {
-                "node": ">=14"
+                "node": ">=18"
             }
         },
         "node_modules/wait-on": {
@@ -20255,9 +21203,9 @@
             "license": "Apache-2.0"
         },
         "node_modules/webdriver-bidi-protocol": {
-            "version": "0.3.7",
-            "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.7.tgz",
-            "integrity": "sha512-wIx5Gu/LLTeexxilpk8WxU2cpGAKlfbWRO5h+my6EMD1k5PYqM1qQO1MHUFf4f3KRnhBvpbZU7VkizAgeSEf7g==",
+            "version": "0.3.10",
+            "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.10.tgz",
+            "integrity": "sha512-5LAE43jAVLOhB/QqX4bwSiv0Hg1HBfMmOuwBSXHdvg4GMGu9Y0lIq7p4R/yySu6w74WmaR4GM4H9t2IwLW7hgw==",
             "dev": true,
             "license": "Apache-2.0"
         },
@@ -20692,46 +21640,46 @@
             }
         },
         "node_modules/whatwg-encoding": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
-            "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+            "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
                 "iconv-lite": "0.6.3"
             },
             "engines": {
-                "node": ">=12"
+                "node": ">=18"
             }
         },
         "node_modules/whatwg-mimetype": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
-            "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+            "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
             "dev": true,
             "license": "MIT",
             "engines": {
-                "node": ">=12"
+                "node": ">=18"
             }
         },
         "node_modules/whatwg-url": {
-            "version": "11.0.0",
-            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
-            "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+            "version": "14.2.0",
+            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+            "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
-                "tr46": "^3.0.0",
+                "tr46": "^5.1.0",
                 "webidl-conversions": "^7.0.0"
             },
             "engines": {
-                "node": ">=12"
+                "node": ">=18"
             }
         },
         "node_modules/when-exit": {
-            "version": "2.1.4",
-            "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz",
-            "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==",
+            "version": "2.1.5",
+            "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
+            "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
             "dev": true,
             "license": "MIT"
         },
@@ -20932,13 +21880,13 @@
             }
         },
         "node_modules/xml-name-validator": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
-            "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+            "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
             "dev": true,
             "license": "Apache-2.0",
             "engines": {
-                "node": ">=12"
+                "node": ">=18"
             }
         },
         "node_modules/xmlchars": {
diff --git a/package.json b/package.json
index 0ccfb57..705c424 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,6 @@
         "start": "wp-scripts start"
     },
     "devDependencies": {
-        "@wordpress/scripts": "^30.26.0"
+        "@wordpress/scripts": "^31.2.0"
     }
 }
diff --git a/src/drawer-menu/block.json b/src/drawer-menu/block.json
new file mode 100644
index 0000000..93bc73d
--- /dev/null
+++ b/src/drawer-menu/block.json
@@ -0,0 +1,27 @@
+{
+    "$schema": "https://schemas.wp.org/trunk/block.json",
+    "apiVersion": 3,
+    "name": "jvb/drawer-menu",
+    "title": "Drawer Menu",
+    "category": "jvb",
+	"icon": "menu",
+	"version": "1.0.0",
+	"textdomain": "jvb",
+	"supports": {
+		"html": false
+	},
+	"attributes": {
+		"menuId": {
+			"type": "string",
+			"default": ""
+		},
+		"collapsed": {
+			"type": "boolean",
+			"default": true
+		}
+	},
+	"editorScript": "file:./index.js",
+	"editorStyle": "file:./index.css",
+	"style": "file:./style-index.css",
+	"render": "file:./render.php"
+}
diff --git a/src/drawer-menu/edit.js b/src/drawer-menu/edit.js
new file mode 100644
index 0000000..c37544b
--- /dev/null
+++ b/src/drawer-menu/edit.js
@@ -0,0 +1,33 @@
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, ToggleControl, TextControl } from '@wordpress/components';
+
+export default function Edit({ attributes, setAttributes }) {
+	const { menuId, collapsed } = attributes;
+	const blockProps = useBlockProps();
+
+	return (
+		<>
+			<InspectorControls>
+				<PanelBody title="Drawer Settings">
+					<TextControl
+						label="Menu ID"
+						value={menuId}
+						onChange={(value) => setAttributes({ menuId: value })}
+						help="PHP-generated menu identifier"
+					/>
+					<ToggleControl
+						label="Start Collapsed"
+						checked={collapsed}
+						onChange={(value) => setAttributes({ collapsed: value })}
+					/>
+				</PanelBody>
+			</InspectorControls>
+			<div {...blockProps}>
+				<div className="drawer-menu-preview">
+					<p>Drawer Menu: {menuId || 'Not configured'}</p>
+					<p>State: {collapsed ? 'Collapsed' : 'Expanded'}</p>
+				</div>
+			</div>
+		</>
+	);
+}
diff --git a/src/drawer-menu/editor.scss b/src/drawer-menu/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/drawer-menu/editor.scss
diff --git a/src/drawer-menu/index.js b/src/drawer-menu/index.js
new file mode 100644
index 0000000..e81333b
--- /dev/null
+++ b/src/drawer-menu/index.js
@@ -0,0 +1,10 @@
+import { registerBlockType } from '@wordpress/blocks';
+import edit from './edit';
+import save from './save';
+import './style.scss';
+import './editor.scss';
+
+registerBlockType('jvb/drawer-menu', {
+	edit,
+	save,
+});
diff --git a/src/drawer-menu/index.php b/src/drawer-menu/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/drawer-menu/index.php
diff --git a/src/drawer-menu/render.php b/src/drawer-menu/render.php
new file mode 100644
index 0000000..d0826a3
--- /dev/null
+++ b/src/drawer-menu/render.php
@@ -0,0 +1,39 @@
+<?php
+
+use JVBase\managers\CacheManager;
+use JVBase\ui\Navigation;
+
+$menu_id = $attributes['menuId'] ?? '';
+$collapsed = $attributes['collapsed'] ?? true;
+
+// You'd populate this from options, a filter, or however you store menu data
+$menu_items = apply_filters('jvbDrawerItems', [], $menu_id);
+
+if (empty($menu_items) || empty($menu_id)) {
+	return '<p>Please configure the drawer menu in block settings.</p>';
+}
+
+$cache = CacheManager::for('drawer');
+$cache->clear();
+
+if (!is_front_page()) {
+	$menu_items[] = [
+		'text'	=> 'Home',
+		'url'	=> home_url(),
+		'icon'	=> 'house-simple',
+	];
+}
+$items = array_map(function($item) { return $item['text'];}, $menu_items);
+
+$key = $cache->generateKey($items);
+$menu =  $cache->remember($key,
+function () use ($menu_items, $menu_id, $collapsed) {
+	$menu = new Navigation($menu_id);
+	$menu->asDrawer($collapsed)->populateFromArray($menu_items);
+	return $menu->render();
+});
+
+global $wp;
+
+$current = home_url($wp->request.'/');
+echo str_replace($current.'"', $current.'" class="current" aria-current="page"', $menu);
diff --git a/src/drawer-menu/save.js b/src/drawer-menu/save.js
new file mode 100644
index 0000000..8432b3a
--- /dev/null
+++ b/src/drawer-menu/save.js
@@ -0,0 +1,3 @@
+export default function save() {
+	return null; // Server-side rendered
+}
diff --git a/src/drawer-menu/style.scss b/src/drawer-menu/style.scss
new file mode 100644
index 0000000..52e11af
--- /dev/null
+++ b/src/drawer-menu/style.scss
@@ -0,0 +1,88 @@
+nav.drawer {
+	position: fixed;
+	right: 0;
+	bottom: 0;
+	max-height: 100vh;
+	overflow: hidden auto;
+	width: var(--btn);
+	z-index: var(--z-5);
+	transition: var(--trans-size);
+	--dir: column-reverse;
+	background-color: var(--base);
+	border-left: 1px solid var(--base-200);
+	box-shadow:rgba(var(--base-rgb), var(--op-4)) var(--shdw-left);
+	height: auto;
+	--w: var(--chip_);
+
+	.title,
+	.section-title {
+		display: none;
+	}
+	ul .icon {
+		min-width: var(--chip_);
+	}
+
+	a,
+	.a {
+		height: var(--chipchip);
+		padding: 0 .5rem;
+		width: 100%;
+		gap: .5rem;
+		justify-content: center;
+	}
+
+	.toggle {
+		width: 100%;
+		.icon {
+			transform: rotate(0);
+			transition: var(--trans-transform);
+		}
+	}
+	&.open {
+		width: 240px;
+		.title,
+		.section-title {
+			display: block;
+		}
+
+		.toggle .icon {
+			transform: rotate(180deg);
+		}
+
+		a,
+		.a {
+			justify-content: flex-start;
+		}
+	}
+
+	ul {
+		--dir: column;
+		--gap: 0;
+		--height: auto;
+		padding: 0;
+		margin: 0;
+		height: auto;
+		width:100%;
+	}
+	li {
+		height: auto;
+		width: 100%;
+	}
+
+
+	.row {
+		width: 100%;
+	}
+
+	.menu-section {
+		border-bottom: 1px solid var(--contrast-200);
+	}
+	.section-title {
+		padding: 0.5rem var(--px);
+		font-size: var(--small);
+		text-transform: uppercase;
+		opacity: 0.6;
+		font-weight: bold;
+	}
+
+}
diff --git a/src/drawer-menu/view.js b/src/drawer-menu/view.js
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/drawer-menu/view.js
@@ -0,0 +1 @@
+
diff --git a/src/feed/render.php b/src/feed/render.php
index 7af8d29..57c50e9 100644
--- a/src/feed/render.php
+++ b/src/feed/render.php
@@ -79,8 +79,7 @@
             $type = 'term';
             $context = 'taxonomy:'.$context;
         }
-        jvbDump($settings);
-        jvbDump($contentTypes);
+
         if (empty($contentTypes)) {
             return;
         }
diff --git a/src/feed/style.scss b/src/feed/style.scss
index 74fbf73..b126889 100644
--- a/src/feed/style.scss
+++ b/src/feed/style.scss
@@ -643,7 +643,7 @@
 
 .feed-block {
 	grid-column: full;
-	.feed-filters {
+	.filters {
 		padding: 1rem 0;
 		max-width:var(--wide);
 		margin: 0 auto;
@@ -690,6 +690,10 @@
 
 
 	}
+	h3 {
+		margin: 0 0 .25rem;
+		font-size: var(--medium);
+	}
 }
 /** PLACEHOLDERS **/
 .placeholder {
@@ -721,20 +725,42 @@
 	box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 	height: fit-content;
 	padding: 0;
+	details {
+		z-index: var(--z-2);
+		width: 100%;
+		position: relative;
+		padding: 0;
+		summary {
+			position:absolute;
+			top: -3rem;
+			left:0;
+			width: 100%;
+			background-color: rgba(var(--base-rgb),var(--op-2));
+			backdrop-filter: blur(5px);
+			&:hover {
+				background-color: rgba(var(--action-rgb),var(--op-45));
+			}
+		}
+		&[open] {
+			padding: .25rem .5rem;
+			summary .icon {
+				opacity: 0;
+			}
+		}
+
+	}
 
 	img {
-		opacity: .7;
-		filter: grayscale(.5) sepia(.3) blur(7px);
-		transition: opacity var(--trans-base), filter var(--trans-base);
-		&[data-loaded=true] {
-			opacity: 1;
-			filter: none;
+		object-fit: cover;
+		//opacity: .7;
+		//filter: grayscale(.5) sepia(.3);
+		&:hover {
+			opacity: .8;
 		}
 	}
 
 	&[data-timeline] {
-		aspect-ratio: unset;
-		summary {
+		.images {
 			aspect-ratio: 3/2;
 			padding: 0 0 1rem;
 			span {
@@ -778,46 +804,6 @@
 			display: none;
 		}
 	}
-	details a {
-		font-size: clamp(1rem, 0.9306rem + 0.2222vw, 1.125rem);
-	}
-
-	&.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: rgba(var(--base-rgb),var(--op-3));
-			backdrop-filter: blur(5px);
-			border-radius: var(--radius);
-			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;
diff --git a/src/feed/view.js b/src/feed/view.js
index 710dd6c..fdec017 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -63,15 +63,17 @@
 		};
 		this.ui = window.uiFromSelectors(this.elements);
 
-		this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]');
+
+		this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
 		this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
-		if (this.ui.content.length > 0) {
+		if (this.ui.content && 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,
@@ -79,6 +81,8 @@
 		} else {
 			this.taxonomies = [];
 		}
+
+
 	}
 
 	async initTaxonomies() {
@@ -176,13 +180,15 @@
 		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;
-			}
-		});
+		if (this.ui.filterContainer) {
+			// 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);
@@ -282,7 +288,7 @@
 				this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
 			}
 		});
-		if (hasTaxonomy) {
+		if (this.ui.filterContainer && hasTaxonomy) {
 			for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
 				let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
 				if (button) {
@@ -359,9 +365,6 @@
 				this.removePlaceholders();
 				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());
 				}
@@ -377,8 +380,12 @@
 			this.a11y.announceItems(0, this.store.filters['page'] >1, false);
 		}
 
-		this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
-		this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		if (this.ui.filters.match) {
+			this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		}
+		if (this.ui.clearFilter) {
+			this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		}
 	}
 
 	/**
@@ -386,13 +393,50 @@
 	 * @param {object} item
 	 */
 	createItemElement(item) {
-		let template = window.getTemplate('feed-item');
+		let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
 		if (Object.hasOwn(template.dataset, 'timeline')) {
 			return this.createTimelineElement(item, template);
 		}
+		//fields
+		for (let [fieldName, value] of Object.entries(item.fields)) {
+			let el = template.querySelector(`[data-field="${fieldName}"]`);
+			if (!el) {
+				continue;
+			}
+			if (Object.keys(item.images).map((img)=> parseInt(img)).includes(value)) {
+				[
+					el.src,
+					el.srcset,
+					el.alt
+				] = [
+					item.images[value].tiny,
+					`${item.images[value].tiny} 50w, ${item.images[value].small} 300w, ${item.images[value].medium} 1024w`,
+					item.images[value]['image-alt-text']
+				];
+			} else if (el.tagName === 'TIME') {
+				el.setAttribute('datetime', value);
+				el.textContent = window.formatTimeAgo(value);
+			} else {
+				el.textContent = value;
+			}
+			if (value === '') {
+				el.remove();
+			}
+		}
+		let link = template.querySelector('a');
+		if (link && item.url !== '') {
+			[
+				link.href,
+				link.title
+			] = [
+				item.url,
+				`View ${item.fields['post_title']??'Item'}`
+			];
+		}
 		return template;
 	}
 
+
 	createTimelineElement(item, template) {
 		let [
 			main,
@@ -467,7 +511,7 @@
 
 			let rand = Math.floor(Math.random() * total);
 			let icon;
-			if (this.ui.content.length > 0) {
+			if (this.ui.content && 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);
@@ -584,35 +628,6 @@
 		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);
-			}
-		});
-	}
-
 	handlePopState(e) {
 		if (e.state?.filters) {
 			if (this.processURLFilters()) {
@@ -688,4 +703,32 @@
 		}
 	});
 
+	let item = {
+		content: "art",
+		date: "2025-12-24 03:37:26",
+		fields: {
+			gallery: "",
+			post_content: "",
+			post_thumbnail: 200,
+			post_title: "Great Gray Owl",
+			price: "",
+		},
+		icon: "arrows-clockwise",
+		id: 195,
+		images: {
+			200: {
+				'image-alt-text': "",
+				'image-caption': "",
+				'image-title': "Great Gray Owl",
+				large: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl.jpg",
+				medium: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-1024x1024.jpg",
+				small: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-300x300.jpg",
+				tiny: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-50x50.jpg"
+			}
+		},
+		url: "http://jakevan.test/art/great-gray-owl/",
+		user_id: 3
+	};
 });
+
+
diff --git a/src/forms/view.js b/src/forms/view.js
index 957a5df..20b1f71 100644
--- a/src/forms/view.js
+++ b/src/forms/view.js
@@ -3,12 +3,19 @@
  * Frontend JavaScript for the Form Block
  * Handles form validation and submission
  */
+/**
+ * view.js
+ * Frontend JavaScript for the Form Block
+ */
 class FormBlock {
 	constructor() {
 		this.controller = new window.jvbForm();
 
 		document.querySelectorAll('.jvb-form-block form').forEach(form => {
-			this.controller.registerForm(form, {autosave: true,autoUpload: false});
+			this.controller.registerForm(form, {
+				autosave: true,
+				autoUpload: false
+			});
 		});
 
 		this.controller.subscribe((event, data) => {
@@ -19,68 +26,57 @@
 	}
 
 	async handleFormSubmission(data) {
-		let formId = data.formId;
-		let formConfig = data.config;
-		let formData = data.fullData;
-		let form = formConfig.element;
+		const { formId, config: formConfig, fullData: formData } = data;
+		const form = formConfig.element;
 
-		// Create FormData instead of JSON
 		const submitData = new FormData();
 
-		// Add all regular form fields
+		// Add regular form fields
 		for (const [key, value] of Object.entries(formData)) {
-			// Skip the nonce field - we don't need it for REST API with __return_true
-			if (key === '_wpnonce' || key === '_wp_http_referer') {
-				continue;
-			}
+			if (key === '_wpnonce' || key === '_wp_http_referer') continue;
+
 			if (Array.isArray(value)) {
 				value.forEach(v => submitData.append(`${key}[]`, v));
 			} else if (typeof value === 'object' && value !== null) {
-				// For nested objects, you might want to JSON.stringify them
 				submitData.append(key, JSON.stringify(value));
 			} else {
 				submitData.append(key, value);
 			}
 		}
 
-		// Get uploaded files from UploadManager
+		// Add uploaded files
 		if (window.jvbUploads) {
-			const files = await window.jvbUploads.getFilesForForm(form);
-
-			// Append files with their field names
-			files.forEach(({file, fieldName, uploadId, meta}) => {
-				// Use fieldName[] for multiple files per field
-				submitData.append(`${fieldName}[]`, file);
-			});
+			try {
+				const files = await window.jvbUploads.getFilesForForm(form);
+				files.forEach(({ file, fieldName }) => {
+					submitData.append(`${fieldName}[]`, file);
+				});
+			} catch (error) {
+				console.error('Error getting files:', error);
+			}
 		}
 
-
-		let headers = {
-			// 'X-WP-Nonce': window.auth.getNonce()
-		};
-
-		let block = form.closest('.jvb-form-block');
 		this.controller.showFormStatus(formId, 'uploading');
 
 		try {
 			const response = await fetch(`${jvbSettings.api}forms`, {
 				method: 'POST',
 				credentials: 'same-origin',
-				headers,
-				body: submitData // Send FormData instead of JSON
+				body: submitData
 			});
+
 			const result = await response.json();
 
 			if (!response.ok) {
 				this.controller.showFormStatus(formId, 'error');
-				this.controller.handleFormError(form, result); // ✓ Show error to user
-				return; // Stop processing
+				this.controller.handleFormError(form, result);
+				return;
 			}
 
 			this.controller.showFormStatus(formId, 'submitted');
 			this.controller.showSummary(formId, '.jvb-form-block');
 
-			// Clean up uploaded files from IndexedDB after successful submission
+			// Clean up uploaded files
 			if (window.jvbUploads) {
 				const uploadFields = form.querySelectorAll('[data-upload-field]');
 				for (const field of uploadFields) {
@@ -88,17 +84,16 @@
 					await window.jvbUploads.clearFieldFromStores(fieldId);
 				}
 			}
+
 		} catch (error) {
 			console.error('Form submission error:', error);
 			this.controller.showFormStatus(formId, 'error');
-
-			// Show user-friendly network error - IMPROVED
 			this.controller.handleFormError(form, {
 				message: 'Network error. Please check your connection and try again.',
 				code: 'network_error'
 			});
 		} finally {
-			this.controller.store.delete(formId);
+			await this.controller.store.delete(formId);
 		}
 	}
 }
diff --git a/src/list/render.php b/src/list/render.php
index 2b56c65..f8ef568 100644
--- a/src/list/render.php
+++ b/src/list/render.php
@@ -169,7 +169,7 @@
     $out .= '</section>';
 
     echo $out;
-    echo jvbBuildDirectoryNavigation();
+    echo JVB()->directories()?->renderNavigation();
 
     return ob_get_clean();
 }
diff --git a/src/summary/render.php b/src/summary/render.php
index d6a2040..4c22c7a 100644
--- a/src/summary/render.php
+++ b/src/summary/render.php
@@ -44,7 +44,7 @@
     $artist = jvbContentFromUser((int)$current->post_author);
 
 	$sections = JVB_CONTENT[jvbNoBase($current->post_type)]['sections']??[];
-	jvbDump($sections);
+
 
 
 //    $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type));
diff --git a/wp-config.php b/wp-config.php
new file mode 100644
index 0000000..5fe4c60
--- /dev/null
+++ b/wp-config.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * The base configuration for WordPress
+ *
+ * The wp-config.php creation script uses this file during the installation.
+ * You don't have to use the web site, you can copy this file to "wp-config.php"
+ * and fill in the values.
+ *
+ * This file contains the following configurations:
+ *
+ * * Database settings
+ * * Secret keys
+ * * Database table prefix
+ * * Localized language
+ * * ABSPATH
+ *
+ * @link https://wordpress.org/support/article/editing-wp-config-php/
+ *
+ * @package WordPress
+ */
+
+// ** Database settings - You can get this info from your web host ** //
+/** The name of the database for WordPress */
+define( 'DB_NAME', 'wp_9090417' );
+
+/** Database username */
+define( 'DB_USER', 'user-8484095' );
+
+/** Database password */
+define( 'DB_PASSWORD', 'CZvHXJ0wd2' );
+
+/** Database hostname */
+define( 'DB_HOST', '127.0.0.1' );
+
+/** Database charset to use in creating database tables. */
+define( 'DB_CHARSET', 'utf8' );
+
+/** The database collate type. Don't change this if in doubt. */
+define( 'DB_COLLATE', '' );
+
+/**#@+
+ * Authentication unique keys and salts.
+ *
+ * Change these to different unique phrases! You can generate these using
+ * the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
+ *
+ * You can change these at any point in time to invalidate all existing cookies.
+ * This will force all users to have to log in again.
+ *
+ * @since 2.6.0
+ */
+define( 'AUTH_KEY',          'qxPk61Jf2tn|mz0Dx$9(ikHT4)v[[uagWf3T3`x.Ds2NU[0y7*C3)Ci=ex+A~kp^' );
+define( 'SECURE_AUTH_KEY',   'kK7n[x_}ClcUw2^ow~%WR9aP.!O}kq7FXxbKuAwtBg@.2=r1~!x<R12Y_g10[P^{' );
+define( 'LOGGED_IN_KEY',     '-0tgnq P _y8~m95C9E.[S+o]Z-GC/(|wyyn/}yO5 de51AM#)y#$i4=OQ!Q(2k=' );
+define( 'NONCE_KEY',         '?V-na8MDy-n`S2>3<[i.D#27H/Qlt|v{=R/dSC}Km%p}r){aE?LB=-k:v2ip2#r)' );
+define( 'AUTH_SALT',         '4L0JSr]Z!27na~D?P];2TEz*BVVG~jG;1Od|r28H;$CgWP:J~NUx>$ng.gke2s<5' );
+define( 'SECURE_AUTH_SALT',  '8{LtR_$P;gi=c7j!w+>Pt6KS(T-]F2Wlf~Ca~4-_}A8=~]k3~6):06$;leK_?a G' );
+define( 'LOGGED_IN_SALT',    'hp6]B8`a? w^):H+|c!`UR[T5$rDjHQ$~X)^lZ~N>|swQ8h.1,c:Ut^K|~Rhe-kA' );
+define( 'NONCE_SALT',        'MnNfXD+<Ibe0C&<u]3xmawWV4~>#ckfXV#9@t<;gqr8>o$]D}NQZ6dXTY.L$by,G' );
+define( 'WP_CACHE_KEY_SALT', 'pU95(>.]6vpDh~9!F1C`T/ 02JB[EEMj*)W_pNDD{PRzEm`Oc+5iu?q6}9;F2[ F' );
+
+
+/**#@-*/
+
+/**
+ * WordPress database table prefix.
+ *
+ * You can have multiple installations in one database if you give each
+ * a unique prefix. Only numbers, letters, and underscores please!
+ */
+$table_prefix = 'wp_';
+define( 'WP_AUTO_UPDATE_CORE', false );
+
+
+/* Add any custom values between this line and the "stop editing" line. */
+// Redis Object Cache Configuration
+define('WP_REDIS_HOST', '10.3.2.169');
+define('WP_REDIS_PORT', 6379);
+define('WP_REDIS_PASSWORD', 'CFKceq00762');
+define('WP_REDIS_TIMEOUT', 1);
+define('WP_REDIS_READ_TIMEOUT', 1);
+
+define('WP_REDIS_DATABASE', 0);
+define('WP_REDIS_PREFIX', 'jakevan_');
+define('WP_REDIS_MAXTTL', 86400 * 7);
+
+
+/**
+ * For developers: WordPress debugging mode.
+ *
+ * Change this to true to enable the display of notices during development.
+ * It is strongly recommended that plugin and theme developers use WP_DEBUG
+ * in their development environments.
+ *
+ * For information on other constants that can be used for debugging,
+ * visit the documentation.
+ *
+ * @link https://wordpress.org/support/article/debugging-in-wordpress/
+ */
+define( 'WP_DEBUG', true );
+define( 'WP_DEBUG_DISPLAY', true );
+@ini_set( 'display_errors', 1 );
+
+// Use dev versions of core JS and CSS files (only needed if you are modifying these core files)
+define('SCRIPT_DEBUG', true);
+
+
+
+define( 'WP_ALLOW_MULTISITE', true );
+define( 'MULTISITE', true );
+define( 'SUBDOMAIN_INSTALL', true );
+$base = '/';
+define( 'DOMAIN_CURRENT_SITE', 'jakevan.ca' );
+define( 'PATH_CURRENT_SITE', '/' );
+define( 'SITE_ID_CURRENT_SITE', 1 );
+define( 'BLOG_ID_CURRENT_SITE', 1 );
+
+/* That's all, stop editing! Happy publishing. */
+
+/** Absolute path to the WordPress directory. */
+if ( ! defined( 'ABSPATH' ) ) {
+	define( 'ABSPATH', __DIR__ . '/' );
+}
+
+/** Sets up WordPress vars and included files. */
+require_once ABSPATH . 'wp-jelastic.php';
+require_once ABSPATH . 'wp-settings.php';
+

--
Gitblit v1.10.0