From 3aada9949d51024a92a8b5c6cb70d12f9c3cac16 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 21 Dec 2025 19:59:48 +0000
Subject: [PATCH] =auth refactored via rest, referral system set up for Jane, some javascript consolidation

---
 build/list/style-index-rtl.css              |    2 
 src/faq/style.scss                          |   17 
 assets/js/concise/NotificationManager.js    |    4 
 inc/blocks/SummaryBlock.php                 |    1 
 jvb.php                                     |  779 -
 inc/managers/OperationQueue.php             |    4 
 assets/js/admin/seo-admin.js                |  344 
 assets/js/min/form.min.js                   |    2 
 inc/managers/DashboardManager.php           |  237 
 inc/rest/routes/ContentRoutes.php           |    1 
 assets/js/min/referral.min.js               |    2 
 assets/js/concise/Notifications.js          |   36 
 build/timeline/style-index-rtl.css          |    2 
 inc/managers/_setup.php                     |   22 
 assets/js/concise/CRUD.js                   |   53 
 build/faq/style-index-rtl.css               |    2 
 inc/managers/LoginManager.php               |   40 
 inc/rest/routes/ShopRoutes.php              |    2 
 build/video/block.json                      |    1 
 assets/js/concise/quill.js                  |    2 
 assets/js/concise/Tabs.js                   |   87 
 assets/js/dash/UploadManager.js             |    2 
 build/summary/style-index-rtl.css           |    2 
 inc/helpers/breadcrumbs.php                 |  269 
 src/forms/view.js                           |   58 
 src/glossary/style.scss                     |   30 
 assets/js/concise/ShopManager.js            |    2 
 assets/js/concise/NewsManager.js            |   10 
 SystemReport.php                            |    6 
 assets/js/min/news.min.js                   |    2 
 assets/js/concise/View.js                   |   45 
 inc/managers/SEO/ConfigManager.php          |  390 
 src/gmbreviews/style.scss                   |    4 
 inc/managers/SEO/_edmonotonink.php          |  537 +
 assets/js/concise/ErrorHandler.js           |  112 
 inc/managers/MagicLinkManager.php           |  155 
 build/timeline/style-index.css              |    2 
 build/video/style-index.css                 |    2 
 inc/managers/SEO/SEOAdminPage.php           |  281 
 assets/js/concise/AuthManager.js            |  290 
 assets/js/min/auth.min.js                   |    1 
 inc/managers/SEO/SchemaOutputManager.php    |  710 +
 src/forms/edit.js                           |    1 
 build/video/style-index-rtl.css             |    2 
 src/summary/style.scss                      |    4 
 inc/blocks/GlossaryBlock.php                |    1 
 inc/managers/ErrorHandler.php               |  190 
 assets/js/concise/TaxonomySelector.js       |   15 
 inc/utility/Image.php                       |   67 
 inc/rest/routes/Invitations.php             |   11 
 inc/integrations/PostMark.php               |    1 
 inc/blocks/VideoCoverBlock.php              |    6 
 inc/importers/JaneAppClientImporter.php     |    2 
 assets/js/min/favouritesManager.min.js      |    2 
 src/forms/save.js                           |    1 
 build/feed/view.js                          |    2 
 inc/rest/routes/MagicLinkRoutes.php         |   48 
 inc/integrations/Helcim.php                 |    7 
 assets/js/concise/Modal.js                  |    0 
 assets/js/concise/navigation.js             |   27 
 inc/managers/SEO/TypeBuilder.php            |   85 
 assets/js/concise/UtilityFunctions.js       |  312 
 inc/managers/RoleManager.php                |   19 
 assets/js/min/queue.min.js                  |    2 
 assets/js/min/tabs.min.js                   |    2 
 inc/blocks/CustomBlocks.php                 |   40 
 src/forms/index.js                          |    1 
 inc/managers/SEO/TemplateResolver.php       |  663 +
 assets/js/concise/FrontendVotes.js          |    6 
 build/gmbreviews/style-index-rtl.css        |    2 
 inc/blocks/TimelineBlock.php                |    1 
 inc/managers/SEO/_setup.php                 |   15 
 inc/managers/IconsManagerBackup.php         |  670 +
 assets/js/min/gallery.min.js                |    2 
 inc/managers/IconsManager.php               |  687 +
 build/feed/style-index-rtl.css              |    2 
 inc/helpers/all.php                         |    2 
 assets/js/min/selector.min.js               |    2 
 assets/js/concise/FormController.js         |  500 
 inc/managers/SEO/SchemaBuilder.php          | 1735 +++
 cleanup.php                                 |   11 
 assets/css/copy-hours.min.css               |    2 
 assets/js/concise/UserSettings.js           |   22 
 assets/js/concise/FrontendFavourites.js     |    6 
 src/video/style.scss                        |   11 
 inc/rest/routes/FavouritesRoutes.php        |    4 
 src/video/view.js                           |   54 
 assets/js/concise/TaxonomyCreator.js        |    2 
 assets/js/min/quill.min.js                  |    2 
 build/forms/view.asset.php                  |    2 
 assets/js/concise/SchemaManager.js          |  459 +
 assets/js/concise/on-this-page.js           |    0 
 build/summary/style-index.css               |    2 
 inc/helpers/members.php                     |    1 
 inc/rest/routes/QueueRoutes.php             |   27 
 assets/js/min/bioManager.min.js             |    2 
 assets/js/min/creator.min.js                |    2 
 build/feed/view.asset.php                   |    2 
 inc/blocks/FormBlock.php                    |    3 
 inc/meta/MetaRenderer.php                   |   66 
 inc/managers/FormManager.php                |    2 
 assets/css/dash.min.css                     |    2 
 inc/managers/SEO/FieldBuilder.php           |   89 
 inc/managers/SEO/FieldOverrideBuilder.php   |   44 
 src/forms/style.scss                        |  250 
 assets/js/min/notifications.min.js          |    2 
 inc/ui/Tabs.php                             |  210 
 assets/js/min/schema.min.js                 |    1 
 assets/js/concise/Gallery.js                |   65 
 assets/js/concise/SquareCheckout.js         |    0 
 JVBase.php                                  |   50 
 assets/css/style.min.css                    |    2 
 build/list/style-index.css                  |    2 
 inc/integrations/Square.php                 |    7 
 inc/meta/MetaManager.php                    |   21 
 assets/css/feed.min.css                     |    2 
 assets/js/concise/Queue.js                  |  231 
 inc/managers/EmailManager.php               |   45 
 inc/ui/CRUDSkeleton.php                     | 1731 +++
 inc/rest/routes/SEORoutes.php               |  297 
 src/feed/style.scss                         |   58 
 assets/js/concise/PostSelector.js           |    2 
 src/list/style.scss                         |    2 
 build/feed/style-index.css                  |    2 
 assets/js/concise/UploadManager.js          |   33 
 src/feed/view.js                            |   24 
 assets/js/concise/A11yHelper.js             |    0 
 src/video/block.json                        |    1 
 build/glossary/style-index-rtl.css          |    2 
 activate.php                                |    2 
 inc/ui/_setup.php                           |    5 
 assets/css/forms.min.css                    |    2 
 inc/registry/FieldRegistry.php              |    2 
 inc/rest/routes/ReferralRoutes.php          | 1363 +--
 assets/js/min/integrations.min.js           |    2 
 assets/js/min/settings.min.js               |    2 
 assets/css/admin/seo-admin.css              |  260 
 assets/js/concise/UserInteractions.js       |  290 
 inc/rest/_setup.php                         |    1 
 inc/meta/MetaForm.php                       |  175 
 assets/js/min/interactions.min.js           |    1 
 assets/js/concise/CopyHours.js              |    0 
 build/gmbreviews/style-index.css            |    2 
 inc/managers/ScriptLoader.php               |  558 +
 assets/js/min/view.min.js                   |    2 
 assets/js/concise/DataStore.js              |  305 
 assets/js/min/ContentManager.min.js         |    2 
 webpack.jvb.js                              |   53 
 assets/js/min/notificationManager.min.js    |    2 
 inc/admin/Integrations.php                  |    2 
 inc/meta/MetaSanitizer.php                  |   50 
 assets/js/min/dataStore.min.js              |    2 
 inc/ui/Modal.php                            |  175 
 inc/rest/routes/LoginRoutes.php             |  229 
 assets/js/concise/ContentManager.js         |   16 
 assets/js/concise/FavouritesManager.js      |   16 
 build/forms/view.js                         |    2 
 inc/managers/CacheManager.php               |   61 
 assets/js/min/navigation.min.js             |    2 
 build/faq/style-index.css                   |    2 
 build/video/view.asset.php                  |    1 
 inc/managers/SEO/SchemaReferenceBuilder.php |  539 +
 inc/helpers/ui.php                          |    8 
 build/video/view.js                         |    1 
 inc/managers/CRUDManager.php                | 1399 --
 assets/js/concise/Integrations.js           |   27 
 assets/js/min/utility.min.js                |    2 
 inc/managers/SEO/SchemaRegistry.php         | 1857 ++++
 src/timeline/style.scss                     |   15 
 inc/managers/SEO/SchemaFieldHelpers.php     | 1199 ++
 inc/managers/NotificationManager.php        |    2 
 assets/js/concise/GoogleMaps.js             |    0 
 inc/ui/Navigation.php                       |  326 
 assets/js/concise/Referral.js               |  408 
 inc/registry/CheckCustomTables.php          |   43 
 inc/rest/routes/FormRoutes.php              |    2 
 inc/managers/SEO/BreadcrumbManager.php      |  327 
 inc/meta/MetaValidator.php                  |  185 
 inc/utility/Validator.php                   |  241 
 assets/js/min/crud.min.js                   |    2 
 inc/helpers/renderFields.php                |    2 
 inc/meta/MetaTypeManager.php                |    5 
 inc/managers/ReferralManager.php            |  695 +
 inc/blocks/MenuBlock.php                    |    1 
 assets/js/min/error.min.js                  |    2 
 base/seo.php                                |  146 
 inc/managers/AdminPages.php                 |  101 
 assets/css/nav.min.css                      |    2 
 build/glossary/style-index.css              |    2 
 assets/js/min/uploader.min.js               |    2 
 /dev/null                                   | 1414 ---
 inc/rest/RestRouteManager.php               |   30 
 assets/js/concise/BioManager.js             |    2 
 inc/rest/routes/AdminRoutes.php             |    1 
 194 files changed, 19,425 insertions(+), 6,692 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 04bb699..45fc12e 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -2,13 +2,17 @@
 namespace JVBase;
 
 use JVBase\integrations\BlueSky;
+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\ReferralManager;
 use JVBase\managers\RoleManager;
-use JVBase\managers\SchemaManager;
+//use JVBase\managers\SchemaManager;
+use JVBase\managers\SEO\SchemaOutputManager;
+use JVBase\managers\SEO\SEOAdminPage;
 use JVBase\managers\AdminPages;
 use JVBase\managers\NotificationManager;
 use JVBase\managers\UserTermsManager;
@@ -22,6 +26,7 @@
 use JVBase\rest\routes\BioRoutes;
 use JVBase\rest\routes\SettingsRoutes;
 use JVBase\rest\routes\ShopRoutes;
+use JVBase\rest\routes\SEORoutes;
 use JVBase\rest\routes\QueueRoutes;
 use JVBase\rest\routes\ErrorRoutes;
 use JVBase\rest\routes\FormRoutes;
@@ -82,15 +87,27 @@
 //            'dash'          => new DashboardManager(),
             'roles'         => new RoleManager(),
 //            'forms'         => new FormManager(),
-            'schema'        => new SchemaManager(),
+            'schema'        => new SchemaOutputManager(),
             'admin'         => new AdminPages(),
+			'seoAdmin'		=> new SEOAdminPage(),
 //			'uploads'		=> new UploadManager(),
 			'userTerms'		=> new UserTermsManager(),
+			'email'			=> new EmailManager(),
         ];
 
+		$this->routes = [
+			'login'			=> new LoginRoutes(),
+			'integrations'	=> new IntegrationsRoutes(),
+			'seo'  			=> new SEORoutes(),
+			'queue'  		=> new QueueRoutes(),
+			'settings'		=> new SettingsRoutes(),
+			'upload' 		=> new UploadRoutes(),
+			'forms'			=> new FormRoutes()
+		];
 
 		if (Features::forSite()->has('magicLink')) {
 			$this->routes['magicLink'] = new MagicLinkRoutes();
+			$this->managers['magicLink'] = new MagicLinkManager();
 		}
 		if (Features::forSite()->has('referrals')) {
 			$this->managers['referral'] = new ReferralManager();
@@ -105,10 +122,6 @@
 			$this->routes['square'] = new IntegrationsSquareRoutes();
 		}
 
-        $this->routes = [
-			'login'			=> new LoginRoutes(),
-			'integrations'	=> new IntegrationsRoutes(),
-		];
         if (Features::forSite()->has('feed_block')) {
             $this->routes['feed'] = new FeedRoutes();
         }
@@ -120,9 +133,7 @@
             $this->routes['term'] = new TermRoutes();
         }
 
-		$this->routes['queue']  = new QueueRoutes();
-		$this->routes['settings']= new SettingsRoutes();
-		$this->routes['upload'] = new UploadRoutes();
+
         if (jvbSiteHasDashboard()) {
             $this->routes['error']  = new ErrorRoutes();
             $this->routes['admin']  = new AdminRoutes();
@@ -131,7 +142,6 @@
             $this->routes['shop']   = new ShopRoutes();
             $this->routes['options']= new OptionsRoutes();
         }
-		$this->routes['forms']= new FormRoutes();
 
         if (jvbSiteHasFavourites()) {
             $this->routes['favourites'] = new FavouritesRoutes();
@@ -226,10 +236,14 @@
     {
         return $this->managers['admin'];
     }
+	public function seoAdmin()
+	{
+		return $this->managers['seoAdmin'];
+	}
 
     public function getFields($type):array
     {
-        $content = JVB_CONTENT[$type]??JVB_TAXONOMY[$type]??JVB_USER[$type]??null;
+        $content = JVB_CONTENT[$type]??JVB_TAXONOMY[$type]??JVB_USER[$type]??[];
         return $content['fields']??[];
     }
     public function getContent($type):mixed
@@ -272,9 +286,19 @@
 		$this->routes[$slug] = $class;
 	}
 
-	public function referrals():ReferralManager
+	public function email():EmailManager
 	{
-		return $this->managers['referral'];
+		return $this->managers['email'];
+	}
+
+	public function referrals():ReferralManager|false
+	{
+		return $this->managers['referral']??false;
+	}
+
+	public function magicLink():MagicLinkManager|false
+	{
+		return $this->managers['magicLink']??false;
 	}
 
 	public function additionalActions():void
diff --git a/SystemReport.php b/SystemReport.php
index 30e1672..adcc3d4 100644
--- a/SystemReport.php
+++ b/SystemReport.php
@@ -1650,13 +1650,13 @@
     {
         $admin_email = get_option('admin_email');
 
-        jvbMail($admin_email, $subject, $content);
+        JVB()->email()->sendEmail($admin_email, $subject, $content);
 
         // Also send to any additional configured recipients
         $additional_recipients = get_option('jvb_report_recipients', []);
         if (!empty($additional_recipients)) {
             foreach ($additional_recipients as $recipient) {
-                jvbMail($recipient, $subject, $content);
+				JVB()->email()->sendEmail($recipient, $subject, $content);
             }
         }
     }
@@ -1962,7 +1962,7 @@
 
         // Send to admin only
         $admin_email = get_option('admin_email');
-        return jvbMail($admin_email, $subject, $content);
+        return JVB()->email()->sendEmail($admin_email, $subject, $content);
     }
 
     //List report:
diff --git a/activate.php b/activate.php
index 72e59e5..0dad565 100644
--- a/activate.php
+++ b/activate.php
@@ -2,6 +2,7 @@
 
 use JVBase\integrations\Umami;
 use JVBase\managers\ReferralManager;
+use JVBase\managers\SEO\SEOAdminPage;
 use JVBase\utility\Features;
 
 if (!defined('ABSPATH')) {
@@ -278,4 +279,5 @@
 	if (Features::forSite()->has('referrals')){
 		ReferralManager::addSubpage();
 	}
+	SEOAdminPage::addSubpage();
 }
diff --git a/assets/css/admin/seo-admin.css b/assets/css/admin/seo-admin.css
new file mode 100644
index 0000000..14b46a5
--- /dev/null
+++ b/assets/css/admin/seo-admin.css
@@ -0,0 +1,260 @@
+/**
+ * JVBase SEO Admin Styles
+ */
+
+.jvb-seo-admin {
+	max-width: 1200px;
+}
+
+/* Tabs */
+.jvb-seo-tabs {
+	display: flex;
+	gap: 0;
+	border-bottom: 1px solid #c3c4c7;
+	margin-bottom: 20px;
+}
+
+.jvb-seo-tabs .tab-btn {
+	padding: 10px 20px;
+	border: 1px solid transparent;
+	border-bottom: none;
+	background: #f0f0f1;
+	cursor: pointer;
+	font-size: 14px;
+	margin-bottom: -1px;
+	border-radius: 4px 4px 0 0;
+}
+
+.jvb-seo-tabs .tab-btn:hover {
+	background: #fff;
+}
+
+.jvb-seo-tabs .tab-btn.active {
+	background: #fff;
+	border-color: #c3c4c7;
+	font-weight: 600;
+}
+
+/* Tab content */
+.tab-content {
+	display: none;
+}
+
+.tab-content.active {
+	display: block;
+}
+
+/* Forms */
+.jvb-seo-form,
+.jvb-seo-fieldset {
+	background: #fff;
+	border: 1px solid #c3c4c7;
+	padding: 20px;
+	margin-bottom: 20px;
+	border-radius: 4px;
+}
+
+.jvb-seo-form h2,
+.jvb-seo-content-type h3 {
+	margin-top: 0;
+	padding-bottom: 10px;
+	border-bottom: 1px solid #eee;
+}
+
+.form-field {
+	margin-bottom: 15px;
+}
+
+.form-field label {
+	display: block;
+	font-weight: 600;
+	margin-bottom: 5px;
+}
+
+.form-field input.regular-text,
+.form-field input.large-text,
+.form-field textarea,
+.form-field select {
+	width: 100%;
+	max-width: 600px;
+}
+
+.form-field input.small-text {
+	width: 80px;
+}
+
+.form-field .description {
+	display: block;
+	color: #646970;
+	font-size: 12px;
+	margin-top: 4px;
+}
+
+.form-field textarea {
+	min-height: 80px;
+}
+
+/* Fieldsets */
+.jvb-seo-fieldset fieldset {
+	border: 1px solid #ddd;
+	padding: 15px;
+	margin-bottom: 15px;
+	border-radius: 4px;
+}
+
+.jvb-seo-fieldset fieldset legend {
+	font-weight: 600;
+	padding: 0 10px;
+}
+
+/* Content type sections */
+.jvb-seo-content-type {
+	margin-bottom: 30px;
+}
+
+.jvb-seo-content-type h3 {
+	cursor: pointer;
+	padding: 15px;
+	background: #f6f7f7;
+	border: 1px solid #c3c4c7;
+	margin: 0;
+	border-radius: 4px;
+}
+
+.jvb-seo-content-type h3:hover {
+	background: #f0f0f1;
+}
+
+.jvb-seo-content-type .jvb-seo-fieldset {
+	border-top: none;
+	border-radius: 0 0 4px 4px;
+}
+
+/* Schema fields */
+.schema-fields {
+	margin-top: 15px;
+	padding-top: 15px;
+	border-top: 1px dashed #ddd;
+}
+
+.schema-field-mapping {
+	padding: 10px;
+	background: #f9f9f9;
+	border-radius: 4px;
+	margin-bottom: 10px;
+}
+
+/* Form actions */
+.form-actions {
+	display: flex;
+	gap: 10px;
+	margin-top: 20px;
+	padding-top: 15px;
+	border-top: 1px solid #eee;
+}
+
+/* Repeater fields */
+.repeater-field .repeater-row {
+	display: flex;
+	gap: 10px;
+	align-items: center;
+	margin-bottom: 8px;
+}
+
+.repeater-field .repeater-row input {
+	flex: 1;
+}
+
+.repeater-field .remove-row {
+	color: #b32d2e;
+	border-color: #b32d2e;
+	line-height: 1;
+	padding: 4px 10px;
+}
+
+.repeater-field .add-row {
+	margin-top: 5px;
+}
+
+/* Image field */
+.image-field {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+}
+
+.image-field .image-preview img {
+	max-height: 50px;
+	border-radius: 4px;
+}
+
+/* Variable reference */
+.jvb-seo-variable-ref {
+	background: #f0f6fc;
+	border: 1px solid #72aee6;
+	padding: 15px;
+	margin-top: 20px;
+	border-radius: 4px;
+}
+
+.jvb-seo-variable-ref h3 {
+	margin-top: 0;
+	color: #135e96;
+}
+
+.jvb-seo-variable-ref .variable-list code {
+	background: #fff;
+	padding: 2px 6px;
+	border-radius: 3px;
+	font-size: 12px;
+}
+
+/* Notices */
+.jvb-seo-admin .notice {
+	margin: 15px 0;
+}
+
+/* Template fields - visual indicator */
+.template-field {
+	font-family: monospace;
+	background: #fff;
+}
+
+.template-field:focus {
+	border-color: #2271b1;
+	box-shadow: 0 0 0 1px #2271b1;
+}
+
+/* Responsive */
+@media (max-width: 782px) {
+	.jvb-seo-tabs {
+		flex-wrap: wrap;
+	}
+
+	.jvb-seo-tabs .tab-btn {
+		flex: 1 1 auto;
+		text-align: center;
+	}
+
+	.form-field input.regular-text,
+	.form-field input.large-text,
+	.form-field textarea,
+	.form-field select {
+		max-width: 100%;
+	}
+}
+
+/* Dashboard integration */
+.jvb-dash .jvb-seo-admin {
+	padding: 0;
+}
+
+.jvb-dash .jvb-seo-tabs {
+	background: transparent;
+}
+
+.jvb-dash .jvb-seo-form,
+.jvb-dash .jvb-seo-fieldset {
+	background: var(--jvb-bg, #fff);
+	border-color: var(--jvb-border, #c3c4c7);
+}
diff --git a/assets/css/copy-hours.min.css b/assets/css/copy-hours.min.css
index 101a314..7348331 100644
--- a/assets/css/copy-hours.min.css
+++ b/assets/css/copy-hours.min.css
@@ -1 +1 @@
-.group-fields{position:relative}.hours-copy-btn:hover{background-color:var(--action-50);transform:scale(1.05)}.hours-copy-btn:active{transform:scale(.95)}.hours-copy-btn .icon{--w:0.875rem}.copy-hours-content h3{margin:0 0 1rem 0;color:var(--contrast);font-size:var(--large)}.copy-hours-source{background-color:var(--base-100);padding:1rem;border-radius:var(--innerRadius);margin-bottom:1.5rem;border:1px solid var(--base-200)}.copy-hours-source h4{margin:0 0 .5rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--small);font-weight:600}.source-info{--gap:.25rem}.source-day{font-weight:600;color:var(--contrast);text-transform:capitalize}.source-hours{--gap:1rem;font-weight:500;color:var(--contrast)}.source-hours.closed{color:var(--contrast-200);font-style:italic}.copy-hours-targets{margin-bottom:2rem}.copy-hours-targets h4{margin:0 0 1rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--small);font-weight:600}.day-checkboxes{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem}.feedback{position:fixed;top:2rem;right:2rem;background-color:var(--action-50);color:var(--action-contrast);padding:1rem 1.5rem;border-radius:var(--innerRadius);box-shadow:var(--shadow);z-index:10000;opacity:0;transform:translateX(100px);transition:all var(--transition-base);display:flex;align-items:center;gap:.5rem}.feedback.show{opacity:1;transform:translateX(0)}.feedback .icon{--w:1.25rem}
\ No newline at end of file
+.group-fields{position:relative}.hours-copy-btn:hover{background-color:var(--action-50);transform:scale(1.05)}.hours-copy-btn:active{transform:scale(.95)}.hours-copy-btn .icon{--w:0.875rem}.copy-hours-content h3{margin:0 0 1rem 0;color:var(--contrast);font-size:var(--txt-large)}.copy-hours-source{background-color:var(--base-100);padding:1rem;border-radius:var(--radius);margin-bottom:1.5rem;border:1px solid var(--base-200)}.copy-hours-source h4{margin:0 0 .5rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--txt-small);font-weight:600}.source-info{--gap:.25rem}.source-day{font-weight:600;color:var(--contrast);text-transform:capitalize}.source-hours{--gap:1rem;font-weight:500;color:var(--contrast)}.source-hours.closed{color:var(--contrast-200);font-style:italic}.copy-hours-targets{margin-bottom:2rem}.copy-hours-targets h4{margin:0 0 1rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--txt-small);font-weight:600}.day-checkboxes{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem}.feedback{position:fixed;top:2rem;right:2rem;background-color:var(--action-50);color:var(--action-contrast);padding:1rem 1.5rem;border-radius:var(--radius);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:10000;opacity:0;transform:translateX(100px);transition:all var(--trans-base);display:flex;align-items:center;gap:.5rem}.feedback.show{opacity:1;transform:translateX(0)}.feedback .icon{--w:1.25rem}
\ No newline at end of file
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index ee26f5f..addfcae 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--height)}.dashboard h1:first-of-type{margin-top:4rem!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:fit-content;padding:.5rem!important;min-width:0;min-height:0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}.dashboard.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--outerRadius);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:1200px;margin:0 auto}.referral-header{text-align:center;margin-bottom:30px}.referral-code-card{background:var(--base-100);padding:30px;border-radius:8px;text-align:center;margin-bottom:30px}.code-display{display:flex;align-items:center;justify-content:center;gap:15px;margin:20px 0}.code-display .code{font-size:32px;font-weight:700;letter-spacing:2px;color:var(--action-0);user-select:all}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:20px;margin-bottom:30px}.stat-card{background:#fff;padding:25px;border-radius:8px;border:1px solid #ddd;text-align:center}.stat-card.highlight{background:#d4edda;border-color:#c3e6cb}.stat-card h4{margin:0 0 10px 0;color:#666;font-size:14px;font-weight:600;text-transform:uppercase}.stat-number{font-size:36px;font-weight:700;color:#2271b1}.referrals-list-card{background:#fff;padding:25px;border-radius:8px;border:1px solid #ddd}.referrals-table{width:100%;border-collapse:collapse;margin-top:15px}.referrals-table td,.referrals-table th{padding:12px;text-align:left;border-bottom:1px solid #eee}.referrals-table th{background:#f5f5f5;font-weight:600}.status-badge{padding:4px 12px;border-radius:12px;font-size:12px;font-weight:500}.status-badge.pending{background:#fff3cd;color:#856404}.status-badge.consulted{background:#d1ecf1;color:#0c5460}.status-badge.treated{background:#d4edda;color:#155724}
\ No newline at end of file
+: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-bottom: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{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--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(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height: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}.dashboard.settings nav.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{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(--rgb-medium)) 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/feed.min.css b/assets/css/feed.min.css
index 732e939..bae572f 100644
--- a/assets/css/feed.min.css
+++ b/assets/css/feed.min.css
@@ -1 +1 @@
-.feed-block{max-width:var(--full);margin:0 auto}.feed-block>:not(.feed-grid,h2){max-width:var(--alignWide);margin:1rem var(--mr) 1rem var(--ml)}.feed-block>h2{max-width:var(--maxWidth)}.feed-block[data-loading=true]{opacity:.7}.feed-block:empty::before{content:"Looks like there's nothing here yet.";display:block;text-align:center;padding:2rem}.feed-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.5rem;margin-bottom:2rem}@media (min-width:768px){.feed-grid{grid-template-columns:repeat(4,1fr);gap:1rem}.feed-empty-state{grid-column:2/span 2!important}}@media (min-width:1200px){.feed-grid{grid-template-columns:repeat(6,1fr)}.feed-empty-state{grid-column:2/span 4!important}}.feed-item{position:relative;border-radius:.5rem;overflow:hidden;background:var(--base-50);box-shadow:0 2px 4px rgba(0,0,0,.1);opacity:0;transition:opacity var(--transition-base) var(--delay);height:fit-content;padding:0}.feed-item[data-loaded]{opacity:1}.feed-item[data-loaded]+.feed-item[data-loaded]{--delay:var(--delay) + var(--increase)}.feed-item.artist{grid-column:span 2}.feed-item.highlighted{animation:highlight 2s ease-out}.feed-image{display:block;aspect-ratio:1;overflow:hidden;width:100%;height:100%}.artist-tattoos img,.feed-image img{width:100%;height:100%;object-fit:cover;transition:transform var(--timing) var(--function)}.artist-tattoos a:hover img,.feed-image:hover img{transform:scale(1.05)}.item-info{padding:.25rem 1rem}.item-info h3{margin:0!important;font-size:1.1rem;font-family:var(--body);font-weight:var(--bWeight);text-align:center}.item-info span{text-transform:uppercase;display:flex;align-items:center}.item-info .icon{margin-right:1em}.taxonomy-lists{margin:.5rem 0}.taxonomy-group{display:flex;flex-direction:column;align-items:flex-start;gap:.5rem;margin-bottom:.25rem}.taxonomy-group ul{list-style:none;margin:0;padding:0}.item-labels{margin-top:.5rem;display:flex;flex-wrap:wrap;gap:.5rem}.label{display:flex;align-items:center;gap:.25rem;font-size:.9rem}.label a{color:inherit;text-decoration:none}.label a:hover{color:var(--pink-0)}.favourite-button{position:absolute;top:.5rem;right:.5rem;z-index:10;background:var(--overlay-medium);border-radius:50%;box-shadow:var(--subtle);border:none;cursor:pointer;width:2rem;height:2rem;display:flex;justify-content:center;align-items:center;backdrop-filter:blur(5px);transition:all var(--transition-base)}.favourite-button:hover{transform:scale(1.1);color:var(--pink-0);background:var(--base);box-shadow:0 4px 8px rgba(0,0,0,.15)}.favourite-button.favourited{animation:favourite-pop .4s cubic-bezier(.25,.46,.45,.94)}.feed-filters{margin:2rem auto;position:relative}.feed-filters .feed-controls{display:flex;justify-content:space-between;align-items:center;gap:2rem;width:100%}.feed-filters details summary{justify-content:flex-start;padding:2rem .5rem .5rem}.feed-filters details[open] summary{background-color:var(--base-50)}.feed-filters summary:after{position:absolute;right:.5rem;top:.5rem}.feed-filters .filter-toggle,.feed-filters .type-filter>label,.radio-group-label>label{display:flex;justify-content:center;align-items:center;padding:.35rem;white-space:nowrap;width:fit-content;height:fit-content;cursor:pointer;border:1px solid var(--base-200);border-radius:4px;font-size:.875rem;transition:border-color var(--transition-base);margin-bottom:.5rem}.filter-toggle .icon{margin-right:.5rem}.type-filter:hover{color:var(--pink-0);border-color:var(--pink-0);transition:var(--transition-color)}.feed-filters .type-filter>label{flex-direction:column}.type-filter.favourites-toggle{margin-left:auto}.type-filter.favourites-toggle label{position:relative}.type-filter.favourites-toggle label .label{top:100%;right:0}input[hidden]+label{display:none}.feed-filters svg{width:25px;height:25px}.order-options{position:relative;display:flex;justify-content:space-between}.order-options .order-by{display:flex}.order-options .order-by .radio-group-label,.order-options .order-direction{display:flex;padding-top:1.5rem;position:relative}.order-options .order-by>.label{margin-right:2rem}.radio-group-label{display:flex;gap:.5rem}.feed-filters .radio-group-label label .label{top:.5rem;right:.5rem}.feed-filters .order-options label svg{width:20px;height:20px}.feed-filters input:checked+label,.feed-filters label:hover,.radio-group-label input:checked+label{background-color:var(--white);border-color:var(--pink);color:var(--pink)}.feed-filters label .label{position:absolute;visibility:hidden;top:.5rem;right:4rem;opacity:0;transition:transform var(--timing) var(--function);transition-property:max-width,transform}.feed-filters input:checked+label .label{visibility:visible;opacity:1}.feed-filters .filters{padding:1rem;margin-top:1rem;background-color:transparent}.has-filters.filters{background-color:var(--base-50)}.filter-group{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.25rem;position:relative}.feed-overlay{display:none;opacity:0;visibility:hidden}.loading .feed-overlay{position:fixed;top:0;left:0;right:0;bottom:0;margin:0!important;max-width:none!important;width:100%;height:100%;background:var(--overlay-medium);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);display:flex;justify-content:center;align-items:center;z-index:999999;opacity:1;visibility:visible;transition:opacity .3s ease,visibility .3s ease}.feed-overlay-content{background:var(--base);padding:2rem;border-radius:1rem;box-shadow:var(--shadow);text-align:center;width:min(400px,60vw)}.loading .loading-icon-container{position:relative;margin-bottom:1.5rem;animation:dance 1s ease-in-out infinite;transition:opacity .2s ease;will-change:transform,opacity}.loading .loading-message .icon{width:3em;height:3em}.loading .loading-message .icon svg{width:100%;height:100%;margin-right:1rem;animation:dance 2s ease-in-out infinite;transition:color .3s ease}.loading .loading-message{will-change:opacity;font-size:1rem;color:#666;text-align:center;min-height:24px;transition:opacity .2s ease;margin-bottom:1rem}.loading .loading-dots{color:var(--pink-0);width:4px;aspect-ratio:1;border-radius:50%;box-shadow:19px 0 0 7px,38px 0 0 3px,57px 0 0 0;transform:translateX(-38px) scale(.666);animation:bubble .5s infinite alternate linear}.feed-empty-state{grid-column-start:1;grid-column-end:2;text-align:center;padding:2rem;background:var(--base);border-radius:1rem;margin:0 auto;max-width:600px}.feed-empty-state h3{text-align:center;font-family:var(--heading);font-size:clamp(1.5rem,3vw,2.5rem);margin:0 0 2rem 0;color:var(--pink-0)}.feed-empty-state p{font-family:var(--body);margin:1rem 0;font-size:clamp(1rem,2vw,1.2rem);line-height:1.4}.feed-empty-state p:last-child{color:var(--pink-0);margin-top:2rem}@keyframes highlight{0%,100%{box-shadow:none}50%{box-shadow:0 0 0 4px var(--pink-0)}}@keyframes favourite-pop{0%{transform:scale(1)}50%{transform:scale(1.3)}75%{transform:scale(.9)}100%{transform:scale(1)}}@keyframes bubble{50%{box-shadow:19px 0 0 3px,38px 0 0 7px,57px 0 0 3px}100%{box-shadow:19px 0 0 0,38px 0 0 3px,57px 0 0 7px}}@keyframes dance{0%,100%{transform:rotate(-5deg) scale(1)}50%{transform:rotate(5deg) scale(1.1)}}.artist-tattoos{display:grid;grid-template-columns:repeat(3,1fr);gap:.25em}.artist-tattoos a:has(img){overflow:hidden;background-color:var(--base-100)}.artist-tattoos a:not(.feed-image) img{width:100%;height:100%;object-fit:cover}.artist-tattoos a::after,.artist-tattoos a::before{display:none}.artist-tattoos .feed-image{grid-row:span 2;grid-column:span 2}.feed-item summary .handle{position:absolute;bottom:0;left:0;right:0;background-color:var(--overlay-light);backdrop-filter:blur(5px);border-radius:var(--innerRadius);z-index:1;padding:.25rem .25rem .25rem 1.1rem}.feed-item:hover summary .handle,.feed-item[open] summary .handle{background-color:var(--overlay-pink-medium);backdrop-filter:blur(5px)}.feed-item summary:after{z-index:11;position:absolute;bottom:.35rem;right:.7rem;width:1.5rem;height:1.5rem;cursor:pointer}.loading .feed-overlay h2{width:fit-content;margin:1rem auto!important;color:transparent;-webkit-text-stroke:1px var(--contrast);--g:conic-gradient(var(--pink-0) 0 0) no-repeat text;background:var(--g) 0,var(--g) 1ch,var(--g) 2ch,var(--g) 3ch,var(--g) 4ch,var(--g) 5ch,var(--g) 6ch;animation:l17-0 1s linear infinite alternate,l17-1 2s linear infinite}@keyframes l17-0{0%{background-size:1ch 0}100%{background-size:1ch 100%}}@keyframes l17-1{0%,50%{background-position-y:100%,0}50.01%,to{background-position-y:0,100%}}.loading .loading-message{display:flex;justify-content:center;align-items:center;overflow:hidden}.loading .dots-wrapper{display:flex;justify-content:center;align-items:center}.loading .loading-message p{opacity:1;transform:scaleY(1);transform-origin:bottom;transition:opacity var(--transition-base),transform var(--transition-base)}.loading .changing .loading-message p{opacity:0;transform:scaleY(0);transform-origin:top}.loading .feed-overlay::after{content:'';position:absolute;z-index:-1;inset:0;background:linear-gradient(90deg,var(--shimmer));animation:shimmer 3s ease-in-out infinite}@keyframes shimmer{0%{transform:translateX(-100%)}100%,50%{transform:translateX(100%)}}@media (max-width:768px){.feed-filters .feed-controls{flex-direction:column;gap:1rem}.feed-empty-state{grid-column-end:none;padding:2rem 1rem;margin:1rem}.feed-filters details summary{gap:.5rem;justify-content:flex-start}}[hidden],[hidden]+label{display:none}.feed-loader{display:flex;flex-direction:column;align-items:center;gap:1rem;margin:2rem auto 0!important}.load-more{opacity:1;display:flex;align-items:center;gap:.5rem;padding:.75rem 1.5rem;background:var(--base-200);color:var(--contrast-200);border:none;border-radius:4px;font-size:var(--medium);cursor:pointer;transition:all var(--transition-base)}.load-more[hidden]{opacity:0;transition:all var(--transition-base)}.load-more:hover{background:var(--pink-0);transform:translateY(-2px)}.load-more:focus-visible{outline:2px solid var(--pink-0);outline-offset:2px}.feed-filters:not(:has(details)){display:flex;flex-direction:column;position:relative}.feed-filters:not(:has(details)) .favourites-toggle{position:absolute;top:1.5rem;left:-3.5rem;z-index:10}@media (min-width:768px){.feed-filters:not(:has(details)) .favourites-toggle{right:0;left:auto}}.icon.colour{background:#ff0080;background:linear-gradient(180deg,rgba(255,0,128,1) 0,rgba(250,71,101,1) 14%,rgba(251,121,35,1) 28%,rgba(176,190,19,1) 42%,rgba(14,204,0,1) 56%,rgba(14,225,166,1) 70%,rgba(63,152,253,1) 84%,rgba(166,90,196,1) 100%);mask-image:var(--colour);-webkit-mask-image:var(--colour);-webkit-mask-repeat:no-repeat;-webkit-mask-size:contain;mask-repeat:no-repeat;mask-size:contain;width:1.25rem;height:1.25rem}.feed-item:focus,.feed-item:focus-visible,[role=button]:focus,[role=button]:focus-visible,a:focus,a:focus-visible,button:focus,button:focus-visible,input:focus,input:focus-visible,select:focus,select:focus-visible,textarea:focus,textarea:focus-visible{outline:2px solid #ff0080!important;outline-offset:2px!important;box-shadow:0 0 0 4px rgba(255,0,128,.2)!important}:focus:not(:focus-visible){outline:0!important;box-shadow:none!important}.skip-to-content{background:#ff0080;color:#fff;height:auto;left:50%;padding:8px;position:absolute;transform:translateY(-100%) translateX(-50%);transition:transform .3s;width:auto;z-index:100}.skip-to-content:focus{transform:translateY(0) translateX(-50%)}[aria-busy=true]{cursor:progress}[aria-disabled=true],[disabled]{cursor:not-allowed;opacity:.7}@media (forced-colors:active){.feed-item{border:1px solid CanvasText}[role=button],button{border:1px solid ButtonText}.favourite-button.favourited{background-color:Highlight;color:HighlightText}}@media (prefers-reduced-motion:reduce){*,::after,::before{animation-duration:0s!important;animation-iteration-count:1!important;transition-duration:0s!important;scroll-behavior:auto!important}.feed-overlay-content,.gallery-modal,.loading-dots{animation:none!important;transition:none!important}.feed-item{transition:none!important}}.feed-item[tabindex="0"]{cursor:pointer;position:relative}.feed-item[tabindex="0"]::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;border:2px solid transparent;transition:border-color .2s ease}.feed-item[tabindex="0"]:focus::after{border-color:#ff0080}.feed-item.highlighted{box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1);animation:highlight-pulse 2s ease-in-out}@keyframes highlight-pulse{0%,100%{box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}50%{box-shadow:0 0 0 8px #ff0080,0 12px 24px rgba(0,0,0,.15)}}.error-state{padding:2rem;border:1px solid #ff0080;border-radius:.5rem;margin:2rem 0;text-align:center}.error-state h3{color:#ff0080;margin-top:0}.error-state button{margin-top:1rem}.error-feedback-modal{padding:2rem;border:2px solid #ff0080;border-radius:.5rem;max-width:500px;width:100%}.error-feedback-modal h2{margin-top:0;color:#ff0080}.error-feedback-modal textarea{width:100%;min-height:100px;margin:1rem 0;padding:.5rem;border:1px solid #ccc;border-radius:.25rem}.error-feedback-modal .actions{display:flex;justify-content:flex-end;gap:1rem}.error-feedback-modal button{padding:.5rem 1rem;border:1px solid #ccc;border-radius:.25rem;background:#f5f5f5;cursor:pointer}.error-feedback-modal button.primary{background:#ff0080;color:#fff;border-color:#ff0080}dialog::backdrop{background-color:rgba(0,0,0,.5)}dialog.filter-dropdown{max-height:80vh;overflow:auto}dialog.filter-dropdown .cancel{position:sticky;top:0;z-index:1}.term-divider{position:relative;text-align:center;margin:1rem 0;border-bottom:1px solid var(--base-200)}.term-divider span{background:var(--base);padding:0 1rem;color:var(--contrast);font-size:.9rem;position:relative;top:.5em}.common-term{background:var(--base-50);border-radius:var(--innerRadius)}.loading-indicator{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:1rem;color:var(--contrast-100);font-size:.9rem}.loading-indicator svg{animation:spin 1s linear infinite}.pagination-info{text-align:center;padding:.5rem;font-size:.9rem;color:var(--contrast-100);border-top:1px solid var(--base-100)}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.term-breadcrumb{margin-bottom:1rem;padding:.5rem;background:var(--base-50);border-radius:4px}.back-to-parent{display:flex;align-items:center;gap:.5rem;border:none;background:0 0;color:var(--contrast);cursor:pointer;padding:.5rem;border-radius:4px;font-size:var(--small)}.back-to-parent:hover{background:var(--base-100)}.term-row{display:flex;align-items:center;gap:.5rem;width:100%;padding:.25rem 0}.toggle-children{border:none;background:0 0;padding:.25rem;cursor:pointer;color:var(--contrast);display:flex;align-items:center;justify-content:center;margin-left:auto;border-radius:4px}.toggle-children:hover{background:var(--base-50)}.loading-indicator{display:flex;align-items:center;justify-content:center;width:24px;height:24px}.loading-indicator .loading{width:16px;height:16px;border:2px solid var(--base-100);border-top-color:var(--contrast);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.term-breadcrumb{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem;padding:.5rem;background:var(--base-50);border-radius:4px}.term-breadcrumb .path{display:flex;align-items:center;gap:.25rem;flex-wrap:wrap}.term-breadcrumb button{border:none;background:0 0;padding:.25rem .5rem;border-radius:4px;cursor:pointer;color:var(--contrast);font-size:var(--small)}.term-breadcrumb button:hover{background:var(--base-100)}.path-separator{color:var(--contrast-50)}.path-level{white-space:nowrap}.create-term-section{margin-top:2rem;padding-top:1rem;border-top:1px solid var(--base-100)}.suggestion-prompt{font-size:var(--small);color:var(--contrast-50);margin-bottom:1rem}.create-term-form{display:flex;flex-direction:column;gap:.5rem}.form-row{display:flex;align-items:center;gap:.5rem}.name-row{position:relative}.name-row input{width:100%;padding:.5rem;border:2px solid var(--base-100);border-radius:4px;background:var(--base);color:var(--contrast)}.name-row input:focus{border-color:var(--pink-0);outline:0}.parent-row{font-size:var(--small)}.parent-row label{display:flex;align-items:center;gap:.5rem;cursor:pointer}dialog[open].gallery-modal{width:calc(100vw - var(--padding) * 2);height:99vh;background:var(--base);display:flex;align-items:center;justify-content:center}.gallery-content{position:relative;max-width:100%;max-height:100%;display:flex;align-items:center;justify-content:center;padding:2rem}.gallery-favourite .favourite-button{top:unset;bottom:1rem;right:1rem}.gallery-image{max-width:100%;max-height:calc(100vh - 4rem);object-fit:contain}.gallery-close{position:absolute;top:1rem;right:1rem;background:0 0;border:none;color:#fff;cursor:pointer;padding:.5rem;z-index:10;transition:color .3s ease}.gallery-close:hover{color:#ff0080}.gallery-nav{position:absolute;top:50%;height:50%;z-index:5;transform:translateY(-50%);border:none;color:var(--contrast);cursor:pointer;padding:1rem;transition:color .3s ease;display:flex;justify-content:center;align-items:center}.gallery-nav:hover{background-color:var(--overlay-heavy)}.gallery-nav:hover{color:#ff0080}.gallery-prev{left:1rem}.gallery-next{right:1rem}.gallery-counter{position:absolute;top:1rem;left:1rem;color:#fff;font-size:.875rem}.gallery-content details{position:absolute;bottom:1rem;left:2rem;width:calc(100% - 4rem);background-color:var(--overlay-light);padding:0}.gallery-content details:hover,.gallery-content details[open]{background-color:var(--overlay-heavy);backdrop-filter:blur(5px)}
+.feed-block{max-width:var(--full);margin:0 auto}.feed-block>:not(.feed-grid,h2){max-width:var(--alignWide);margin:1rem var(--mr) 1rem var(--ml)}.feed-block>h2{max-width:var(--content)}.feed-block[data-loading=true]{opacity:.7}.feed-block:empty::before{content:"Looks like there's nothing here yet.";display:block;text-align:center;padding:2rem}.feed-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.5rem;margin-bottom:2rem}@media (min-width:768px){.feed-grid{grid-template-columns:repeat(4,1fr);gap:1rem}.feed-empty-state{grid-column:2/span 2!important}}@media (min-width:1200px){.feed-grid{grid-template-columns:repeat(6,1fr)}.feed-empty-state{grid-column:2/span 4!important}}.feed-item{position:relative;border-radius:.5rem;overflow:hidden;background:var(--base-50);box-shadow:0 2px 4px rgba(0,0,0,.1);opacity:0;transition:opacity var(--transition-base) var(--delay);height:fit-content;padding:0}.feed-item[data-loaded]{opacity:1}.feed-item[data-loaded]+.feed-item[data-loaded]{--delay:var(--delay) + var(--increase)}.feed-item.artist{grid-column:span 2}.feed-item.highlighted{animation:highlight 2s ease-out}.feed-image{display:block;aspect-ratio:1;overflow:hidden;width:100%;height:100%}.artist-tattoos img,.feed-image img{width:100%;height:100%;object-fit:cover;transition:transform var(--timing) var(--function)}.artist-tattoos a:hover img,.feed-image:hover img{transform:scale(1.05)}.item-info{padding:.25rem 1rem}.item-info h3{margin:0!important;font-size:1.1rem;font-family:var(--body);font-weight:var(--bWeight);text-align:center}.item-info span{text-transform:uppercase;display:flex;align-items:center}.item-info .icon{margin-right:1em}.taxonomy-lists{margin:.5rem 0}.taxonomy-group{display:flex;flex-direction:column;align-items:flex-start;gap:.5rem;margin-bottom:.25rem}.taxonomy-group ul{list-style:none;margin:0;padding:0}.item-labels{margin-top:.5rem;display:flex;flex-wrap:wrap;gap:.5rem}.label{display:flex;align-items:center;gap:.25rem;font-size:.9rem}.label a{color:inherit;text-decoration:none}.label a:hover{color:var(--pink-0)}.favourite-button{position:absolute;top:.5rem;right:.5rem;z-index:10;background:var(--overlay-medium);border-radius:50%;box-shadow:var(--subtle);border:none;cursor:pointer;width:2rem;height:2rem;display:flex;justify-content:center;align-items:center;backdrop-filter:blur(5px);transition:all var(--transition-base)}.favourite-button:hover{transform:scale(1.1);color:var(--pink-0);background:var(--base);box-shadow:0 4px 8px rgba(0,0,0,.15)}.favourite-button.favourited{animation:favourite-pop .4s cubic-bezier(.25,.46,.45,.94)}.feed-filters{margin:2rem auto;position:relative}.feed-filters .feed-controls{display:flex;justify-content:space-between;align-items:center;gap:2rem;width:100%}.feed-filters details summary{justify-content:flex-start;padding:2rem .5rem .5rem}.feed-filters details[open] summary{background-color:var(--base-50)}.feed-filters summary:after{position:absolute;right:.5rem;top:.5rem}.feed-filters .filter-toggle,.feed-filters .type-filter>label,.radio-group-label>label{display:flex;justify-content:center;align-items:center;padding:.35rem;white-space:nowrap;width:fit-content;height:fit-content;cursor:pointer;border:1px solid var(--base-200);border-radius:4px;font-size:.875rem;transition:border-color var(--transition-base);margin-bottom:.5rem}.filter-toggle .icon{margin-right:.5rem}.type-filter:hover{color:var(--pink-0);border-color:var(--pink-0);transition:var(--transition-color)}.feed-filters .type-filter>label{flex-direction:column}.type-filter.favourites-toggle{margin-left:auto}.type-filter.favourites-toggle label{position:relative}.type-filter.favourites-toggle label .label{top:100%;right:0}input[hidden]+label{display:none}.feed-filters svg{width:25px;height:25px}.order-options{position:relative;display:flex;justify-content:space-between}.order-options .order-by{display:flex}.order-options .order-by .radio-group-label,.order-options .order-direction{display:flex;padding-top:1.5rem;position:relative}.order-options .order-by>.label{margin-right:2rem}.radio-group-label{display:flex;gap:.5rem}.feed-filters .radio-group-label label .label{top:.5rem;right:.5rem}.feed-filters .order-options label svg{width:20px;height:20px}.feed-filters input:checked+label,.feed-filters label:hover,.radio-group-label input:checked+label{background-color:var(--white);border-color:var(--pink);color:var(--pink)}.feed-filters label .label{position:absolute;visibility:hidden;top:.5rem;right:4rem;opacity:0;transition:transform var(--timing) var(--function);transition-property:max-width,transform}.feed-filters input:checked+label .label{visibility:visible;opacity:1}.feed-filters .filters{padding:1rem;margin-top:1rem;background-color:transparent}.has-filters.filters{background-color:var(--base-50)}.filter-group{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.25rem;position:relative}.feed-overlay{display:none;opacity:0;visibility:hidden}.loading .feed-overlay{position:fixed;top:0;left:0;right:0;bottom:0;margin:0!important;max-width:none!important;width:100%;height:100%;background:var(--overlay-medium);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);display:flex;justify-content:center;align-items:center;z-index:999999;opacity:1;visibility:visible;transition:opacity .3s ease,visibility .3s ease}.feed-overlay-content{background:var(--base);padding:2rem;border-radius:1rem;box-shadow:var(--shadow);text-align:center;width:min(400px,60vw)}.loading .loading-icon-container{position:relative;margin-bottom:1.5rem;animation:dance 1s ease-in-out infinite;transition:opacity .2s ease;will-change:transform,opacity}.loading .loading-message .icon{width:3em;height:3em}.loading .loading-message .icon svg{width:100%;height:100%;margin-right:1rem;animation:dance 2s ease-in-out infinite;transition:color .3s ease}.loading .loading-message{will-change:opacity;font-size:1rem;color:#666;text-align:center;min-height:24px;transition:opacity .2s ease;margin-bottom:1rem}.loading .loading-dots{color:var(--pink-0);width:4px;aspect-ratio:1;border-radius:50%;box-shadow:19px 0 0 7px,38px 0 0 3px,57px 0 0 0;transform:translateX(-38px) scale(.666);animation:bubble .5s infinite alternate linear}.feed-empty-state{grid-column-start:1;grid-column-end:2;text-align:center;padding:2rem;background:var(--base);border-radius:1rem;margin:0 auto;max-width:600px}.feed-empty-state h3{text-align:center;font-family:var(--heading);font-size:clamp(1.5rem,3vw,2.5rem);margin:0 0 2rem 0;color:var(--pink-0)}.feed-empty-state p{font-family:var(--body);margin:1rem 0;font-size:clamp(1rem,2vw,1.2rem);line-height:1.4}.feed-empty-state p:last-child{color:var(--pink-0);margin-top:2rem}@keyframes highlight{0%,100%{box-shadow:none}50%{box-shadow:0 0 0 4px var(--pink-0)}}@keyframes favourite-pop{0%{transform:scale(1)}50%{transform:scale(1.3)}75%{transform:scale(.9)}100%{transform:scale(1)}}@keyframes bubble{50%{box-shadow:19px 0 0 3px,38px 0 0 7px,57px 0 0 3px}100%{box-shadow:19px 0 0 0,38px 0 0 3px,57px 0 0 7px}}@keyframes dance{0%,100%{transform:rotate(-5deg) scale(1)}50%{transform:rotate(5deg) scale(1.1)}}.artist-tattoos{display:grid;grid-template-columns:repeat(3,1fr);gap:.25em}.artist-tattoos a:has(img){overflow:hidden;background-color:var(--base-100)}.artist-tattoos a:not(.feed-image) img{width:100%;height:100%;object-fit:cover}.artist-tattoos a::after,.artist-tattoos a::before{display:none}.artist-tattoos .feed-image{grid-row:span 2;grid-column:span 2}.feed-item summary .handle{position:absolute;bottom:0;left:0;right:0;background-color:var(--overlay-light);backdrop-filter:blur(5px);border-radius:var(--innerRadius);z-index:1;padding:.25rem .25rem .25rem 1.1rem}.feed-item:hover summary .handle,.feed-item[open] summary .handle{background-color:var(--overlay-pink-medium);backdrop-filter:blur(5px)}.feed-item summary:after{z-index:11;position:absolute;bottom:.35rem;right:.7rem;width:1.5rem;height:1.5rem;cursor:pointer}.loading .feed-overlay h2{width:fit-content;margin:1rem auto!important;color:transparent;-webkit-text-stroke:1px var(--contrast);--g:conic-gradient(var(--pink-0) 0 0) no-repeat text;background:var(--g) 0,var(--g) 1ch,var(--g) 2ch,var(--g) 3ch,var(--g) 4ch,var(--g) 5ch,var(--g) 6ch;animation:l17-0 1s linear infinite alternate,l17-1 2s linear infinite}@keyframes l17-0{0%{background-size:1ch 0}100%{background-size:1ch 100%}}@keyframes l17-1{0%,50%{background-position-y:100%,0}50.01%,to{background-position-y:0,100%}}.loading .loading-message{display:flex;justify-content:center;align-items:center;overflow:hidden}.loading .dots-wrapper{display:flex;justify-content:center;align-items:center}.loading .loading-message p{opacity:1;transform:scaleY(1);transform-origin:bottom;transition:opacity var(--transition-base),transform var(--transition-base)}.loading .changing .loading-message p{opacity:0;transform:scaleY(0);transform-origin:top}.loading .feed-overlay::after{content:'';position:absolute;z-index:-1;inset:0;background:linear-gradient(90deg,var(--shimmer));animation:shimmer 3s ease-in-out infinite}@keyframes shimmer{0%{transform:translateX(-100%)}100%,50%{transform:translateX(100%)}}@media (max-width:768px){.feed-filters .feed-controls{flex-direction:column;gap:1rem}.feed-empty-state{grid-column-end:none;padding:2rem 1rem;margin:1rem}.feed-filters details summary{gap:.5rem;justify-content:flex-start}}[hidden],[hidden]+label{display:none}.feed-loader{display:flex;flex-direction:column;align-items:center;gap:1rem;margin:2rem auto 0!important}.load-more{opacity:1;display:flex;align-items:center;gap:.5rem;padding:.75rem 1.5rem;background:var(--base-200);color:var(--contrast-200);border:none;border-radius:4px;font-size:var(--medium);cursor:pointer;transition:all var(--transition-base)}.load-more[hidden]{opacity:0;transition:all var(--transition-base)}.load-more:hover{background:var(--pink-0);transform:translateY(-2px)}.load-more:focus-visible{outline:2px solid var(--pink-0);outline-offset:2px}.feed-filters:not(:has(details)){display:flex;flex-direction:column;position:relative}.feed-filters:not(:has(details)) .favourites-toggle{position:absolute;top:1.5rem;left:-3.5rem;z-index:10}@media (min-width:768px){.feed-filters:not(:has(details)) .favourites-toggle{right:0;left:auto}}.icon.colour{background:#ff0080;background:linear-gradient(180deg,rgba(255,0,128,1) 0,rgba(250,71,101,1) 14%,rgba(251,121,35,1) 28%,rgba(176,190,19,1) 42%,rgba(14,204,0,1) 56%,rgba(14,225,166,1) 70%,rgba(63,152,253,1) 84%,rgba(166,90,196,1) 100%);mask-image:var(--colour);-webkit-mask-image:var(--colour);-webkit-mask-repeat:no-repeat;-webkit-mask-size:contain;mask-repeat:no-repeat;mask-size:contain;width:1.25rem;height:1.25rem}.feed-item:focus,.feed-item:focus-visible,[role=button]:focus,[role=button]:focus-visible,a:focus,a:focus-visible,button:focus,button:focus-visible,input:focus,input:focus-visible,select:focus,select:focus-visible,textarea:focus,textarea:focus-visible{outline:2px solid #ff0080!important;outline-offset:2px!important;box-shadow:0 0 0 4px rgba(255,0,128,.2)!important}:focus:not(:focus-visible){outline:0!important;box-shadow:none!important}.skip-to-content{background:#ff0080;color:#fff;height:auto;left:50%;padding:8px;position:absolute;transform:translateY(-100%) translateX(-50%);transition:transform .3s;width:auto;z-index:100}.skip-to-content:focus{transform:translateY(0) translateX(-50%)}[aria-busy=true]{cursor:progress}[aria-disabled=true],[disabled]{cursor:not-allowed;opacity:.7}@media (forced-colors:active){.feed-item{border:1px solid CanvasText}[role=button],button{border:1px solid ButtonText}.favourite-button.favourited{background-color:Highlight;color:HighlightText}}@media (prefers-reduced-motion:reduce){*,::after,::before{animation-duration:0s!important;animation-iteration-count:1!important;transition-duration:0s!important;scroll-behavior:auto!important}.feed-overlay-content,.gallery-modal,.loading-dots{animation:none!important;transition:none!important}.feed-item{transition:none!important}}.feed-item[tabindex="0"]{cursor:pointer;position:relative}.feed-item[tabindex="0"]::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;border:2px solid transparent;transition:border-color .2s ease}.feed-item[tabindex="0"]:focus::after{border-color:#ff0080}.feed-item.highlighted{box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1);animation:highlight-pulse 2s ease-in-out}@keyframes highlight-pulse{0%,100%{box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}50%{box-shadow:0 0 0 8px #ff0080,0 12px 24px rgba(0,0,0,.15)}}.error-state{padding:2rem;border:1px solid #ff0080;border-radius:.5rem;margin:2rem 0;text-align:center}.error-state h3{color:#ff0080;margin-top:0}.error-state button{margin-top:1rem}.error-feedback-modal{padding:2rem;border:2px solid #ff0080;border-radius:.5rem;max-width:500px;width:100%}.error-feedback-modal h2{margin-top:0;color:#ff0080}.error-feedback-modal textarea{width:100%;min-height:100px;margin:1rem 0;padding:.5rem;border:1px solid #ccc;border-radius:.25rem}.error-feedback-modal .actions{display:flex;justify-content:flex-end;gap:1rem}.error-feedback-modal button{padding:.5rem 1rem;border:1px solid #ccc;border-radius:.25rem;background:#f5f5f5;cursor:pointer}.error-feedback-modal button.primary{background:#ff0080;color:#fff;border-color:#ff0080}dialog::backdrop{background-color:rgba(0,0,0,.5)}dialog.filter-dropdown{max-height:80vh;overflow:auto}dialog.filter-dropdown .cancel{position:sticky;top:0;z-index:1}.term-divider{position:relative;text-align:center;margin:1rem 0;border-bottom:1px solid var(--base-200)}.term-divider span{background:var(--base);padding:0 1rem;color:var(--contrast);font-size:.9rem;position:relative;top:.5em}.common-term{background:var(--base-50);border-radius:var(--innerRadius)}.loading-indicator{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:1rem;color:var(--contrast-100);font-size:.9rem}.loading-indicator svg{animation:spin 1s linear infinite}.pagination-info{text-align:center;padding:.5rem;font-size:.9rem;color:var(--contrast-100);border-top:1px solid var(--base-100)}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.term-breadcrumb{margin-bottom:1rem;padding:.5rem;background:var(--base-50);border-radius:4px}.back-to-parent{display:flex;align-items:center;gap:.5rem;border:none;background:0 0;color:var(--contrast);cursor:pointer;padding:.5rem;border-radius:4px;font-size:var(--small)}.back-to-parent:hover{background:var(--base-100)}.term-row{display:flex;align-items:center;gap:.5rem;width:100%;padding:.25rem 0}.toggle-children{border:none;background:0 0;padding:.25rem;cursor:pointer;color:var(--contrast);display:flex;align-items:center;justify-content:center;margin-left:auto;border-radius:4px}.toggle-children:hover{background:var(--base-50)}.loading-indicator{display:flex;align-items:center;justify-content:center;width:24px;height:24px}.loading-indicator .loading{width:16px;height:16px;border:2px solid var(--base-100);border-top-color:var(--contrast);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.term-breadcrumb{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem;padding:.5rem;background:var(--base-50);border-radius:4px}.term-breadcrumb .path{display:flex;align-items:center;gap:.25rem;flex-wrap:wrap}.term-breadcrumb button{border:none;background:0 0;padding:.25rem .5rem;border-radius:4px;cursor:pointer;color:var(--contrast);font-size:var(--small)}.term-breadcrumb button:hover{background:var(--base-100)}.path-separator{color:var(--contrast-50)}.path-level{white-space:nowrap}.create-term-section{margin-top:2rem;padding-top:1rem;border-top:1px solid var(--base-100)}.suggestion-prompt{font-size:var(--small);color:var(--contrast-50);margin-bottom:1rem}.create-term-form{display:flex;flex-direction:column;gap:.5rem}.form-row{display:flex;align-items:center;gap:.5rem}.name-row{position:relative}.name-row input{width:100%;padding:.5rem;border:2px solid var(--base-100);border-radius:4px;background:var(--base);color:var(--contrast)}.name-row input:focus{border-color:var(--pink-0);outline:0}.parent-row{font-size:var(--small)}.parent-row label{display:flex;align-items:center;gap:.5rem;cursor:pointer}dialog[open].gallery-modal{width:calc(100vw - var(--padding) * 2);height:99vh;background:var(--base);display:flex;align-items:center;justify-content:center}.gallery-content{position:relative;max-width:100%;max-height:100%;display:flex;align-items:center;justify-content:center;padding:2rem}.gallery-favourite .favourite-button{top:unset;bottom:1rem;right:1rem}.gallery-image{max-width:100%;max-height:calc(100vh - 4rem);object-fit:contain}.gallery-close{position:absolute;top:1rem;right:1rem;background:0 0;border:none;color:#fff;cursor:pointer;padding:.5rem;z-index:10;transition:color .3s ease}.gallery-close:hover{color:#ff0080}.gallery-nav{position:absolute;top:50%;height:50%;z-index:5;transform:translateY(-50%);border:none;color:var(--contrast);cursor:pointer;padding:1rem;transition:color .3s ease;display:flex;justify-content:center;align-items:center}.gallery-nav:hover{background-color:var(--overlay-heavy)}.gallery-nav:hover{color:#ff0080}.gallery-prev{left:1rem}.gallery-next{right:1rem}.gallery-counter{position:absolute;top:1rem;left:1rem;color:#fff;font-size:.875rem}.gallery-content details{position:absolute;bottom:1rem;left:2rem;width:calc(100% - 4rem);background-color:var(--overlay-light);padding:0}.gallery-content details:hover,.gallery-content details[open]{background-color:var(--overlay-heavy);backdrop-filter:blur(5px)}
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index 031ce4c..d59c952 100644
--- a/assets/css/forms.min.css
+++ b/assets/css/forms.min.css
@@ -1 +1 @@
-details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group{margin-bottom:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.preview summary span{display:none}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--innerRadius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.upload-group .selected .field{margin:0}.upload-group .group-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{order:-1;grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--innerRadius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--small)}form nav.tabs button h2{font-size:var(--small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{--height:fit-content;padding:.5rem}.success-message{color:var(--success,#16a34a);background-color:var(--success-bg,#f0fdf4);border:1px solid var(--success,#16a34a);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--success-bg,#f0fdf4);border:2px solid var(--success,#16a34a);padding:1.5rem;border-radius:var(--outerRadius);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success,#16a34a);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error,#dc2626)}.error-message{color:var(--error,#dc2626);font-size:var(--small);margin-top:.25rem;display:block}.form-error{background-color:var(--error-bg,#fee);border:1px solid var(--error,#dc2626);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success,#16a34a)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:var(--shadow)}
\ No newline at end of file
+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-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--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{--height:fit-content;padding:.5rem}.success-message{color:var(--success,#16a34a);background-color:var(--success-bg,#f0fdf4);border:1px solid var(--success,#16a34a);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--success-bg,#f0fdf4);border:2px solid var(--success,#16a34a);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success,#16a34a);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error,#dc2626)}.error-message{color:var(--error,#dc2626);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--error-bg,#fee);border:1px solid var(--error,#dc2626);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success,#16a34a)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow: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/nav.min.css b/assets/css/nav.min.css
index c3ce512..e9f2124 100644
--- a/assets/css/nav.min.css
+++ b/assets/css/nav.min.css
@@ -1 +1 @@
-nav{--py:.25rem;--px:1rem;max-width:100%;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}ul.socials{--w:1.2em;--height:fit-content;gap:.5rem;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;flex-direction:row;overflow:auto hidden;touch-action:pan-x;list-style:none}nav:not(:has(>ul)),nav>ul{--justify:flex-start;--align:center;--wrap:nowrap;--w:1em;--dir:row;position:relative;overflow:auto hidden;touch-action:pan-x}nav a{padding:0 var(--px);white-space:nowrap;text-transform:uppercase}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav a:hover:visited{background-color:var(--action-0);color:var(--action-contrast);transition:background-color var(--transition-base),color var(--transition-base)}nav ol,nav ul{list-style:none;margin:0;padding:0}.has-submenu button:hover,nav a:hover{background-color:var(--action-0);color:var(--action-contrast)}.has-submenu button{height:var(--height);width:var(--height);padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}.toggle .icon{transform:rotate(0);transition:transform var(--timing) var(--function);transition-property:transform,background-color,color}.has-submenu.open>button:not(.notifications,.quick-help) .icon,.has-submenu:hover>button:not(.notifications,.quick-help) .icon{transform:rotate(900deg)}ul.submenu{--dir:column;--wrap:nowrap;--gap:0;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max-content;min-width:100%;background-color:var(--overlay-light);border:2px solid var(--overlay-light);transition:all var(--timing) var(--function);box-shadow:var(--shadow-none)}.always ul.submenu{position:relative;top:0;left:0}.submenu li{background-color:var(--overlay-heavy);border:1px solid var(--base-50)}.submenu li:hover{--c:var(--action-rgb);background-color:var(--overlay-heavy)}.submenu a:hover{background-color:transparent}.wp-site-blocks>header ul.submenu{right:0;left:auto}.has-submenu.open>ul.submenu,.has-submenu:hover>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:var(--shadow)}nav.fixed.bottom,nav.on-this-page{--dir:row;--gap:0;width:calc(100% - var(--height));left:0;bottom:0;position:fixed;box-shadow:var(--shadow);z-index:var(--zz-top)}nav.fixed.bottom ul{width:100%;--justify:space-between;background-color:var(--base);padding:0 .25rem}nav.fixed a,nav.fixed li{--justify:center;width:100%}nav.fixed.bottom a,nav.fixed.bottom a:visited{color:var(--contrast);font-size:var(--small);padding:0}@media (min-width:768px){nav.fixed.bottom a,nav.fixed.bottom a:visited{font-size:var(--medium)}}nav.fixed.bottom a:focus,nav.fixed.bottom a:focus:visited,nav.fixed.bottom a:hover,nav.fixed.bottom a:hover:visited{color:var(--action-contrast)}.fixed.bottom li{flex:1}nav.always a{padding:0;--justify:center}nav.always .socials{width:100%}nav.always .socials li{width:100%}nav.always li{gap:0;--justify:flex-start}nav.always>ul>li>a{width:80%}nav.always .submenu{width:80%;min-width:80%;box-shadow:none!important;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}nav.always .submenu li{background-color:var(--overlay-light)}nav.always .has-submenu>a,nav.fixed .has-submenu>a{width:80%}.has-submenu>button{width:20%}nav#breadcrumbs{--height:1.5em;--w:20px;width:fit-content;max-width:var(--full);position:absolute;background-color:var(--overlay-medium);font-size:var(--small);padding:.125em;overflow:visible;--gap:0}nav#breadcrumbs li+li::before{content:'/';color:var(--contrast-200)}nav#breadcrumbs li:last-of-type{margin-right:.5em}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;white-space:nowrap;height:2em;color:var(--contrast);text-transform:none;width:max-content}nav#breadcrumbs span{display:flex;align-items:center;padding-left:.5em}nav#breadcrumbs a:focus,nav#breadcrumbs a:focus:visited,nav#breadcrumbs a:hover,nav#breadcrumbs a:hover:visited{background-color:transparent;color:var(--action-0)}nav#breadcrumbs a:has(.icon){width:2rem}nav.always{z-index:var(--z-above);position:fixed;width:var(--height);bottom:0;right:0}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--offHeight);background-color:var(--overlay-heavy);backdrop-filter:blur(5px);justify-content:flex-end;flex-direction:column;z-index:var(--z-above)}nav.always>ul{--dir:column;--wrap:nowrap;--justify:flex-start;--align:center;--gap:0;position:relative;right:-300vw;padding:1rem 0 0;width:100vw;height:fit-content;max-height:100%;overflow:hidden auto;transition:right var(--transition-base)}nav.always.open>ul{right:0;transition:right var(--transition-base)}nav.always li{max-inline-size:none;width:100%;height:fit-content;--dir:row;--wrap:wrap}nav.always a{--py:1rem;width:100%;min-height:var(--height)}nav.always>button{position:fixed;bottom:0;right:0;width:var(--height);height:var(--height);border-radius:0;background-color:var(--base);color:var(--contrast);transition:width var(--timing) var(--function);transition-property:width,background-color;box-shadow:var(--shadow)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{--c:var(--action-rgb);z-index:1000000;width:100%;background-color:var(--overlay-heavy);color:var(--contrast);backdrop-filter:blur(5px)}nav.always.open>button:focus,nav.always.open>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button .icon-list,nav.always>button .icon-x{transform:scale(0);height:0;width:0;position:absolute}nav.always.open>button .icon-x,nav.always>button .icon-list{transform:scale(1);height:32px;width:32px}nav.always .has-submenu.open>.submenu,nav.always .has-submenu:hover>.submenu{height:max-content}nav.always .has-submenu:hover>a,nav.always .submenu>li>a:focus,nav.always .submenu>li>a:hover{background-color:var(--action-0);color:var(--action-contrast)}@media (min-width:768px){nav.always>ul{padding:var(--height) 0 0}}nav.on-this-page{--justify:space-between;max-width:none;z-index:var(--z-6);margin:0;padding:0 .5rem;background-color:var(--overlay-medium);color:var(--base-200)}body:has(nav.fixed) nav.on-this-page{bottom:var(--offHeight)}.on-this-page ul{--justify:flex-start;gap:0;width:100%}.on-this-page li:not(.has){padding:0}nav.letters li{width:100%;max-width:calc(7.69% - 2px)}.on-this-page .active a{--c:var(--action-rgb);background-color:var(--overlay-heavy);color:var(--action-contrast)}@media (min-width:768px){nav.letters li{max-width:none;width:fit-content}nav.letters a,nav.letters li:not(.has){padding:.25rem .66rem}}nav.index{--justify:flex-start;--px:0;background-color:var(--overlay-heavy)}.index ul{--justify:flex-start;width:fit-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--timing) var(--function)}.index li.active{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (min-width:768px){.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index .active a:hover,.index a:hover{background-color:var(--action-0);color:var(--action-contrast)}.index label{display:flex;color:var(--contrast);align-items:center;margin:0}.index label button{margin-left:1em}.index.open{--dir:column-reverse;height:calc(100% - 8rem);z-index:var(--z-above);width:100%;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);align-items:flex-end}.index.open label{max-width:90%;margin-top:1rem;margin-right:2rem}.index.open .toggle .icon{transform:rotate(45deg)}.index.open ul{--dir:column;--justify:flex-end;height:100%;max-width:100%;width:100%}.index.open li{background-color:transparent;max-width:100%!important;width:100%;height:var(--height);transform:scaleX(1);flex-shrink:1;overflow:visible}.index.open a{--justify:flex-end;background-color:transparent;padding:0 2rem 0 0}.condensed{--dir:row;--wrap:wrap;--height:1.2em;--py:.25rem;--px:.25rem;height:fit-content}.condensed>ul{--wrap:wrap;height:fit-content}.condensed ul{--justify:center;--gap:0}.condensed li{width:fit-content}.condensed li+li::before{content:'·';display:block;padding:0 .25em}nav.condensed a{text-transform:none;white-space:nowrap;border-bottom:2px solid transparent}.condensed a:focus,.condensed a:focus:visited,.condensed a:hover,.condensed a:hover:visited{border-color:var(--action-0)}.dashboard-nav{width:100%;--dir:row;--justify:flex-start;--wrap:nowrap}.wp-site-blocks>header,body>header{--dir:row;position:sticky;top:0;left:0;right:0;height:var(--height);width:100vw;display:flex;justify-content:space-between;align-items:center;padding:0 .5rem;background-color:var(--base);z-index:var(--zz-top);box-shadow:var(--shadow);border-bottom:1px solid var(--action-0)}.wp-site-blocks>header img{width:var(--height)}body>header{justify-content:space-between}header .title{--w:5em;margin:0;position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:flex-start;max-inline-size:none}.current-hours{position:sticky;top:var(--height);bottom:unset;width:unset;z-index:100;background-color:var(--action-0);color:var(--action-contrast);box-shadow:var(--shadow);padding:.25rem 1rem;display:flex;justify-content:space-between}.current-hours p{margin:0;display:flex;flex-wrap:wrap;flex:1}.current-hours p+p{justify-content:flex-end}.current-hours a{color:var(--action-contrast)}.current-hours a:hover{color:var(--action-200)}.current-hours b{margin-right:.25rem}.find-us{display:flex;align-items:center;gap:0 .5rem}.find-us a{display:flex;padding:.25rem 1rem;border:1px solid var(--action-contrast);border-radius:var(--innerRadius)}.find-us a:hover{background-color:var(--base);color:var(--contrast);border-color:var(--contrast)}nav.menu{--justify:flex-start}nav.menu a{padding:.5rem .66rem}nav.tabs{--gap:0;--wrap:nowrap;padding-bottom:2px;z-index:var(--z-6);position:fixed;bottom:var(--height);left:var(--doubleHeight);right:var(--doubleHeight)}nav.term-navigation:has([hidden]){display:none}ul.socials a{padding:.5rem}ul.socials a .icon{margin:0}nav.share{height:max-content;margin:1rem 0;--align:center}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--small)}nav.share .icon{margin-right:0}nav.share .button{position:relative;transition:top var(--transition-base),box-shadow var(--transition-base);top:0;box-shadow:var(--shadow-none)}nav.share .button:hover{top:-4px;box-shadow:var(--shadow-down)}
\ 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}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
diff --git a/assets/css/style.min.css b/assets/css/style.min.css
index 3081897..8411bf5 100644
--- a/assets/css/style.min.css
+++ b/assets/css/style.min.css
@@ -1 +1 @@
-:root{--narrow:min(500px, 50vw);--maxWidth:min(768px, 65vw);--alignWide:min(1024px, 90vw);--alignMed:min(962px, 82.5vw);--full:100vw;--mr:auto;--ml:auto;--mt:1rem;--mb:1rem;--setMargin:var(--mt) var(--mr) var(--mb) var(--ml);--insetMargin:var(--mt) calc((var(--maxWidth) - var(--narrow)) / 2 + var(--mr)) var(--mb) var(--ml);--height:4rem;--doubleHeight:8rem;--offHeight:5rem;--maxHeight:calc(100vh - var(--height) - var(--height));--gap:.5rem;--wrap:wrap;--justify:center;--align:center;--dir:row;--w:1.2em;--filter:grayscale(.3) sepia(.4);--font-base:-apple-system,BlinkMacSystemFont,avenir next,avenir,segoe ui,helvetica neue,helvetica,Cantarell,Ubuntu,roboto,noto,arial,sans-serif;--heading:'Aleo',var(--font-base);--body:'Josefin Slab',var(--font-base);--hWeight:900;--hlight:400;--bWeight:400;--bBold:700;--bLight:200;--enormous:calc(26vh - 4rem);--xxxlarge:clamp(2.5rem, 1.429rem + 2.857vw, 4rem);--xxlarge:clamp(2rem, 1.286rem + 1.905vw, 3rem);--xlarge:clamp(1.6rem, .957rem + 1.714vw, 2.5rem);--large:clamp(1.3rem, .6rem + 1.867vw, 2rem);--xmedium:clamp(1.4rem, .971rem + 1.143vw, 2rem);--medium:clamp(1.1rem, .993rem + .286vw, 1.25rem);--small:clamp(.95rem, .879rem + .19vw, 1.05rem);--extra-small:clamp(.75rem, 1.1337rem + -1.2278vw, .059375rem);--light-0:#fafafa;--light-50:#fcfbfb;--light-100:#f1eded;--light-200:#e6dfdf;--dark-0:#100404;--dark-50:#201212;--dark-100:#322423;--dark-200:#443635;--action-0:#B7332E;--action-50:#a32d29;--action-100:#8e2824;--action-200:#7a221f;--secondary-0:#E8A737;--secondary-50:#e59d20;--secondary-100:#d48f18;--secondary-200:#bd7f16;--success:#4CAF50;--warning:#E8A737;--error:#B7332E;--action-contrast:var(--light-0);--secondary-contrast:var(--light-0);--light-rgb:250,250,250;--dark-rgb:16,4,4;--action-rgb:183,51,46;--secondary-rgb:232,167,55;--rgba-subtle:rgba(var(--c),.5);--rgba-subtle-hover:rgba(var(--c),.1);--base:var(--light-0);--base-50:var(--light-50);--base-100:var(--light-100);--base-200:var(--light-200);--contrast:var(--dark-0);--contrast-50:var(--dark-50);--contrast-100:var(--dark-100);--contrast-200:var(--dark-200);--c:var(--light-rgb);--base-rgb:var(--light-rgb);--contrast-rgb:var(--dark-rgb);--z-1:5;--z-2:10;--z-3:15;--z-4:20;--z-5:50;--z-6:100;--z-top:999;--zz-top:999999;--rgb-light:.25;--rgb-medium:.66;--rgb-heavy:.85;--overlay-light:rgba(var(--c), .25);--overlay-medium:rgba(var(--c), .66);--overlay-heavy:rgba(var(--c), .85);--shimmer:rgba(var(--dark-rgb),0) 0%,rgba(var(--dark-rgb),.05) 50%,rgba(var(--dark-rgb),0) 100%;--shadow:rgba(var(--dark-rgb),.45) 0px 0px 4px;--shadow-down:rgba(var(--dark-rgb),.45) 0 6px 5px -5px;--shadow-right:rgba(var(--dark-rgb),.45) 6px 0 5px -5px;--shadow-left:rgba(var(--dark-rgb), .45) -6px 0 5px -5px;--shadow-up:rgba(var(--dark-rgb), .45) 0 -6px 5px -5px;--subtle:rgba(var(--dark-rgb), .45) 0px 25px 20px -20px;--subtleRight:rgba(var(--dark-rgb), .45) 10px 0 20px -20px;--shadow-none:transparent 0px 0px 0px;--innerRadius:4px;--outerPadding:1rem;--outerRadius:calc(var(--innerRadius) + var(--outerPadding));--function:cubic-bezier(.47,.24,.07,.47);--timing:.25s;--transition-base:var(--timing) var(--function);--transition-color:background-color var(--transition-base),color var(--transition-base),border var(--transition-base);--transition-transform:transform var(--transition-base);--transition-size:width var(--transition-base),height var(--transition-base),max-width var(--transition-base),max-height var(--transition-base);--offScreen:-200vw;--minus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23151515" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H88a4,4,0,0,1,0-8h80A4,4,0,0,1,172,128Z"></path></svg>');--plus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23151515" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H132v36a4,4,0,0,1-8,0V132H88a4,4,0,0,1,0-8h36V88a4,4,0,0,1,8,0v36h36A4,4,0,0,1,172,128Z"></path></svg>');--close:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4ZM162.83,98.83,133.66,128l29.17,29.17a4,4,0,0,1-5.66,5.66L128,133.66,98.83,162.83a4,4,0,0,1-5.66-5.66L122.34,128,93.17,98.83a4,4,0,0,1,5.66-5.66L128,122.34l29.17-29.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--chevron:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23151515" viewBox="0 0 256 256"><path d="M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm42.83-110.83a4,4,0,0,1,0,5.66l-40,40a4,4,0,0,1-5.66,0l-40-40a4,4,0,0,1,5.66-5.66L128,146.34l37.17-37.17A4,4,0,0,1,170.83,109.17Z"></path></svg>');--details:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M210.83,98.83l-80,80a4,4,0,0,1-5.66,0l-80-80a4,4,0,0,1,5.66-5.66L128,170.34l77.17-77.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--shop:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M28.15,95A3.81,3.81,0,0,0,28,96v16a36,36,0,0,0,16,29.92V216a4,4,0,0,0,4,4H208a4,4,0,0,0,4-4V141.92A36,36,0,0,0,228,112V96a3.81,3.81,0,0,0-.17-1.08L213.5,44.7A12,12,0,0,0,202,36H54A12,12,0,0,0,42.5,44.7Zm22-48.08A4,4,0,0,1,54,44H202a4,4,0,0,1,3.84,2.9L218.7,92H37.3ZM100,100h56v12a28,28,0,0,1-56,0ZM36,112V100H92v12a28,28,0,0,1-41.37,24.59,4,4,0,0,0-1.31-.76A28,28,0,0,1,36,112ZM204,212H52V145.94a36,36,0,0,0,44-17.48,36,36,0,0,0,64,0,36,36,0,0,0,44,17.48Zm2.68-76.17a3.94,3.94,0,0,0-1.3.76A28,28,0,0,1,164,112V100h56v12A28,28,0,0,1,206.68,135.83Z"></path></svg>');--style:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M224,92H170.61l9.33-51.28a4,4,0,1,0-7.88-1.44L162.48,92H106.61l9.33-51.28a4,4,0,1,0-7.88-1.44L98.48,92H48a4,4,0,0,0,0,8H97L86.84,156H32a4,4,0,0,0,0,8H85.39l-9.33,51.28a4,4,0,0,0,3.22,4.65A3.65,3.65,0,0,0,80,220a4,4,0,0,0,3.94-3.29L93.52,164h55.87l-9.33,51.28a4,4,0,0,0,3.22,4.65,3.65,3.65,0,0,0,.72.07,4,4,0,0,0,3.94-3.29L157.52,164H208a4,4,0,0,0,0-8H159l10.19-56H224a4,4,0,0,0,0-8Zm-73.16,64H95l10.19-56H161Z"></path></svg>');--map:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M128,68a36,36,0,1,0,36,36A36,36,0,0,0,128,68Zm0,64a28,28,0,1,1,28-28A28,28,0,0,1,128,132Zm0-112a84.09,84.09,0,0,0-84,84c0,30.42,14.17,62.79,41,93.62a250,250,0,0,0,40.73,37.66,4,4,0,0,0,4.58,0A250,250,0,0,0,171,197.62c26.81-30.83,41-63.2,41-93.62A84.09,84.09,0,0,0,128,20Zm37.1,172.23A254.62,254.62,0,0,1,128,227a254.62,254.62,0,0,1-37.1-34.81C73.15,171.8,52,139.9,52,104a76,76,0,0,1,152,0C204,139.9,182.85,171.8,165.1,192.23Z"></path></svg>');--theme:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M241.72,113a11.88,11.88,0,0,0-9.73-5H212V88a12,12,0,0,0-12-12H129.33l-28.8-21.6a12.05,12.05,0,0,0-7.2-2.4H40A12,12,0,0,0,28,64V208a4,4,0,0,0,4,4H211.09a4,4,0,0,0,3.79-2.74l28.49-85.47A11.86,11.86,0,0,0,241.72,113ZM40,60H93.33a4,4,0,0,1,2.4.8L125.6,83.2a4,4,0,0,0,2.4.8h72a4,4,0,0,1,4,4v20H69.76a12,12,0,0,0-11.38,8.21L36,183.35V64A4,4,0,0,1,40,60Zm195.78,61.26L208.2,204H37.55L66,118.74A4,4,0,0,1,69.76,116H232a4,4,0,0,1,3.79,5.26Z"></path></svg>');--arrow-up:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M236,192a4,4,0,0,1-4,4H88a4,4,0,0,1-4-4V57.66L42.83,98.83a4,4,0,0,1-5.66-5.66l48-48a4,4,0,0,1,5.66,0l48,48a4,4,0,0,1-5.66,5.66L92,57.66V188H232A4,4,0,0,1,236,192Z"></path></svg>');--colour:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75Zm9.85,105.59a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"></path></svg>');--linkIcon:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M236,88.12a50.44,50.44,0,0,1-14.81,34.31l-34.75,34.74A50.33,50.33,0,0,1,150.62,172h-.05A50.63,50.63,0,0,1,100,120a4,4,0,0,1,4-3.89h.11a4,4,0,0,1,3.89,4.11A42.64,42.64,0,0,0,150.58,164h0a42.32,42.32,0,0,0,30.14-12.49l34.75-34.74a42.63,42.63,0,1,0-60.29-60.28l-11,11a4,4,0,0,1-5.66-5.65l11-11A50.64,50.64,0,0,1,236,88.12ZM111.78,188.49l-11,11A42.33,42.33,0,0,1,70.6,212h0a42.63,42.63,0,0,1-30.11-72.77l34.75-34.74A42.63,42.63,0,0,1,148,135.82a4,4,0,0,0,8,.23A50.64,50.64,0,0,0,69.55,98.83L34.8,133.57A50.63,50.63,0,0,0,70.56,220h0a50.33,50.33,0,0,0,35.81-14.83l11-11a4,4,0,1,0-5.65-5.66Z"></path></svg>');--swipeRight:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iIzAwMDAwMCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMTIsMTQwdjM2YzAsMjQuNjYtOC4wOCw0MS4xLTguNDIsNDEuNzlhNCw0LDAsMSwxLTcuMTYtMy41OGMuMDctLjE1LDcuNTgtMTUuNTUsNy41OC0zOC4yMVYxNDBhMTYsMTYsMCwwLDAtMzIsMHY0YTQsNCwwLDAsMS04LDBWMTI0YTE2LDE2LDAsMCwwLTMyLDB2MTJhNCw0LDAsMCwxLTgsMFY2OGExNiwxNiwwLDAsMC0zMiwwVjE3NmE0LDQsMCwwLDEtNy4zOSwyLjExbC0xOC42OC0zMGEuNzUuNzUsMCwwLDEtLjA3LS4xMiwxNiwxNiwwLDAsMC0yNy43MiwxNmwyOS4zMSw1MGE0LDQsMCwwLDEtNi45LDRMMzEuMjIsMTY4YTI0LDI0LDAsMCwxLDQxLjUyLTI0LjA5TDg0LDE2MlY2OGEyNCwyNCwwLDAsMSw0OCwwdjM4LjEzYTI0LDI0LDAsMCwxLDM5Ljk0LDE2LjA2QTI0LDI0LDAsMCwxLDIxMiwxNDBabTM4LjgzLTg2LjgzLTMyLTMyYTQsNCwwLDAsMC01LjY2LDUuNjZMMjM4LjM0LDUySDE3NmE0LDQsMCwwLDAsMCw4aDYyLjM0TDIxMy4xNyw4NS4xN2E0LDQsMCwwLDAsNS42Niw1LjY2bDMyLTMyQTQsNCwwLDAsMCwyNTAuODMsNTMuMTdaIj48L3BhdGg+PC9zdmc+');--scrollbar-width:8px;--scrollbar-track-color:var(--base-100);--scrollbar-thumb-color:var(--action-0);--scrollbar-thumb-hover-color:var(--action-50);--scrollbar-thumb-border:2px solid var(--base-50);--scrollbar-border-radius:4px;--can-scroll:0}body:has(#theme-switcher:checked){--action-50:#cb3933;--action-100:#d14c47;--action-200:#d6605c;--secondary-50:#ebb14e;--secondary-100:#edbb65;--secondary-200:#f0c57c;--contrast:var(--light-0);--contrast-50:var(--light-50);--contrast-100:var(--light-100);--contrast-200:var(--light-200);--base:var(--dark-0);--base-50:var(--dark-50);--base-100:var(--dark-100);--base-200:var(--dark-200);--c:var(--dark-rgb);--base-rgb:var(--dark-rgb);--contrast-rgb:var(--light-rgb);--overlay-light:rgba(var(--c), .25);--overlay-medium:rgba(var(--c), .5);--overlay-heavy:rgba(var(--c), .85);--shimmer:rgba(var(--c),0) 0%,rgba(var(--c),.05) 50%,rgba(var(--c),0) 100%;--shadow:rgba(var(--light-rgb),.45) 0px 0px 4px;--shadow-down:rgba(var(--light-rgb),.45) 0 6px 5px -5px;--shadow-right:rgba(var(--light-rgb),.45) 6px 0 5px -5px;--shadow-left:rgba(var(--light-rgb), .45) -6px 0 5px -5px;--shadow-up:rgba(var(--light-rgb), .45) 0 -6px 5px -5px;--subtle:rgba(var(--light-rgb), .45) 0px 25px 20px -20px;--subtleRight:rgba(var(--light-rgb), .45) 10px 0 20px -20px;--minus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H88a4,4,0,0,1,0-8h80A4,4,0,0,1,172,128Z"></path></svg>');--plus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H132v36a4,4,0,0,1-8,0V132H88a4,4,0,0,1,0-8h36V88a4,4,0,0,1,8,0v36h36A4,4,0,0,1,172,128Z"></path></svg>');--close:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4ZM162.83,98.83,133.66,128l29.17,29.17a4,4,0,0,1-5.66,5.66L128,133.66,98.83,162.83a4,4,0,0,1-5.66-5.66L122.34,128,93.17,98.83a4,4,0,0,1,5.66-5.66L128,122.34l29.17-29.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--chevron:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm42.83-110.83a4,4,0,0,1,0,5.66l-40,40a4,4,0,0,1-5.66,0l-40-40a4,4,0,0,1,5.66-5.66L128,146.34l37.17-37.17A4,4,0,0,1,170.83,109.17Z"></path></svg>');--details:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M210.83,98.83l-80,80a4,4,0,0,1-5.66,0l-80-80a4,4,0,0,1,5.66-5.66L128,170.34l77.17-77.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--shop:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M28.15,95A3.81,3.81,0,0,0,28,96v16a36,36,0,0,0,16,29.92V216a4,4,0,0,0,4,4H208a4,4,0,0,0,4-4V141.92A36,36,0,0,0,228,112V96a3.81,3.81,0,0,0-.17-1.08L213.5,44.7A12,12,0,0,0,202,36H54A12,12,0,0,0,42.5,44.7Zm22-48.08A4,4,0,0,1,54,44H202a4,4,0,0,1,3.84,2.9L218.7,92H37.3ZM100,100h56v12a28,28,0,0,1-56,0ZM36,112V100H92v12a28,28,0,0,1-41.37,24.59,4,4,0,0,0-1.31-.76A28,28,0,0,1,36,112ZM204,212H52V145.94a36,36,0,0,0,44-17.48,36,36,0,0,0,64,0,36,36,0,0,0,44,17.48Zm2.68-76.17a3.94,3.94,0,0,0-1.3.76A28,28,0,0,1,164,112V100h56v12A28,28,0,0,1,206.68,135.83Z"></path></svg>');--style:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M224,92H170.61l9.33-51.28a4,4,0,1,0-7.88-1.44L162.48,92H106.61l9.33-51.28a4,4,0,1,0-7.88-1.44L98.48,92H48a4,4,0,0,0,0,8H97L86.84,156H32a4,4,0,0,0,0,8H85.39l-9.33,51.28a4,4,0,0,0,3.22,4.65A3.65,3.65,0,0,0,80,220a4,4,0,0,0,3.94-3.29L93.52,164h55.87l-9.33,51.28a4,4,0,0,0,3.22,4.65,3.65,3.65,0,0,0,.72.07,4,4,0,0,0,3.94-3.29L157.52,164H208a4,4,0,0,0,0-8H159l10.19-56H224a4,4,0,0,0,0-8Zm-73.16,64H95l10.19-56H161Z"></path></svg>');--map:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M128,68a36,36,0,1,0,36,36A36,36,0,0,0,128,68Zm0,64a28,28,0,1,1,28-28A28,28,0,0,1,128,132Zm0-112a84.09,84.09,0,0,0-84,84c0,30.42,14.17,62.79,41,93.62a250,250,0,0,0,40.73,37.66,4,4,0,0,0,4.58,0A250,250,0,0,0,171,197.62c26.81-30.83,41-63.2,41-93.62A84.09,84.09,0,0,0,128,20Zm37.1,172.23A254.62,254.62,0,0,1,128,227a254.62,254.62,0,0,1-37.1-34.81C73.15,171.8,52,139.9,52,104a76,76,0,0,1,152,0C204,139.9,182.85,171.8,165.1,192.23Z"></path></svg>');--theme:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M241.72,113a11.88,11.88,0,0,0-9.73-5H212V88a12,12,0,0,0-12-12H129.33l-28.8-21.6a12.05,12.05,0,0,0-7.2-2.4H40A12,12,0,0,0,28,64V208a4,4,0,0,0,4,4H211.09a4,4,0,0,0,3.79-2.74l28.49-85.47A11.86,11.86,0,0,0,241.72,113ZM40,60H93.33a4,4,0,0,1,2.4.8L125.6,83.2a4,4,0,0,0,2.4.8h72a4,4,0,0,1,4,4v20H69.76a12,12,0,0,0-11.38,8.21L36,183.35V64A4,4,0,0,1,40,60Zm195.78,61.26L208.2,204H37.55L66,118.74A4,4,0,0,1,69.76,116H232a4,4,0,0,1,3.79,5.26Z"></path></svg>');--arrow-up:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M236,192a4,4,0,0,1-4,4H88a4,4,0,0,1-4-4V57.66L42.83,98.83a4,4,0,0,1-5.66-5.66l48-48a4,4,0,0,1,5.66,0l48,48a4,4,0,0,1-5.66,5.66L92,57.66V188H232A4,4,0,0,1,236,192Z"></path></svg>');--colour:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75Zm9.85,105.59a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"></path></svg>');--linkIcon:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M236,88.12a50.44,50.44,0,0,1-14.81,34.31l-34.75,34.74A50.33,50.33,0,0,1,150.62,172h-.05A50.63,50.63,0,0,1,100,120a4,4,0,0,1,4-3.89h.11a4,4,0,0,1,3.89,4.11A42.64,42.64,0,0,0,150.58,164h0a42.32,42.32,0,0,0,30.14-12.49l34.75-34.74a42.63,42.63,0,1,0-60.29-60.28l-11,11a4,4,0,0,1-5.66-5.65l11-11A50.64,50.64,0,0,1,236,88.12ZM111.78,188.49l-11,11A42.33,42.33,0,0,1,70.6,212h0a42.63,42.63,0,0,1-30.11-72.77l34.75-34.74A42.63,42.63,0,0,1,148,135.82a4,4,0,0,0,8,.23A50.64,50.64,0,0,0,69.55,98.83L34.8,133.57A50.63,50.63,0,0,0,70.56,220h0a50.33,50.33,0,0,0,35.81-14.83l11-11a4,4,0,1,0-5.65-5.66Z"></path></svg>')}@font-face{font-display:swap;font-family:Aleo;font-style:normal;font-weight:400;src:url(fonts/aleo-v15-latin-regular.woff2) format('woff2'),url(fonts/aleo-v15-latin-regular.ttf) format('truetype')}@font-face{font-display:swap;font-family:Aleo;font-style:italic;font-weight:400;src:url(fonts/aleo-v15-latin-italic.woff2) format('woff2'),url(fonts/aleo-v15-latin-italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:Aleo;font-style:normal;font-weight:900;src:url(fonts/aleo-v15-latin-900.woff2) format('woff2'),url(fonts/aleo-v15-latin-900.ttf) format('truetype')}@font-face{font-display:swap;font-family:Aleo;font-style:italic;font-weight:900;src:url(fonts/aleo-v15-latin-900italic.woff2) format('woff2'),url(fonts/aleo-v15-latin-900italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:normal;font-weight:200;src:url(fonts/josefin-slab-v28-latin-200.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-200.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:italic;font-weight:200;src:url(fonts/josefin-slab-v28-latin-200italic.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-200italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:normal;font-weight:400;src:url(fonts/josefin-slab-v28-latin-regular.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-regular.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:italic;font-weight:400;src:url(fonts/josefin-slab-v28-latin-italic.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:normal;font-weight:700;src:url(fonts/josefin-slab-v28-latin-700.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-700.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:italic;font-weight:700;src:url(fonts/josefin-slab-v28-latin-700italic.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-700italic.ttf) format('truetype')}*{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb-color) var(--scrollbar-track-color)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track-color)}::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb-color);border-radius:var(--scrollbar-border-radius);border:var(--scrollbar-thumb-border)}::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover-color)}body{background-color:var(--base-50);color:var(--contrast);max-width:100vw;overflow-x:hidden;margin:0;font-family:var(--body);font-weight:var(--bWeight);font-size:var(--medium);line-height:1.4;position:relative}body b,body strong{font-weight:var(--bBold)}:target{scroll-snap-margin-top:max(6rem,20vh);scroll-margin-top:max(6rem,20vh);outline:double var(--action-0);border-radius:var(--outerRadius);padding:var(--outerPadding)}body.menu_item :target h2{background-color:var(--action-0);color:var(--action-contrast)}body,body *{transition:background-color var(--transition-base);transition-property:background-color,border}body.loading,body:has(aside.expanded),body:has(dialog[open]),body:has(nav.open){overflow:hidden}[hidden]{display:none!important}@media (max-width:767px){.hide-small{display:none}}.width-50{width:100%}.width-25{width:50%}.width-75{width:100%}.w-full{width:100%}@media (min-width:768px){.buttons li.width-50,.width-50{width:calc(50% - .3em)}.width-25{width:calc(25% - .3em)}.width-75{width:calc(75% - .3em)}}.col,.row:not(.icon){display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}.col{--dir:column}.row:not(.icon){--dir:row}.col.rev{--dir:column-reverse}.row.rev{--dir:row-reverse}.nowrap{--wrap:nowrap}.col.a-start,.row.start{--justify:flex-start}.col.a-end,.row.end{--justify:flex-end}.col.btw,.row.btw{--justify:space-between}.col.even,.row.even{--justify:space-evenly}.col.start,.row.a-start{--align:flex-start}.col.end,.row.a-end{--align:flex-end}.abs{position:absolute}:has(>.abs){position:relative}.hidden{transform:scale(0);max-width:0;max-height:0;overflow:hidden;transition:var(--transition-transform),var(--transition-size)}.visible{transform:scale(1);max-width:100%;max-height:100%;transition:var(--transition-transform),var(--transition-size)}.theme-switcher{position:absolute;opacity:0;width:0;height:0}#theme-switch{z-index:99;position:absolute;display:flex;align-items:center;justify-content:center}#theme-switch,.toggle-switch{--wrap:nowrap;cursor:pointer}#theme-switch,.toggle-switch input[type=checkbox]{--h:2rem;width:calc(var(--h) * 2);height:var(--h);margin:0 2rem 0 0;left:0;appearance:none;background:var(--base-200);border:1px solid var(--base-50);border-radius:var(--h);cursor:pointer;transition:all .3s ease;opacity:1}.toggle-switch input[type=checkbox]{position:relative}.toggle-switch{position:relative}@media (max-width:600px){#theme-switch{left:1rem}.wp-site-blocks>header{padding:0!important}}#theme-switch .icon{--w:1em;position:relative;top:0;margin:0 .25em;color:var(--contrast-200);z-index:2;transform:translateX(0)}#theme-switcher:checked~.moon,#theme-switcher:not(:checked)~.sun-dim{--w:1.5em;color:var(--contrast)}#theme-switcher:checked~.sun-dim,#theme-switcher:not(:checked)~.moon{top:-.17rem}#theme-switcher:not(:checked)~.sun-dim{color:var(--secondary-0);transform:translate(-2px,2px)}#theme-switcher:checked~.moon{transform:translate(4px,4px)}#theme-switch span,.toggle-switch input[type=checkbox]::before{--m:2px;content:"";position:absolute;top:var(--m);left:var(--m);width:calc(var(--h) - (var(--m) * 2));height:calc(var(--h) - var(--m) * 2);border:1px solid rgba(var(--contrast-rgb),.2);border-bottom:3px solid var(--contrast-200);background:var(--base-50);border-radius:50%;z-index:1;transform:rotate(360deg);transition:transform var(--transition-base),left var(--transition-base),top var(--transition-base),height var(--transition-base)}#theme-switch input:checked~span,.toggle-switch input[type=checkbox]:checked::before{left:calc(100% - (var(--h) - var(--m)));transform:rotate(-180deg);transition:transform var(--transition-base),left var(--transition-base)}.toggle-switch input[type=checkbox]:checked{background:var(--action-0)}.theme-switch:focus-visible+label{outline:2px solid var(--action-0);outline-offset:2px}#theme-switch .icon{transition:transform var(--transition-base),width var(--transition-base),height var(--transition-base),top var(--transition-base),color var(--transition-base)}#theme-switcher:checked~.icon.light,#theme-switcher:not(:checked)~.icon.dark{transform:rotate(360deg);color:var(--contrast-200)}#theme-switcher:checked~.icon.dark,#theme-switcher:not(:checked)~.icon.light{transform:rotate(-360deg);color:var(--contrast)}#theme-switch:hover span{background-color:var(--base-100)}#theme-switch:hover .icon{color:var(--action-50)}#theme-switch:active span{transform:scale(.97)}html{scroll-behavior:smooth}@media(prefers-reduced-motion){html{scroll-behavior:unset}*{transition:none!important;animation:none!important}}main{min-height:60vh}main>*{width:100%;max-width:var(--maxWidth);margin:var(--setMargin)}main>.align-wide{max-width:var(--alignWide)}main>.align-full{--ml:0;--mr:0;max-width:var(--full)}main>section{--mt:6rem}main>:first-child{margin-top:0}footer{padding:1rem 1rem var(--offHeight);background-color:var(--base-200);color:var(--contrast-200);text-align:center;margin:4rem 0 0;position:relative;z-index:var(--z-top)}footer p,footer p+p{margin:.5rem auto}@media (min-width:768px){footer{padding:1rem 2rem var(--offHeight)}}.grid-view,.item-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.grid-view .item,.item-grid .item{border-radius:var(--outerRadius);aspect-ratio:1;display:flex;filter:none;transition:filter var(--transition-base),padding var(--transition-base),background-color var(--transition-base)}.grid-view img,.item-grid img{border-radius:var(--innerRadius)}.item-grid.list-view{display:flex;flex-direction:column;gap:2rem;--gap:2rem}.item-grid.list-view .item .col{--gap:.5rem}.item-grid.list-view img{width:20%}@media (min-width:768px){.grid-view,.item-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}h1 b,h1 strong,h2 b,h2 strong,h3 b,h3 strong,h4 b,h4 strong,h5 b,h5 strong,h6 b,h6 strong{text-decoration:double;-webkit-text-fill-color:transparent;-webkit-text-stroke:2px var(--contrast)}h1,h2,h3,h4,h5,h6{--mt:1.5em;--mb:.875em;font-family:var(--heading);text-transform:uppercase;font-weight:var(--hWeight);line-height:1.3;margin:var(--mt) var(--mr) var(--mb) var(--ml)}h1.inline,h2.inline,h3.inline,h4.inline,h5.inline,h6.inline{font-size:1.2rem;font-weight:600;display:inline-block;margin:0 2rem 0 0;letter-spacing:.05em}h1.inline+*,h2.inline+*,h3.inline+*,h4.inline+*,h5.inline+*,h6.inline+*{display:inline-block;margin:.5rem 0}h1.inline+.term-list,h2.inline+.term-list,h3.inline+.term-list,h4.inline+.term-list,h5.inline+.term-list,h6.inline+.term-list{display:inline-flex;margin:.5rem 0}h1{font-size:var(--xxxlarge);font-weight:var(--hWeight);line-height:1;margin:0 var(--mr) .25em var(--ml)}h1:first-of-type{margin-top:20vh}h1 small{display:block;font-size:var(--small);font-weight:var(--bWeight);line-height:1;font-family:var(--body)}h2{font-size:var(--xxlarge)}h3{font-size:var(--xlarge)}h4{font-weight:400;font-size:var(--large)}h5,h6{font-weight:400;font-size:var(--medium)}p{line-height:1.6}p+p{margin-top:2.5rem}a{color:var(--action-0);text-decoration:none}ul a{display:inline-flex;text-decoration:none}a:visited{color:var(--action-100)}a:hover{color:var(--action-50);text-decoration:underline}.buttons{--wrap:wrap;--justify:flex-start;margin:1rem var(--mr) 1rem var(--ml);width:100%;padding:0}.buttons.fit{width:fit-content;margin:1rem 2rem}.buttons li{--justify:stretch;--align:stretch;padding:0;list-style:none;overflow:hidden}.buttons{margin:3rem auto;max-width:90%}@media (min-width:768px){.buttons{max-width:var(--maxWidth);margin:3rem var(--mr) 3rem var(--ml)}}[type=submit],a.button,a.wp-block-button__link,button{--justify:center;--align:center;--dir:row;width:fit-content;text-transform:uppercase;text-decoration:none;background-color:var(--base-100);color:var(--contrast-50);border:1px solid var(--base-200);border-radius:var(--innerRadius);padding:.25rem 1rem;font:inherit;cursor:pointer;outline:inherit;display:inline-flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir);transition:color var(--transition-base);transition-property:color,border,background-color;position:relative}.buttons a:hover,[type=submit]:focus,[type=submit]:hover,a.button:focus,a.button:hover,a.wp-block-button__link:focus,a.wp-block-button__link:hover,button:focus,button:hover{background-color:var(--action-0);color:var(--action-contrast)}[type=submit]:disabled,[type=submit]:disabled:focus,[type=submit]:disabled:hover,a.button:disabled,a.button:disabled:focus,a.button:disabled:hover,a.wp-block-button__link:disabled,a.wp-block-button__link:disabled:focus,a.wp-block-button__link:disabled:hover,button:disabled,button:disabled:focus,button:disabled:hover{opacity:.5;background-color:var(--base-200)!important;color:var(--contrast-200)!important}details .icon{--w:1.5em}button.favourite.favourited,button.voted svg{animation:favourite-pop .4s cubic-bezier(.25,.46,.45,.94)}@keyframes favourite-pop{0%{transform:scale(1)}50%{transform:scale(1.3)}75%{transform:scale(.9)}100%{transform:scale(1)}}button.filter-toggle{border:1px solid var(--base-200);background-color:transparent;white-space:nowrap;font-size:1rem;padding:.35em;--w:1.2em}.filter-toggle:hover{border-color:var(--action-50);color:var(--action-50)}.filter-toggle:focus{background-color:var(--action-50);color:var(--action-contrast)}.toggle.notifications.has .bell,.toggle.notifications:not(.has) .bell-ringing,.vote .voted .downvote,.vote .voted .upvote,.vote button:not(.voted) .downvoted,.vote button:not(.voted) .upvoted,button.favourite.favourited .heart,button.favourite:not(.favourited) .heart-fill{display:none}.toggle.notifications.has .bell-ringing,.toggle.notifications:not(.has) .bell,.vote .voted .downvoted,.vote .voted .upvoted,.vote button:not(.voted) .downvote,.vote button:not(.voted) .upvote,button.favourite.favourited .heart-fill,button.favourite:not(.favourited) .heart{display:block}.icon{width:var(--w);height:var(--w);display:inline-flex;transition:var(--transition-size),var(--transition-color)}.icon svg{width:100%;height:100%}.icon.small,nav ul .icon{--w:24px}.icon.colour{background:#b7332e;background:linear-gradient(180deg,rgba(255,0,128,1) 0,rgba(250,71,101,1) 14%,rgba(251,121,35,1) 28%,rgba(176,190,19,1) 42%,rgba(14,204,0,1) 56%,rgba(14,225,166,1) 70%,rgba(63,152,253,1) 84%,rgba(166,90,196,1) 100%);mask-image:var(--colour);-webkit-mask-image:var(--colour);-webkit-mask-repeat:no-repeat;-webkit-mask-size:contain;mask-repeat:no-repeat;mask-size:contain;width:1.25rem;height:1.25rem}.icon.logo-basic svg path{transition:fill var(--timing) var(--function)}.icon.logo-basic svg path#innerCircle,.icon.logo-basic svg path#outerSkull{fill:var(--base)}a .icon.logo-basic:hover svg path{fill:var(--base)}a .icon.logo-basic:hover svg path#innerCircle,a .icon.logo-basic:hover svg path#outerSkull{fill:var(--action-0)}.icon.grab{cursor:grab}main a .icon{margin-right:.5em}body:has(#theme-switcher:not(:checked)) .icon.logo-split-color{position:relative}body:has(#theme-switcher:not(:checked)) .icon.logo-split-color::before{content:'';display:block;width:60%;height:60%;border-radius:50%;background-color:var(--dark-200);position:absolute;left:18%;top:22%;z-index:-1}path#refresh{transform-origin:center;transform-box:fill-box;animation:spin 1s var(--function) infinite}.screen-reader-text{border:0;clip:rect(1px,1px,1px,1px);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute!important;width:1px;word-wrap:normal!important}:focus,:focus-visible,input[type=checkbox]+label:focus,input[type=checkbox]+label:focus-visible,input[type=radio]+label:focus,input[type=radio]+label:focus-visible{outline:2px solid var(--action-0)!important;outline-offset:2px!important;box-shadow:0 0 0 4px rgba(var(--action-rgb),var(--rgb-light))!important}[aria-busy=true]{cursor:progress}[aria-disabled=true],[disabled]{cursor:not-allowed;opacity:.7}details{padding:.25rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details[open]{background-color:var(--base-50)}details summary{--wrap:nowrap;list-style:none;text-transform:uppercase;cursor:pointer;border:0;transition:background-color var(--transition-base);transition-property:background-color,border;position:relative;padding:.5rem 2.5rem .5rem .5rem;gap:.5rem}details summary:hover{background-color:var(--base-100);border-color:var(--base-100);color:var(--contrast);transition:background-color var(--transition-base);transition-property:background-color,border,color}details[open]>summary{background-color:var(--base-50)}details summary::after{content:"";background-color:var(--contrast-100);-webkit-mask-repeat:no-repeat;-webkit-mask-size:contain;-webkit-mask-image:var(--details);mask-image:var(--details);mask-repeat:no-repeat;mask-size:contain;width:1.25rem;height:1.25rem;margin-left:auto;transition:background-color var(--transition-base);transition-property:background-color,transform}details summary:hover::after,details[open]>summary::after{background-color:var(--contrast)}details[open]>summary::after{transform:rotate(-540deg);transition:background-color var(--transition-base);transition-property:background-color,transform}details::details-content{opacity:0;block-size:0;overflow-y:clip;transition:content-visibility var(--timing) allow-discrete,opacity var(--timing),block-size var(--timing)}details[open]::details-content{opacity:1;block-size:auto}@media (prefers-reduced-motion:no-preference){details{interpolate-size:allow-keywords}}input[type=date],input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=textarea],input[type=url],textarea{--p-x:1.5rem;font-family:var(--body);font-size:var(--medium);color:var(--contrast);padding:1rem var(--p-x);border-radius:var(--innerRadius);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;transition:background-color var(--transition-base);transition-property:background-color,border}input[type=email]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=textarea]:focus,input[type=url]: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)}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--innerRadius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--small);padding:.5rem 1rem;width:100%;transition:var(--transition-color)}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;transition:opacity var(--transition-base)}.search-container .clear-search{opacity:0;cursor:default;transition:opacity var(--transition-base)}.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}label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.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(--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(--innerRadius);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}[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(--innerRadius);transition:background-color var(--transition-base),border-color var(--transition-base)}[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{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;transition:transform .3s ease;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(--innerRadius);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{display:none}.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[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.field{margin:2rem 0}.toggle-text input{display:none}.toggle-text input+label{font-weight:400;color:var(--contrast)!important;text-transform:none;cursor:pointer;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.toggle-text label::after,.toggle-text label::before{display:none}.toggle-text label{padding-left:0!important}.toggle-text input+label .text{position:relative;margin:0 .5rem;font-weight:700;width:fit-content;padding:2px 4px;border:1px solid var(--action-50);border-radius:4px;color:var(--action-50)!important}table .toggle-text input+label .text{color:var(--contrast)!important;border-color:var(--contrast)}.toggle-text:hover .text,table .toggle-text:hover .text{background-color:var(--action-50);color:var(--light-0)!important;border-color:var(--action-50)}.toggle-text input+label .off,.toggle-text input+label .on{-webkit-transition:opacity .125s ease-out,-webkit-transform .125s ease-out;transition:opacity .125s ease-out,-webkit-transform .125s ease-out;transition:transform .125s ease-out,opacity .125s ease-out;transition:transform .125s ease-out,opacity .125s ease-out,-webkit-transform .125s ease-out}.toggle-text input+label .off{opacity:1;max-width:100%;-webkit-transform:none;transform:none}.toggle-text input+label .on{opacity:0;max-width:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.toggle-text input:checked+label .off{opacity:0;max-width:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.toggle-text input:checked+label .on{max-width:100%;opacity:1;-webkit-transform:none;transform:none}.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{--gap:nowrap}.quantity{margin:0}.quantity label{margin:0;font-size:var(--small)}.quantity{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 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}.term-list{--justify:flex-start;--align:center;--wrap:nowrap;--gap:.5rem;--w:1em;margin:0;padding:0;height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir);position:relative;overflow:auto hidden;touch-action:pan-x;text-transform:lowercase}dialog::backdrop{backdrop-filter:blur(5px);background-color:var(--overlay-medium)}dialog[open]{z-index:999;--padding:0;top:0;width:min(500px,95vw);border-radius:1rem;height:fit-content;max-height:90vh;overflow:hidden;padding:var(--padding);background-color:var(--base-50);color:var(--contrast);border:1px solid var(--base-200);box-shadow:var(--shadow)}dialog>.wrap,dialog>form{overflow:hidden auto;max-height:100%;margin:1.5rem 0 0 1.5rem;padding-right:1.2rem;width:calc(100% - 1.5rem - 1.2rem)}dialog label{font-weight:400}dialog h2,dialog h3{margin:0 0 .5rem 0;font-size:var(--large)}dialog:has(.m-actions){padding-bottom:var(--height)}.m-actions{--w:1.5em;--justify:flex-end;--wrap:nowrap;--gap:0;position:absolute;bottom:0;left:0;right:0;width:100%;z-index:var(--z-6);background-color:var(--action-100);box-shadow:var(--shadow-up)}.m-actions button{width:100%;height:3rem;border-radius:0;color:var(--action-contrast);background-color:var(--action-50);border:2px solid var(--action-50)}.m-actions button:focus,.m-actions button:hover{background-color:var(--base);color:var(--contrast)}.m-actions button:first-of-type{border-bottom-left-radius:1rem}.m-actions button:last-of-type{border-bottom-right-radius:1rem}dialog ul{list-style:none}dialog .search-container{padding-top:1rem;width:100%;gap:.5rem}dialog[open].gallery{width:calc(100vw - var(--padding) * 2);height:99vh;background:var(--overlay-heavy)}.gallery .content{position:relative;max-width:100%;max-height:100%;padding:2rem}.gallery .favourite button.favourite{top:unset;bottom:1rem;right:1rem}.gallery .image{max-width:100%;max-height:calc(100vh - 4rem);object-fit:contain}.gallery .cancel{position:absolute;top:1rem;right:1rem;background:0 0;border:none;color:#fff;cursor:pointer;padding:.5rem;z-index:10;transition:color .3s ease}.gallery .cancel:hover{color:var(--action-0)}.gallery .nav{position:absolute;top:50%;height:50%;z-index:5;transform:translateY(-50%);border:none;color:var(--contrast);cursor:pointer;padding:1rem;transition:color .3s ease}.gallery .nav:hover{background-color:var(--overlay-heavy)}.gallery .nav:hover{color:var(--action-0)}.gallery .prev{left:1rem}.gallery .next{right:1rem}.gallery .counter{position:absolute;top:1rem;left:1rem;color:#fff;font-size:.875rem}.gallery .content details{position:absolute;bottom:1rem;left:2rem;width:calc(100% - 4rem);background-color:var(--overlay-light);padding:0}.gallery .content details:hover,.gallery .content details[open]{background-color:var(--overlay-heavy);backdrop-filter:blur(5px)}.gallery .content details[open] summary{background-color:transparent}table{white-space:nowrap;width:100%;display:block;margin:0 0 2rem;border-radius:4px;height:var(--maxHeight);overflow:auto;position:relative}tfoot,thead{position:sticky;z-index:10;background-color:var(--base);text-transform:uppercase;padding:.5rem 0;line-height:2;font-weight:400}tr:nth-of-type(even){background-color:var(--base-200)}tfoot th{vertical-align:middle}tfoot th:first-of-type{text-align:right}tfoot tr,thead tr{background-color:var(--overlay-heavy);box-shadow:var(--shadow)}thead tr{border-bottom:1px solid var(--contrast-200)}tfoot tr{border-top:1px solid var(--contrast-200)}thead{top:0}tfoot{bottom:0}thead th{width:max-content}th p{margin:0!important}td{width:max-content;padding:.5rem 1rem}td .toggle input[type=checkbox]{margin:0}td .field{margin:.25rem 0}td[data-id=actions] label{margin:0;padding:0}td .description{display:none}td input[type=text]{width:fit-content;max-width:40vw;padding:.25em!important;font-size:var(--small)!important}tbody tr{border:2px solid transparent}tbody tr:focus-within{background-color:var(--base-100);border-color:var(--action-50)}[data-stuck]{background-color:var(--overlay-medium);position:sticky;left:-1rem;z-index:15;box-shadow:var(--subtleRight)}tbody [data-stuck]{z-index:5}tfoot [data-stuck],thead [data-stuck]{background:var(--base)}blockquote{padding:var(--outerPadding);border-radius:var(--outerRadius);background-color:var(--base-50)}cite{width:90%;margin:1rem auto}.hide-tooltip.hide-tooltip.hide-tooltip+[role=tooltip],[role=tooltip]{visibility:hidden;position:absolute;bottom:2rem;left:1rem;width:max-content;height:fit-content;max-width:50vw;padding:.5rem;border-radius:var(--innerRadius);box-shadow:var(--shadow);background:var(--action-0);color:var(--action-contrast)}body.menu_item [role=tooltip]{left:auto;right:100%;top:-200%;z-index:var(--z-4)}[role=tooltip] p{margin:0}[role=tooltip] p+p{margin-top:.5rem}.field:has([aria-describedby]:focus) [role=tooltip],[aria-describedby]:focus~.has-tooltip[role=tooltip],[aria-describedby]:hover~.has-tooltip [role=tooltip]{visibility:visible;display:block}.has-tooltip{display:inline-flex;justify-content:flex-end;position:relative;--w:1.5rem}.tt-toggle{cursor:pointer;display:flex;border-radius:50%;background-color:transparent}.tt-toggle:focus,.tt-toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}.tt-toggle:focus+[role=tooltip],.tt-toggle:hover+[role=tooltip]{visibility:visible}dialog[open]#jvb-selector{height:70vh;top:15vh;display:flex}#jvb-selector>.wrap{flex:1}dialog.loading{opacity:0;transition:opacity var(--transition-base)}dialog.loading[open]{opacity:1;transition:opacity var(--transition-base);width:100vw;height:100vh;display:flex;max-width:100%;max-height:100%;border-radius:0;border:none;background-color:transparent;box-shadow:none;--w:3em;justify-content:center;align-items:center}dialog.loading[open]@starting-style{opacity:0}dialog.loading[open]>.col{height:fit-content;width:min(400px,60vw);border-radius:var(--outerRadius);background-color:var(--overlay-medium);padding:2rem;box-shadow:var(--shadow);position:relative}dialog.loading[open] .spinner{position:absolute;top:1rem;width:5rem;height:5rem;border-width:0;border-top-width:4px;animation:spin 1s var(--function) infinite}.loading[open] .icon{color:var(--action-0)}dialog.loading[open] svg{animation:dance 2s ease-in-out infinite;transition:color .3s ease}dialog.loading[open] h3{color:var(--contrast);margin:2rem 1rem auto!important;font-size:var(--large);width:-moz-fit-content;width:fit-content}dialog.loading[open] p{margin:.5rem auto}dialog.loading[open]::after{animation:shimmer 3s ease-in-out infinite;background:linear-gradient(90deg,var(--shimmer));content:"";inset:0;position:absolute;z-index:-1}.spinner{width:12px;height:12px;border:2px solid transparent;border-top:2px solid var(--action-50);border-radius:50%;animation:spin 1s var(--function) infinite}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes shimmer{0%{left:-50%}50%{left:150%}100%{left:-50%}}@keyframes dance{0%,100%{transform:rotate(-5deg) scale(1)}50%{transform:rotate(5deg) scale(1.1)}}@keyframes letterOutline{0%{background-size:1ch 0}100%{background-size:1ch 100%}}@keyframes letterInside{0%,50%{background-position-y:100%,0}100%,50.01%{background-position-y:0,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(--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;border:none;position:relative}.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}.toggle-details{gap:2px}body.menu_item #top{z-index:var(--z-4);position:relative}section .toggle-details{position:absolute;right:0;top:5rem}[data-toggle=all]{position:fixed;bottom:calc(var(--offHeight) + var(--height) + .5rem);right:0;z-index:var(--z-4);background-color:var(--action-0);color:var(--action-contrast)}[data-toggle]{z-index:var(--z-1)}body:has(#queue[hidden]) [data-toggle=all]{left:1rem}dialog:not([open]).col,dialog:not([open]).row{display:none}@media (min-width:768px){section .toggle-details{right:-10%}}.typeText::after{content:'|';display:inline-block;margin-left:0;animation:blink .75s step-end infinite}@keyframes blink{from,to{opacity:1}50%{opacity:0}}aside#cart,aside#queue{position:fixed;top:var(--doubleHeight);bottom:var(--offHeight);width:min(500px,calc(100vw - 2rem));background-color:var(--base);z-index:var(--z-5);box-shadow:var(--shadow);padding-bottom:var(--height);overflow:visible}.create-item,.qtoggle,.toggle-cart{z-index:var(--z-6);position:fixed;bottom:var(--offHeight);width:var(--height);height:var(--height);background-color:var(--overlay-medium);color:var(--contrast);transition:width var(--transition-base),background-color var(--transition-base),color var(--transition-base),left var(--transition-base);box-shadow:var(--shadow)}.create-item:focus,.create-item:hover,.qtoggle:focus,.qtoggle:hover,.toggle-cart:focus,.toggle-cart:hover{background-color:rgba(var(--action-rgb),var(--rgb-heavy));color:var(--action-contrast)}.create-item:disabled,.create-item:disabled:focus,.create-item:disabled:hover,.qtoggle:disabled,.qtoggle:disabled:focus,.qtoggle:disabled:hover,.toggle-cart:disabled,.toggle-cart:disabled:focus,.toggle-cart:disabled:hover{opacity:.5;background-color:var(--overlay-light);color:var(--contrast)}.create-item,.toggle-cart{right:0;border-radius:4px 4px 4px var(--outerRadius)}body:has(#cart.expanded) .toggle-cart{width:min(500px,calc(100vw - 2rem))}body:has(#cart.expanded) .toggle-cart .icon{display:none}aside#cart{overflow:hidden;right:var(--offScreen);border-radius:var(--outerRadius) 0 0 var(--outerRadius);transition:right var(--transition-base);padding-bottom:6rem}aside#cart.expanded{right:0;transition:right var(--transition-base)}#cart form{max-height:100%;overflow:hidden auto}#cart nav.tabs{z-index:var(--z-6);top:0}#cart table{height:auto}#cart th{padding:0 1.5rem}#cart table th:first-of-type{width:100%}#cart nav.tabs{position:sticky;box-shadow:var(--shadow)}#cart button[data-tab]{flex:1;border-radius:0}#cart form>:not(.tabs){max-width:90%;margin:0 auto}#cart form .empty p{margin:.5rem 0!important}#cart .cart-total.cart-total{--gap:0 1rem;padding-right:1rem;position:absolute;bottom:var(--height);width:100%;max-width:100%;background-color:var(--overlay-heavy);z-index:var(--z-6);box-shadow:var(--shadow-up)}.cart-total p{--gap:2rem;max-width:100%;margin:0}.cart-total p span{width:6rem;display:inline-block;text-align:right}.cart-total p+p{font-weight:700}.cart-items .total{font-weight:700}#cart .restored{background-color:rgba(var(--action-rgb),var(--rgb-light));border-radius:var(--outerRadius);padding:1rem}.restored h3{font-size:var(--medium);margin:0}.restored p{margin:0}.restored .row{--gap:0;--wrap:nowrap;--w:1em}.toasts{position:fixed;top:4rem;right:-350px;z-index:1000;width:350px}.toast{background-color:var(--overlay-heavy);border-left:4px solid var(--action-0);padding:1rem;box-shadow:var(--shadow);left:0;position:relative;opacity:0;transition:left .3s,opacity .3s}.toast.success{border-left-color:var(--success)}.toast.error{border-left-color:var(--error)}.toast.info{border-left-color:var(--warning)}.toast.show{left:calc(-350px - 1rem);opacity:1}.toast.hiding{left:0;opacity:0}.toast-content p{margin:0}.close-toast{background:0 0;border:none;font-size:1.25rem;cursor:pointer;opacity:.5;transition:opacity .2s;color:inherit}.close-toast:hover{opacity:1}aside#queue{left:var(--offScreen);border-radius:0 var(--outerRadius) var(--outerRadius) 0;transition:left var(--transition-base);--wrap:nowrap;--align:stretch}aside#queue.expanded{left:0;overflow:hidden auto}.qtoggle{left:0;border-radius:4px 4px var(--outerRadius) 4px}body:has(#queue.expanded) .qtoggle{left:var(--height);width:min(calc(500px - var(--height)),calc(100vw - 2rem - var(--height)))}.qtoggle.saving svg{color:var(--action-0);animation:spin .87s var(--function) infinite}#queue .status-actions{position:absolute;bottom:0;left:0;right:0;z-index:var(--z-2)}#queue .status-actions .popup{position:absolute;z-index:-1;width:max-content;max-width:300px;background-color:var(--action-50);color:var(--action-contrast);border-radius:var(--innerRadius);padding:.25em .75em;top:1rem;left:-100vw;transition:left var(--transition-base)}aside#queue .popup::before{content:'';width:10px;height:10px;transform:rotate(-45deg);background-color:var(--action-50);z-index:-1;left:-5px;position:absolute;top:calc(50% - 5px)}.expanded#queue .status-actions .popup.showing{left:calc(100% + 1em)}#queue .status-actions .popup.showing{left:calc(200vw + var(--offHeight));max-width:75vw}#queue .item .status,.filter .count,.qtoggle .count,.qtoggle .indicator,.refresh .countdown{z-index:var(--z-3);--offset:0;position:absolute;top:var(--offset);background-color:var(--overlay-light)}.expanded+.qtoggle .count,.expanded+.qtoggle .indicator{--offset:.25rem}.qtoggle .indicator{right:var(--offset);width:.75rem;height:.75rem;border-radius:50%}aside#queue.synced+.qtoggle .indicator{background-color:var(--success)}aside#queue.pending+.qtoggle .indicator{background-color:var(--warning);animation:pulse 2s infinite}aside#queue.pending:not(.expanded)+.qtoggle svg{color:var(--error);animation:spin 1s var(--function) infinite}.qtoggle .count{--align:center;--justify:center;left:var(--offset);min-width:1.25rem;height:1.25rem;padding:0 4px;color:var(--contrast);border-radius:var(--innerRadius);font-size:var(--extra-small)}#queue:has(.empty-queue)+.qtoggle .count{display:none}aside#queue .header{padding:15px;border-bottom:1px solid var(--base-200);flex-shrink:0}.qitems{flex:1;overflow:hidden auto;padding:.5rem 2rem;--gap:.5rem}aside#queue h3{margin:0 0 12px 0;font-size:16px;color:var(--contrast)}#queue .filters .filter{background-color:transparent;white-space:nowrap;font-size:var(--small)}#queue .filters .filter.active{background:var(--base-200);border-color:transparent}#queue .filter:focus,#queue .filter:hover{background-color:var(--action-0);color:var(--action-contrast)}.filter .count{--offset:-8px;right:var(--offset);background:var(--base-200);color:var(--contrast-200);border-radius:10px;min-width:18px;height:18px;font-size:10px}.filter .count:empty{display:none}.empty-queue{height:100px;color:var(--contrast-200);font-size:var(--small);font-style:italic}.refresh .countdown:not(.counting),aside#queue:has(.empty-queue) .refresh .count{display:none}#queue .item{padding:15px;background:var(--base-100);border-radius:var(--innerRadius);transition:all .2s ease;box-shadow:var(--shadow-none)}#queue .item:hover{box-shadow:var(--shadow)}#queue .item .header{position:relative}#queue .item .type{font-size:var(--small)}#queue .item .status{--w:1em;--gap:0;--justify:center;--align:center;--offset:-1.2rem;aspect-ratio:1;right:var(--offset);border-radius:50%;color:var(--contrast-200);background-color:var(--base-50);border:1px solid var(--base-200);width:1.25em;height:1.25em}#queue .item .status.pending{background:var(--base-100);color:var(--contrast-200)}#queue .item .status.processing{background:var(--base-200);color:var(--contrast-100);animation:pulse-color 2s infinite}#queue .item .status.completed{background:var(--base-50);color:var(--base-200)}#queue .item .status.completed:hover{color:var(--contrast-200)}#queue .item .status.failed{background:var(--base);color:var(--error)}#queue .item button{font-size:16px;padding:0;line-height:1;opacity:.5;transition:opacity .2s}#queue .item button:hover{opacity:1}#queue .item .info{margin-top:8px;font-size:var(--small)}#queue .item .info .time{--gap:7px;font-size:10px}#queue .item .actions{margin-top:12px;--gap:8px}#queue .item .actions button{padding:6px 12px;font-size:12px;background:var(--base-200);border:none;border-radius:4px;cursor:pointer;transition:all .2s;color:var(--contrast)}#queue .item .actions .retry{background-color:var(--secondary-200);color:var(--secondary-contrast)}#queue .item .actions button:hover{opacity:.9}.queue-actions{padding:15px;border-top:1px solid var(--base-200);flex-shrink:0}.queue-actions button{padding:8px 12px;font-size:var(--small);transition:all .2s}.status-actions>.refresh{position:relative;font-size:var(--small)}.refresh .countdown{--justify:center;--align:center;--offset:0;right:var(--offset);margin:0 3px;border-radius:50%;border:1px solid var(--base-200)}.refreshNow{width:var(--height);height:var(--height)}.refreshNow:hover{background:var(--base-200);color:var(--contrast-200)}.icon.refresh{--w:18px}#queue.pending.expanded .refreshNow svg{animation:spin 1.5s var(--function) infinite}#queue,.item-grid{counter-reset:delay-counter}.item{counter-increment:delay-counter}.item .progress .fill::after{--delay:calc(counter(delay-counter) * .1s)}.progress .bar{height:6px;display:block;border-radius:6px;overflow:hidden;background:var(--base-200);position:relative}.progress .fill{height:100%;background:var(--action-0);border-radius:6px;width:0;transition:width .3s ease}.progress .details{margin-top:5px;font-size:var(--small);color:var(--contrast);text-align:center;padding:.25rem 0}.progress .details:empty{display:none}.pending .fill::after,.processing .fill::after,.queued .fill::after,.uploading .fill::after{--delay:0s;content:'';position:absolute;top:0;left:-50%;width:30%;height:100%;background:linear-gradient(90deg,rgba(255,255,255,0) 0,rgba(255,255,255,.225) 50%,rgba(255,255,255,0) 100%);animation:shimmer 2.5s infinite linear var(--delay)}@keyframes shimmer{0%{left:-50%}50%{left:150%}100%{left:-50%}}@keyframes pulse-color{0%{box-shadow:0 0 0 0 rgba(var(--secondary-rgb),.4)}70%{box-shadow:0 0 0 6px rgba(var(--secondary-rgb),0)}100%{box-shadow:0 0 0 0 rgba(var(--secondary-rgb),0)}}@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(20px)}}@keyframes detect-scroll{from,to{--can-scroll:1}}.menu-items .menu-item{display:grid;grid-template-columns:repeat(3,1fr);gap:0 1rem}.menu-items .menu-item:not(.variable) label{display:none}.menu-items .menu-item .field{margin:0;--wrap:nowrap}.menu-items .menu-item .has-tooltip{position:absolute;right:-2.5rem}.menu-items .menu-item+.menu-item{border-top:1px solid var(--base-200);margin-top:2rem;padding-top:1rem}.menu-items .menu-item .header{grid-column:1/-1}.menu-items .menu-item .description{grid-column:1/3}.menu-items .menu-item .info{grid-column:3/3}.menu-items .menu-item h3{font-size:var(--medium);font-weight:400;margin:0 0 .5rem 0!important}.menu-items .menu-item .info{--gap:1rem}.price>span{vertical-align:super;font-size:12px}body.menu_item section h2{display:inline-block;max-width:var(--maxWidth);width:max-content;background-color:var(--base-50);color:var(--action-0);position:relative;z-index:5;padding:0 1rem;margin:var(--mt) auto var(--mb) auto}.menu-section{position:relative}.menu-section hr{position:absolute;width:100%;left:-5%;top:3.5rem;border:none;background-color:var(--action-100);height:2px}details.menu-item summary.row{flex-direction:column;align-items:flex-start}details.menu-item summary .row{width:100%}.menu_item h1:first-of-type{margin-top:10vh!important}@media (min-width:768px){.menu-section hr{width:120%;left:-10%;top:4.25rem}.menu_item section{max-width:var(--maxWidth)}}/*!** Forms **!*//*!*.field.time_open,*!*//*!*.field.time_closes,*!*//*!*.field.date_start,*!*//*!*.field.time_start,*!*//*!*.field.time_end {*!*//*!*    margin-bottom: 0;*!*//*!*}*!*//*!*.field.time_open,*!*//*!*.field.time_closes,*!*//*!*.field.time_start,*!*//*!*.field.time_end {*!*//*!*    width: 49%;*!*//*!*    display: inline-block;*!*//*!*    margin-top: 1rem;*!*//*!*}*!*//*!* Style for disabled state *!*//*!** Shop Page **!*//*!** Bio Sections **!*//*!*!* Status notification *!*//*!*.status-notification {*!*//*!*    position: fixed;*!*//*!*    bottom: 20px;*!*//*!*    left: 80px; !* Position to the right of the panel *!*!*//*!*    width: 300px;*!*//*!*    max-width: calc(100vw - 100px);*!*//*!*    border-radius: 8px;*!*//*!*    padding: 15px;*!*//*!*    background: #323232;*!*//*!*    color: white;*!*//*!*    transform: translateY(20px);*!*//*!*    opacity: 0;*!*//*!*    transition: transform .3s, opacity .3s;*!*//*!*    z-index: 10000;*!*//*!*    box-shadow: 0 4px 20px rgba(0, 0, 0, .2);*!*//*!*    pointer-events: none;*!*//*!*}*!*//*!*.status-notification.active {*!*//*!*    transform: translateY(0);*!*//*!*    opacity: 1;*!*//*!*    pointer-events: auto;*!*//*!*}*!*//*!*.status-notification .title {*!*//*!*    font-weight: 600;*!*//*!*    margin-bottom: 5px;*!*//*!*    font-size: 15px;*!*//*!*}*!*//*!*.status-notification .message {*!*//*!*    margin-bottom: 10px;*!*//*!*    font-size: 14px;*!*//*!*}*!*//*!*.status-notification .actions {*!*//*!*    display: flex;*!*//*!*    justify-content: flex-end;*!*//*!*}*!*//*!*.status-notification .actions button {*!*//*!*    padding: 6px 12px;*!*//*!*    background: rgba(255, 255, 255, .2);*!*//*!*    border: none;*!*//*!*    border-radius: 4px;*!*//*!*    color: white;*!*//*!*    cursor: pointer;*!*//*!*    font-size: 13px;*!*//*!*    transition: background .2s;*!*//*!*}*!*//*!*.status-notification .actions button:hover {*!*//*!*    background: rgba(255, 255, 255, .3);*!*//*!*}*!*//*!* Progress containers in notifications *!*//*!* Collapsed state - just show the toggle button *!*//*!***//*!***//*!*.new-term-toggle:disabled + .loader,*!*//*!*.loading .loader {*!*//*!*    width: 50px;*!*//*!*    aspect-ratio: 1;*!*//*!*    display: grid;*!*//*!*    border: 4px solid #0000;*!*//*!*    border-radius: 50%;*!*//*!*    border-right-color: var(--action-0);*!*//*!*    animation: l15 1s infinite linear;*!*//*!*}*!*//*!*.new-term-toggle:disabled + .loader::before,*!*//*!*.new-term-toggle:disabled + .loader::after,*!*//*!*.loading .loader::before,*!*//*!*.loading .loader::after {*!*//*!*    content: "";*!*//*!*    grid-area: 1/1;*!*//*!*    margin: 2px;*!*//*!*    border: inherit;*!*//*!*    border-radius: 50%;*!*//*!*    animation: l15 2s infinite;*!*//*!*}*!*//*!*.new-term-toggle:disabled + .loader::after,*!*//*!*.loading .loader::after {*!*//*!*    margin: 8px;*!*//*!*    animation-duration: 3s;*!*//*!*}*!*//*!*@keyframes l15{*!*//*!*    100%{transform: rotate(1turn)}*!*//*!*}*!*//*!* High contrast mode support *!*//*!** TODO: Verify **!*//*!* Icon styling in form fields *!*//*!* Required field asterisk *!*//*!* Invalid field styling *!*//*!* Frontend Display *!*//*!* Set and Checkbox Field Display *!*//*!* Radio and Select Field Display *!*//*!* True/False Field Display *!*//*!* Group Field Styling *!*//*!* Responsive Design *!*/
\ No newline at end of file
+:root{--narrow:min(500px, 50vw);--maxWidth:min(768px, 65vw);--alignWide:min(1024px, 90vw);--alignMed:min(962px, 82.5vw);--full:100vw;--mr:auto;--ml:auto;--mt:1rem;--mb:1rem;--setMargin:var(--mt) var(--mr) var(--mb) var(--ml);--insetMargin:var(--mt) calc((var(--content) - var(--narrow)) / 2 + var(--mr)) var(--mb) var(--ml);--height:4rem;--doubleHeight:8rem;--offHeight:5rem;--maxHeight:calc(100vh - var(--height) - var(--height));--gap:.5rem;--wrap:wrap;--justify:center;--align:center;--dir:row;--w:1.2em;--filter:grayscale(.3) sepia(.4);--font-base:-apple-system,BlinkMacSystemFont,avenir next,avenir,segoe ui,helvetica neue,helvetica,Cantarell,Ubuntu,roboto,noto,arial,sans-serif;--heading:'Aleo',var(--font-base);--body:'Josefin Slab',var(--font-base);--hWeight:900;--hlight:400;--bWeight:400;--bBold:700;--bLight:200;--enormous:calc(26vh - 4rem);--xxxlarge:clamp(2.5rem, 1.429rem + 2.857vw, 4rem);--xxlarge:clamp(2rem, 1.286rem + 1.905vw, 3rem);--xlarge:clamp(1.6rem, .957rem + 1.714vw, 2.5rem);--large:clamp(1.3rem, .6rem + 1.867vw, 2rem);--xmedium:clamp(1.4rem, .971rem + 1.143vw, 2rem);--medium:clamp(1.1rem, .993rem + .286vw, 1.25rem);--small:clamp(.95rem, .879rem + .19vw, 1.05rem);--extra-small:clamp(.75rem, 1.1337rem + -1.2278vw, .059375rem);--light-0:#fafafa;--light-50:#fcfbfb;--light-100:#f1eded;--light-200:#e6dfdf;--dark-0:#100404;--dark-50:#201212;--dark-100:#322423;--dark-200:#443635;--action-0:#B7332E;--action-50:#a32d29;--action-100:#8e2824;--action-200:#7a221f;--secondary-0:#E8A737;--secondary-50:#e59d20;--secondary-100:#d48f18;--secondary-200:#bd7f16;--success:#4CAF50;--warning:#E8A737;--error:#B7332E;--action-contrast:var(--light-0);--secondary-contrast:var(--light-0);--light-rgb:250,250,250;--dark-rgb:16,4,4;--action-rgb:183,51,46;--secondary-rgb:232,167,55;--rgba-subtle:rgba(var(--c),.5);--rgba-subtle-hover:rgba(var(--c),.1);--base:var(--light-0);--base-50:var(--light-50);--base-100:var(--light-100);--base-200:var(--light-200);--contrast:var(--dark-0);--contrast-50:var(--dark-50);--contrast-100:var(--dark-100);--contrast-200:var(--dark-200);--c:var(--light-rgb);--base-rgb:var(--light-rgb);--contrast-rgb:var(--dark-rgb);--z-1:5;--z-2:10;--z-3:15;--z-4:20;--z-5:50;--z-6:100;--z-top:999;--zz-top:999999;--rgb-light:.25;--rgb-medium:.66;--rgb-heavy:.85;--overlay-light:rgba(var(--c), .25);--overlay-medium:rgba(var(--c), .66);--overlay-heavy:rgba(var(--c), .85);--shimmer:rgba(var(--dark-rgb),0) 0%,rgba(var(--dark-rgb),.05) 50%,rgba(var(--dark-rgb),0) 100%;--shadow:rgba(var(--dark-rgb),.45) 0px 0px 4px;--shadow-down:rgba(var(--dark-rgb),.45) 0 6px 5px -5px;--shadow-right:rgba(var(--dark-rgb),.45) 6px 0 5px -5px;--shadow-left:rgba(var(--dark-rgb), .45) -6px 0 5px -5px;--shadow-up:rgba(var(--dark-rgb), .45) 0 -6px 5px -5px;--subtle:rgba(var(--dark-rgb), .45) 0px 25px 20px -20px;--subtleRight:rgba(var(--dark-rgb), .45) 10px 0 20px -20px;--shadow-none:transparent 0px 0px 0px;--innerRadius:4px;--outerPadding:1rem;--outerRadius:calc(var(--innerRadius) + var(--outerPadding));--function:cubic-bezier(.47,.24,.07,.47);--timing:.25s;--transition-base:var(--timing) var(--function);--transition-color:background-color var(--transition-base),color var(--transition-base),border var(--transition-base);--transition-transform:transform var(--transition-base);--transition-size:width var(--transition-base),height var(--transition-base),max-width var(--transition-base),max-height var(--transition-base);--offScreen:-200vw;--minus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23151515" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H88a4,4,0,0,1,0-8h80A4,4,0,0,1,172,128Z"></path></svg>');--plus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23151515" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H132v36a4,4,0,0,1-8,0V132H88a4,4,0,0,1,0-8h36V88a4,4,0,0,1,8,0v36h36A4,4,0,0,1,172,128Z"></path></svg>');--close:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4ZM162.83,98.83,133.66,128l29.17,29.17a4,4,0,0,1-5.66,5.66L128,133.66,98.83,162.83a4,4,0,0,1-5.66-5.66L122.34,128,93.17,98.83a4,4,0,0,1,5.66-5.66L128,122.34l29.17-29.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--chevron:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23151515" viewBox="0 0 256 256"><path d="M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm42.83-110.83a4,4,0,0,1,0,5.66l-40,40a4,4,0,0,1-5.66,0l-40-40a4,4,0,0,1,5.66-5.66L128,146.34l37.17-37.17A4,4,0,0,1,170.83,109.17Z"></path></svg>');--details:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M210.83,98.83l-80,80a4,4,0,0,1-5.66,0l-80-80a4,4,0,0,1,5.66-5.66L128,170.34l77.17-77.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--shop:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M28.15,95A3.81,3.81,0,0,0,28,96v16a36,36,0,0,0,16,29.92V216a4,4,0,0,0,4,4H208a4,4,0,0,0,4-4V141.92A36,36,0,0,0,228,112V96a3.81,3.81,0,0,0-.17-1.08L213.5,44.7A12,12,0,0,0,202,36H54A12,12,0,0,0,42.5,44.7Zm22-48.08A4,4,0,0,1,54,44H202a4,4,0,0,1,3.84,2.9L218.7,92H37.3ZM100,100h56v12a28,28,0,0,1-56,0ZM36,112V100H92v12a28,28,0,0,1-41.37,24.59,4,4,0,0,0-1.31-.76A28,28,0,0,1,36,112ZM204,212H52V145.94a36,36,0,0,0,44-17.48,36,36,0,0,0,64,0,36,36,0,0,0,44,17.48Zm2.68-76.17a3.94,3.94,0,0,0-1.3.76A28,28,0,0,1,164,112V100h56v12A28,28,0,0,1,206.68,135.83Z"></path></svg>');--style:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M224,92H170.61l9.33-51.28a4,4,0,1,0-7.88-1.44L162.48,92H106.61l9.33-51.28a4,4,0,1,0-7.88-1.44L98.48,92H48a4,4,0,0,0,0,8H97L86.84,156H32a4,4,0,0,0,0,8H85.39l-9.33,51.28a4,4,0,0,0,3.22,4.65A3.65,3.65,0,0,0,80,220a4,4,0,0,0,3.94-3.29L93.52,164h55.87l-9.33,51.28a4,4,0,0,0,3.22,4.65,3.65,3.65,0,0,0,.72.07,4,4,0,0,0,3.94-3.29L157.52,164H208a4,4,0,0,0,0-8H159l10.19-56H224a4,4,0,0,0,0-8Zm-73.16,64H95l10.19-56H161Z"></path></svg>');--map:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M128,68a36,36,0,1,0,36,36A36,36,0,0,0,128,68Zm0,64a28,28,0,1,1,28-28A28,28,0,0,1,128,132Zm0-112a84.09,84.09,0,0,0-84,84c0,30.42,14.17,62.79,41,93.62a250,250,0,0,0,40.73,37.66,4,4,0,0,0,4.58,0A250,250,0,0,0,171,197.62c26.81-30.83,41-63.2,41-93.62A84.09,84.09,0,0,0,128,20Zm37.1,172.23A254.62,254.62,0,0,1,128,227a254.62,254.62,0,0,1-37.1-34.81C73.15,171.8,52,139.9,52,104a76,76,0,0,1,152,0C204,139.9,182.85,171.8,165.1,192.23Z"></path></svg>');--theme:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M241.72,113a11.88,11.88,0,0,0-9.73-5H212V88a12,12,0,0,0-12-12H129.33l-28.8-21.6a12.05,12.05,0,0,0-7.2-2.4H40A12,12,0,0,0,28,64V208a4,4,0,0,0,4,4H211.09a4,4,0,0,0,3.79-2.74l28.49-85.47A11.86,11.86,0,0,0,241.72,113ZM40,60H93.33a4,4,0,0,1,2.4.8L125.6,83.2a4,4,0,0,0,2.4.8h72a4,4,0,0,1,4,4v20H69.76a12,12,0,0,0-11.38,8.21L36,183.35V64A4,4,0,0,1,40,60Zm195.78,61.26L208.2,204H37.55L66,118.74A4,4,0,0,1,69.76,116H232a4,4,0,0,1,3.79,5.26Z"></path></svg>');--arrow-up:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M236,192a4,4,0,0,1-4,4H88a4,4,0,0,1-4-4V57.66L42.83,98.83a4,4,0,0,1-5.66-5.66l48-48a4,4,0,0,1,5.66,0l48,48a4,4,0,0,1-5.66,5.66L92,57.66V188H232A4,4,0,0,1,236,192Z"></path></svg>');--colour:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75Zm9.85,105.59a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"></path></svg>');--linkIcon:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23151515" viewBox="0 0 256 256"><path d="M236,88.12a50.44,50.44,0,0,1-14.81,34.31l-34.75,34.74A50.33,50.33,0,0,1,150.62,172h-.05A50.63,50.63,0,0,1,100,120a4,4,0,0,1,4-3.89h.11a4,4,0,0,1,3.89,4.11A42.64,42.64,0,0,0,150.58,164h0a42.32,42.32,0,0,0,30.14-12.49l34.75-34.74a42.63,42.63,0,1,0-60.29-60.28l-11,11a4,4,0,0,1-5.66-5.65l11-11A50.64,50.64,0,0,1,236,88.12ZM111.78,188.49l-11,11A42.33,42.33,0,0,1,70.6,212h0a42.63,42.63,0,0,1-30.11-72.77l34.75-34.74A42.63,42.63,0,0,1,148,135.82a4,4,0,0,0,8,.23A50.64,50.64,0,0,0,69.55,98.83L34.8,133.57A50.63,50.63,0,0,0,70.56,220h0a50.33,50.33,0,0,0,35.81-14.83l11-11a4,4,0,1,0-5.65-5.66Z"></path></svg>');--swipeRight:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iIzAwMDAwMCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMTIsMTQwdjM2YzAsMjQuNjYtOC4wOCw0MS4xLTguNDIsNDEuNzlhNCw0LDAsMSwxLTcuMTYtMy41OGMuMDctLjE1LDcuNTgtMTUuNTUsNy41OC0zOC4yMVYxNDBhMTYsMTYsMCwwLDAtMzIsMHY0YTQsNCwwLDAsMS04LDBWMTI0YTE2LDE2LDAsMCwwLTMyLDB2MTJhNCw0LDAsMCwxLTgsMFY2OGExNiwxNiwwLDAsMC0zMiwwVjE3NmE0LDQsMCwwLDEtNy4zOSwyLjExbC0xOC42OC0zMGEuNzUuNzUsMCwwLDEtLjA3LS4xMiwxNiwxNiwwLDAsMC0yNy43MiwxNmwyOS4zMSw1MGE0LDQsMCwwLDEtNi45LDRMMzEuMjIsMTY4YTI0LDI0LDAsMCwxLDQxLjUyLTI0LjA5TDg0LDE2MlY2OGEyNCwyNCwwLDAsMSw0OCwwdjM4LjEzYTI0LDI0LDAsMCwxLDM5Ljk0LDE2LjA2QTI0LDI0LDAsMCwxLDIxMiwxNDBabTM4LjgzLTg2LjgzLTMyLTMyYTQsNCwwLDAsMC01LjY2LDUuNjZMMjM4LjM0LDUySDE3NmE0LDQsMCwwLDAsMCw4aDYyLjM0TDIxMy4xNyw4NS4xN2E0LDQsMCwwLDAsNS42Niw1LjY2bDMyLTMyQTQsNCwwLDAsMCwyNTAuODMsNTMuMTdaIj48L3BhdGg+PC9zdmc+');--scrollbar-width:8px;--scrollbar-track-color:var(--base-100);--scrollbar-thumb-color:var(--action-0);--scrollbar-thumb-hover-color:var(--action-50);--scrollbar-thumb-border:2px solid var(--base-50);--scrollbar-border-radius:4px;--can-scroll:0}body:has(#theme-switcher:checked){--action-50:#cb3933;--action-100:#d14c47;--action-200:#d6605c;--secondary-50:#ebb14e;--secondary-100:#edbb65;--secondary-200:#f0c57c;--contrast:var(--light-0);--contrast-50:var(--light-50);--contrast-100:var(--light-100);--contrast-200:var(--light-200);--base:var(--dark-0);--base-50:var(--dark-50);--base-100:var(--dark-100);--base-200:var(--dark-200);--c:var(--dark-rgb);--base-rgb:var(--dark-rgb);--contrast-rgb:var(--light-rgb);--overlay-light:rgba(var(--c), .25);--overlay-medium:rgba(var(--c), .5);--overlay-heavy:rgba(var(--c), .85);--shimmer:rgba(var(--c),0) 0%,rgba(var(--c),.05) 50%,rgba(var(--c),0) 100%;--shadow:rgba(var(--light-rgb),.45) 0px 0px 4px;--shadow-down:rgba(var(--light-rgb),.45) 0 6px 5px -5px;--shadow-right:rgba(var(--light-rgb),.45) 6px 0 5px -5px;--shadow-left:rgba(var(--light-rgb), .45) -6px 0 5px -5px;--shadow-up:rgba(var(--light-rgb), .45) 0 -6px 5px -5px;--subtle:rgba(var(--light-rgb), .45) 0px 25px 20px -20px;--subtleRight:rgba(var(--light-rgb), .45) 10px 0 20px -20px;--minus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H88a4,4,0,0,1,0-8h80A4,4,0,0,1,172,128Z"></path></svg>');--plus:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4Zm-40-80a4,4,0,0,1-4,4H132v36a4,4,0,0,1-8,0V132H88a4,4,0,0,1,0-8h36V88a4,4,0,0,1,8,0v36h36A4,4,0,0,1,172,128Z"></path></svg>');--close:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M208,36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36Zm4,172a4,4,0,0,1-4,4H48a4,4,0,0,1-4-4V48a4,4,0,0,1,4-4H208a4,4,0,0,1,4,4ZM162.83,98.83,133.66,128l29.17,29.17a4,4,0,0,1-5.66,5.66L128,133.66,98.83,162.83a4,4,0,0,1-5.66-5.66L122.34,128,93.17,98.83a4,4,0,0,1,5.66-5.66L128,122.34l29.17-29.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--chevron:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M128,28A100,100,0,1,0,228,128,100.11,100.11,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.1,92.1,0,0,1,128,220Zm42.83-110.83a4,4,0,0,1,0,5.66l-40,40a4,4,0,0,1-5.66,0l-40-40a4,4,0,0,1,5.66-5.66L128,146.34l37.17-37.17A4,4,0,0,1,170.83,109.17Z"></path></svg>');--details:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M210.83,98.83l-80,80a4,4,0,0,1-5.66,0l-80-80a4,4,0,0,1,5.66-5.66L128,170.34l77.17-77.17a4,4,0,1,1,5.66,5.66Z"></path></svg>');--shop:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M28.15,95A3.81,3.81,0,0,0,28,96v16a36,36,0,0,0,16,29.92V216a4,4,0,0,0,4,4H208a4,4,0,0,0,4-4V141.92A36,36,0,0,0,228,112V96a3.81,3.81,0,0,0-.17-1.08L213.5,44.7A12,12,0,0,0,202,36H54A12,12,0,0,0,42.5,44.7Zm22-48.08A4,4,0,0,1,54,44H202a4,4,0,0,1,3.84,2.9L218.7,92H37.3ZM100,100h56v12a28,28,0,0,1-56,0ZM36,112V100H92v12a28,28,0,0,1-41.37,24.59,4,4,0,0,0-1.31-.76A28,28,0,0,1,36,112ZM204,212H52V145.94a36,36,0,0,0,44-17.48,36,36,0,0,0,64,0,36,36,0,0,0,44,17.48Zm2.68-76.17a3.94,3.94,0,0,0-1.3.76A28,28,0,0,1,164,112V100h56v12A28,28,0,0,1,206.68,135.83Z"></path></svg>');--style:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M224,92H170.61l9.33-51.28a4,4,0,1,0-7.88-1.44L162.48,92H106.61l9.33-51.28a4,4,0,1,0-7.88-1.44L98.48,92H48a4,4,0,0,0,0,8H97L86.84,156H32a4,4,0,0,0,0,8H85.39l-9.33,51.28a4,4,0,0,0,3.22,4.65A3.65,3.65,0,0,0,80,220a4,4,0,0,0,3.94-3.29L93.52,164h55.87l-9.33,51.28a4,4,0,0,0,3.22,4.65,3.65,3.65,0,0,0,.72.07,4,4,0,0,0,3.94-3.29L157.52,164H208a4,4,0,0,0,0-8H159l10.19-56H224a4,4,0,0,0,0-8Zm-73.16,64H95l10.19-56H161Z"></path></svg>');--map:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M128,68a36,36,0,1,0,36,36A36,36,0,0,0,128,68Zm0,64a28,28,0,1,1,28-28A28,28,0,0,1,128,132Zm0-112a84.09,84.09,0,0,0-84,84c0,30.42,14.17,62.79,41,93.62a250,250,0,0,0,40.73,37.66,4,4,0,0,0,4.58,0A250,250,0,0,0,171,197.62c26.81-30.83,41-63.2,41-93.62A84.09,84.09,0,0,0,128,20Zm37.1,172.23A254.62,254.62,0,0,1,128,227a254.62,254.62,0,0,1-37.1-34.81C73.15,171.8,52,139.9,52,104a76,76,0,0,1,152,0C204,139.9,182.85,171.8,165.1,192.23Z"></path></svg>');--theme:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M241.72,113a11.88,11.88,0,0,0-9.73-5H212V88a12,12,0,0,0-12-12H129.33l-28.8-21.6a12.05,12.05,0,0,0-7.2-2.4H40A12,12,0,0,0,28,64V208a4,4,0,0,0,4,4H211.09a4,4,0,0,0,3.79-2.74l28.49-85.47A11.86,11.86,0,0,0,241.72,113ZM40,60H93.33a4,4,0,0,1,2.4.8L125.6,83.2a4,4,0,0,0,2.4.8h72a4,4,0,0,1,4,4v20H69.76a12,12,0,0,0-11.38,8.21L36,183.35V64A4,4,0,0,1,40,60Zm195.78,61.26L208.2,204H37.55L66,118.74A4,4,0,0,1,69.76,116H232a4,4,0,0,1,3.79,5.26Z"></path></svg>');--arrow-up:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M236,192a4,4,0,0,1-4,4H88a4,4,0,0,1-4-4V57.66L42.83,98.83a4,4,0,0,1-5.66-5.66l48-48a4,4,0,0,1,5.66,0l48,48a4,4,0,0,1-5.66,5.66L92,57.66V188H232A4,4,0,0,1,236,192Z"></path></svg>');--colour:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75Zm9.85,105.59a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"></path></svg>');--linkIcon:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23F9F9F9" viewBox="0 0 256 256"><path d="M236,88.12a50.44,50.44,0,0,1-14.81,34.31l-34.75,34.74A50.33,50.33,0,0,1,150.62,172h-.05A50.63,50.63,0,0,1,100,120a4,4,0,0,1,4-3.89h.11a4,4,0,0,1,3.89,4.11A42.64,42.64,0,0,0,150.58,164h0a42.32,42.32,0,0,0,30.14-12.49l34.75-34.74a42.63,42.63,0,1,0-60.29-60.28l-11,11a4,4,0,0,1-5.66-5.65l11-11A50.64,50.64,0,0,1,236,88.12ZM111.78,188.49l-11,11A42.33,42.33,0,0,1,70.6,212h0a42.63,42.63,0,0,1-30.11-72.77l34.75-34.74A42.63,42.63,0,0,1,148,135.82a4,4,0,0,0,8,.23A50.64,50.64,0,0,0,69.55,98.83L34.8,133.57A50.63,50.63,0,0,0,70.56,220h0a50.33,50.33,0,0,0,35.81-14.83l11-11a4,4,0,1,0-5.65-5.66Z"></path></svg>')}@font-face{font-display:swap;font-family:Aleo;font-style:normal;font-weight:400;src:url(fonts/aleo-v15-latin-regular.woff2) format('woff2'),url(fonts/aleo-v15-latin-regular.ttf) format('truetype')}@font-face{font-display:swap;font-family:Aleo;font-style:italic;font-weight:400;src:url(fonts/aleo-v15-latin-italic.woff2) format('woff2'),url(fonts/aleo-v15-latin-italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:Aleo;font-style:normal;font-weight:900;src:url(fonts/aleo-v15-latin-900.woff2) format('woff2'),url(fonts/aleo-v15-latin-900.ttf) format('truetype')}@font-face{font-display:swap;font-family:Aleo;font-style:italic;font-weight:900;src:url(fonts/aleo-v15-latin-900italic.woff2) format('woff2'),url(fonts/aleo-v15-latin-900italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:normal;font-weight:200;src:url(fonts/josefin-slab-v28-latin-200.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-200.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:italic;font-weight:200;src:url(fonts/josefin-slab-v28-latin-200italic.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-200italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:normal;font-weight:400;src:url(fonts/josefin-slab-v28-latin-regular.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-regular.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:italic;font-weight:400;src:url(fonts/josefin-slab-v28-latin-italic.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-italic.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:normal;font-weight:700;src:url(fonts/josefin-slab-v28-latin-700.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-700.ttf) format('truetype')}@font-face{font-display:swap;font-family:'Josefin Slab';font-style:italic;font-weight:700;src:url(fonts/josefin-slab-v28-latin-700italic.woff2) format('woff2'),url(fonts/josefin-slab-v28-latin-700italic.ttf) format('truetype')}*{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb-color) var(--scrollbar-track-color)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track-color)}::-webkit-scrollbar-thumb{background-color:var(--scrollbar-thumb-color);border-radius:var(--scrollbar-border-radius);border:var(--scrollbar-thumb-border)}::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-thumb-hover-color)}body{background-color:var(--base-50);color:var(--contrast);max-width:100vw;overflow-x:hidden;margin:0;font-family:var(--body);font-weight:var(--bWeight);font-size:var(--medium);line-height:1.4;position:relative}body b,body strong{font-weight:var(--bBold)}:target{scroll-snap-margin-top:max(6rem,20vh);scroll-margin-top:max(6rem,20vh);outline:double var(--action-0);border-radius:var(--outerRadius);padding:var(--outerPadding)}body.menu_item :target h2{background-color:var(--action-0);color:var(--action-contrast)}body,body *{transition:background-color var(--transition-base);transition-property:background-color,border}body.loading,body:has(aside.expanded),body:has(dialog[open]),body:has(nav.open){overflow:hidden}[hidden]{display:none!important}@media (max-width:767px){.hide-small{display:none}}.width-50{width:100%}.width-25{width:50%}.width-75{width:100%}.w-full{width:100%}@media (min-width:768px){.buttons li.width-50,.width-50{width:calc(50% - .3em)}.width-25{width:calc(25% - .3em)}.width-75{width:calc(75% - .3em)}}.col,.row:not(.icon){display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}.col{--dir:column}.row:not(.icon){--dir:row}.col.rev{--dir:column-reverse}.row.rev{--dir:row-reverse}.nowrap{--wrap:nowrap}.col.a-start,.row.start{--justify:flex-start}.col.a-end,.row.end{--justify:flex-end}.col.btw,.row.btw{--justify:space-between}.col.even,.row.even{--justify:space-evenly}.col.start,.row.a-start{--align:flex-start}.col.end,.row.a-end{--align:flex-end}.abs{position:absolute}:has(>.abs){position:relative}.hidden{transform:scale(0);max-width:0;max-height:0;overflow:hidden;transition:var(--transition-transform),var(--transition-size)}.visible{transform:scale(1);max-width:100%;max-height:100%;transition:var(--transition-transform),var(--transition-size)}.theme-switcher{position:absolute;opacity:0;width:0;height:0}#theme-switch{z-index:99;position:absolute;display:flex;align-items:center;justify-content:center}#theme-switch,.toggle-switch{--wrap:nowrap;cursor:pointer}#theme-switch,.toggle-switch input[type=checkbox]{--h:2rem;width:calc(var(--h) * 2);height:var(--h);margin:0 2rem 0 0;left:0;appearance:none;background:var(--base-200);border:1px solid var(--base-50);border-radius:var(--h);cursor:pointer;transition:all .3s ease;opacity:1}.toggle-switch input[type=checkbox]{position:relative}.toggle-switch{position:relative}@media (max-width:600px){#theme-switch{left:1rem}.wp-site-blocks>header{padding:0!important}}#theme-switch .icon{--w:1em;position:relative;top:0;margin:0 .25em;color:var(--contrast-200);z-index:2;transform:translateX(0)}#theme-switcher:checked~.moon,#theme-switcher:not(:checked)~.sun-dim{--w:1.5em;color:var(--contrast)}#theme-switcher:checked~.sun-dim,#theme-switcher:not(:checked)~.moon{top:-.17rem}#theme-switcher:not(:checked)~.sun-dim{color:var(--secondary-0);transform:translate(-2px,2px)}#theme-switcher:checked~.moon{transform:translate(4px,4px)}#theme-switch span,.toggle-switch input[type=checkbox]::before{--m:2px;content:"";position:absolute;top:var(--m);left:var(--m);width:calc(var(--h) - (var(--m) * 2));height:calc(var(--h) - var(--m) * 2);border:1px solid rgba(var(--contrast-rgb),.2);border-bottom:3px solid var(--contrast-200);background:var(--base-50);border-radius:50%;z-index:1;transform:rotate(360deg);transition:transform var(--transition-base),left var(--transition-base),top var(--transition-base),height var(--transition-base)}#theme-switch input:checked~span,.toggle-switch input[type=checkbox]:checked::before{left:calc(100% - (var(--h) - var(--m)));transform:rotate(-180deg);transition:transform var(--transition-base),left var(--transition-base)}.toggle-switch input[type=checkbox]:checked{background:var(--action-0)}.theme-switch:focus-visible+label{outline:2px solid var(--action-0);outline-offset:2px}#theme-switch .icon{transition:transform var(--transition-base),width var(--transition-base),height var(--transition-base),top var(--transition-base),color var(--transition-base)}#theme-switcher:checked~.icon.light,#theme-switcher:not(:checked)~.icon.dark{transform:rotate(360deg);color:var(--contrast-200)}#theme-switcher:checked~.icon.dark,#theme-switcher:not(:checked)~.icon.light{transform:rotate(-360deg);color:var(--contrast)}#theme-switch:hover span{background-color:var(--base-100)}#theme-switch:hover .icon{color:var(--action-50)}#theme-switch:active span{transform:scale(.97)}html{scroll-behavior:smooth}@media(prefers-reduced-motion){html{scroll-behavior:unset}*{transition:none!important;animation:none!important}}main{min-height:60vh}main>*{width:100%;max-width:var(--content);margin:var(--setMargin)}main>.align-wide{max-width:var(--alignWide)}main>.align-full{--ml:0;--mr:0;max-width:var(--full)}main>section{--mt:6rem}main>:first-child{margin-top:0}footer{padding:1rem 1rem var(--offHeight);background-color:var(--base-200);color:var(--contrast-200);text-align:center;margin:4rem 0 0;position:relative;z-index:var(--z-top)}footer p,footer p+p{margin:.5rem auto}@media (min-width:768px){footer{padding:1rem 2rem var(--offHeight)}}.grid-view,.item-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.grid-view .item,.item-grid .item{border-radius:var(--outerRadius);aspect-ratio:1;display:flex;filter:none;transition:filter var(--transition-base),padding var(--transition-base),background-color var(--transition-base)}.grid-view img,.item-grid img{border-radius:var(--innerRadius)}.item-grid.list-view{display:flex;flex-direction:column;gap:2rem;--gap:2rem}.item-grid.list-view .item .col{--gap:.5rem}.item-grid.list-view img{width:20%}@media (min-width:768px){.grid-view,.item-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}h1 b,h1 strong,h2 b,h2 strong,h3 b,h3 strong,h4 b,h4 strong,h5 b,h5 strong,h6 b,h6 strong{text-decoration:double;-webkit-text-fill-color:transparent;-webkit-text-stroke:2px var(--contrast)}h1,h2,h3,h4,h5,h6{--mt:1.5em;--mb:.875em;font-family:var(--heading);text-transform:uppercase;font-weight:var(--hWeight);line-height:1.3;margin:var(--mt) var(--mr) var(--mb) var(--ml)}h1.inline,h2.inline,h3.inline,h4.inline,h5.inline,h6.inline{font-size:1.2rem;font-weight:600;display:inline-block;margin:0 2rem 0 0;letter-spacing:.05em}h1.inline+*,h2.inline+*,h3.inline+*,h4.inline+*,h5.inline+*,h6.inline+*{display:inline-block;margin:.5rem 0}h1.inline+.term-list,h2.inline+.term-list,h3.inline+.term-list,h4.inline+.term-list,h5.inline+.term-list,h6.inline+.term-list{display:inline-flex;margin:.5rem 0}h1{font-size:var(--xxxlarge);font-weight:var(--hWeight);line-height:1;margin:0 var(--mr) .25em var(--ml)}h1:first-of-type{margin-top:20vh}h1 small{display:block;font-size:var(--small);font-weight:var(--bWeight);line-height:1;font-family:var(--body)}h2{font-size:var(--xxlarge)}h3{font-size:var(--xlarge)}h4{font-weight:400;font-size:var(--large)}h5,h6{font-weight:400;font-size:var(--medium)}p{line-height:1.6}p+p{margin-top:2.5rem}a{color:var(--action-0);text-decoration:none}ul a{display:inline-flex;text-decoration:none}a:visited{color:var(--action-100)}a:hover{color:var(--action-50);text-decoration:underline}.buttons{--wrap:wrap;--justify:flex-start;margin:1rem var(--mr) 1rem var(--ml);width:100%;padding:0}.buttons.fit{width:fit-content;margin:1rem 2rem}.buttons li{--justify:stretch;--align:stretch;padding:0;list-style:none;overflow:hidden}.buttons{margin:3rem auto;max-width:90%}@media (min-width:768px){.buttons{max-width:var(--content);margin:3rem var(--mr) 3rem var(--ml)}}[type=submit],a.button,a.wp-block-button__link,button{--justify:center;--align:center;--dir:row;width:fit-content;text-transform:uppercase;text-decoration:none;background-color:var(--base-100);color:var(--contrast-50);border:1px solid var(--base-200);border-radius:var(--innerRadius);padding:.25rem 1rem;font:inherit;cursor:pointer;outline:inherit;display:inline-flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir);transition:color var(--transition-base);transition-property:color,border,background-color;position:relative}.buttons a:hover,[type=submit]:focus,[type=submit]:hover,a.button:focus,a.button:hover,a.wp-block-button__link:focus,a.wp-block-button__link:hover,button:focus,button:hover{background-color:var(--action-0);color:var(--action-contrast)}[type=submit]:disabled,[type=submit]:disabled:focus,[type=submit]:disabled:hover,a.button:disabled,a.button:disabled:focus,a.button:disabled:hover,a.wp-block-button__link:disabled,a.wp-block-button__link:disabled:focus,a.wp-block-button__link:disabled:hover,button:disabled,button:disabled:focus,button:disabled:hover{opacity:.5;background-color:var(--base-200)!important;color:var(--contrast-200)!important}details .icon{--w:1.5em}button.favourite.favourited,button.voted svg{animation:favourite-pop .4s cubic-bezier(.25,.46,.45,.94)}@keyframes favourite-pop{0%{transform:scale(1)}50%{transform:scale(1.3)}75%{transform:scale(.9)}100%{transform:scale(1)}}button.filter-toggle{border:1px solid var(--base-200);background-color:transparent;white-space:nowrap;font-size:1rem;padding:.35em;--w:1.2em}.filter-toggle:hover{border-color:var(--action-50);color:var(--action-50)}.filter-toggle:focus{background-color:var(--action-50);color:var(--action-contrast)}.toggle.notifications.has .bell,.toggle.notifications:not(.has) .bell-ringing,.vote .voted .downvote,.vote .voted .upvote,.vote button:not(.voted) .downvoted,.vote button:not(.voted) .upvoted,button.favourite.favourited .heart,button.favourite:not(.favourited) .heart-fill{display:none}.toggle.notifications.has .bell-ringing,.toggle.notifications:not(.has) .bell,.vote .voted .downvoted,.vote .voted .upvoted,.vote button:not(.voted) .downvote,.vote button:not(.voted) .upvote,button.favourite.favourited .heart-fill,button.favourite:not(.favourited) .heart{display:block}.icon{width:var(--w);height:var(--w);display:inline-flex;transition:var(--transition-size),var(--transition-color)}.icon svg{width:100%;height:100%}.icon.small,nav ul .icon{--w:24px}.icon.colour{background:#b7332e;background:linear-gradient(180deg,rgba(255,0,128,1) 0,rgba(250,71,101,1) 14%,rgba(251,121,35,1) 28%,rgba(176,190,19,1) 42%,rgba(14,204,0,1) 56%,rgba(14,225,166,1) 70%,rgba(63,152,253,1) 84%,rgba(166,90,196,1) 100%);mask-image:var(--colour);-webkit-mask-image:var(--colour);-webkit-mask-repeat:no-repeat;-webkit-mask-size:contain;mask-repeat:no-repeat;mask-size:contain;width:1.25rem;height:1.25rem}.icon.logo-basic svg path{transition:fill var(--timing) var(--function)}.icon.logo-basic svg path#innerCircle,.icon.logo-basic svg path#outerSkull{fill:var(--base)}a .icon.logo-basic:hover svg path{fill:var(--base)}a .icon.logo-basic:hover svg path#innerCircle,a .icon.logo-basic:hover svg path#outerSkull{fill:var(--action-0)}.icon.grab{cursor:grab}main a .icon{margin-right:.5em}body:has(#theme-switcher:not(:checked)) .icon.logo-split-color{position:relative}body:has(#theme-switcher:not(:checked)) .icon.logo-split-color::before{content:'';display:block;width:60%;height:60%;border-radius:50%;background-color:var(--dark-200);position:absolute;left:18%;top:22%;z-index:-1}path#refresh{transform-origin:center;transform-box:fill-box;animation:spin 1s var(--function) infinite}.screen-reader-text{border:0;clip:rect(1px,1px,1px,1px);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute!important;width:1px;word-wrap:normal!important}:focus,:focus-visible,input[type=checkbox]+label:focus,input[type=checkbox]+label:focus-visible,input[type=radio]+label:focus,input[type=radio]+label:focus-visible{outline:2px solid var(--action-0)!important;outline-offset:2px!important;box-shadow:0 0 0 4px rgba(var(--action-rgb),var(--rgb-light))!important}[aria-busy=true]{cursor:progress}[aria-disabled=true],[disabled]{cursor:not-allowed;opacity:.7}details{padding:.25rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details[open]{background-color:var(--base-50)}details summary{--wrap:nowrap;list-style:none;text-transform:uppercase;cursor:pointer;border:0;transition:background-color var(--transition-base);transition-property:background-color,border;position:relative;padding:.5rem 2.5rem .5rem .5rem;gap:.5rem}details summary:hover{background-color:var(--base-100);border-color:var(--base-100);color:var(--contrast);transition:background-color var(--transition-base);transition-property:background-color,border,color}details[open]>summary{background-color:var(--base-50)}details summary::after{content:"";background-color:var(--contrast-100);-webkit-mask-repeat:no-repeat;-webkit-mask-size:contain;-webkit-mask-image:var(--details);mask-image:var(--details);mask-repeat:no-repeat;mask-size:contain;width:1.25rem;height:1.25rem;margin-left:auto;transition:background-color var(--transition-base);transition-property:background-color,transform}details summary:hover::after,details[open]>summary::after{background-color:var(--contrast)}details[open]>summary::after{transform:rotate(-540deg);transition:background-color var(--transition-base);transition-property:background-color,transform}details::details-content{opacity:0;block-size:0;overflow-y:clip;transition:content-visibility var(--timing) allow-discrete,opacity var(--timing),block-size var(--timing)}details[open]::details-content{opacity:1;block-size:auto}@media (prefers-reduced-motion:no-preference){details{interpolate-size:allow-keywords}}input[type=date],input[type=email],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=textarea],input[type=url],textarea{--p-x:1.5rem;font-family:var(--body);font-size:var(--medium);color:var(--contrast);padding:1rem var(--p-x);border-radius:var(--innerRadius);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;transition:background-color var(--transition-base);transition-property:background-color,border}input[type=email]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=textarea]:focus,input[type=url]: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)}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--innerRadius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--small);padding:.5rem 1rem;width:100%;transition:var(--transition-color)}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;transition:opacity var(--transition-base)}.search-container .clear-search{opacity:0;cursor:default;transition:opacity var(--transition-base)}.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}label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.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(--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(--innerRadius);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}[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(--innerRadius);transition:background-color var(--transition-base),border-color var(--transition-base)}[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{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;transition:transform .3s ease;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(--innerRadius);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{display:none}.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[type=url]{background:var(--link);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.field{margin:2rem 0}.toggle-text input{display:none}.toggle-text input+label{font-weight:400;color:var(--contrast)!important;text-transform:none;cursor:pointer;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.toggle-text label::after,.toggle-text label::before{display:none}.toggle-text label{padding-left:0!important}.toggle-text input+label .text{position:relative;margin:0 .5rem;font-weight:700;width:fit-content;padding:2px 4px;border:1px solid var(--action-50);border-radius:4px;color:var(--action-50)!important}table .toggle-text input+label .text{color:var(--contrast)!important;border-color:var(--contrast)}.toggle-text:hover .text,table .toggle-text:hover .text{background-color:var(--action-50);color:var(--light-0)!important;border-color:var(--action-50)}.toggle-text input+label .off,.toggle-text input+label .on{-webkit-transition:opacity .125s ease-out,-webkit-transform .125s ease-out;transition:opacity .125s ease-out,-webkit-transform .125s ease-out;transition:transform .125s ease-out,opacity .125s ease-out;transition:transform .125s ease-out,opacity .125s ease-out,-webkit-transform .125s ease-out}.toggle-text input+label .off{opacity:1;max-width:100%;-webkit-transform:none;transform:none}.toggle-text input+label .on{opacity:0;max-width:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.toggle-text input:checked+label .off{opacity:0;max-width:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.toggle-text input:checked+label .on{max-width:100%;opacity:1;-webkit-transform:none;transform:none}.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{--gap:nowrap}.quantity{margin:0}.quantity label{margin:0;font-size:var(--small)}.quantity{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 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}.term-list{--justify:flex-start;--align:center;--wrap:nowrap;--gap:.5rem;--w:1em;margin:0;padding:0;height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir);position:relative;overflow:auto hidden;touch-action:pan-x;text-transform:lowercase}dialog::backdrop{backdrop-filter:blur(5px);background-color:var(--overlay-medium)}dialog[open]{z-index:999;--padding:0;top:0;width:min(500px,95vw);border-radius:1rem;height:fit-content;max-height:90vh;overflow:hidden;padding:var(--padding);background-color:var(--base-50);color:var(--contrast);border:1px solid var(--base-200);box-shadow:var(--shadow)}dialog>.wrap,dialog>form{overflow:hidden auto;max-height:100%;margin:1.5rem 0 0 1.5rem;padding-right:1.2rem;width:calc(100% - 1.5rem - 1.2rem)}dialog label{font-weight:400}dialog h2,dialog h3{margin:0 0 .5rem 0;font-size:var(--large)}dialog:has(.m-actions){padding-bottom:var(--height)}.m-actions{--w:1.5em;--justify:flex-end;--wrap:nowrap;--gap:0;position:absolute;bottom:0;left:0;right:0;width:100%;z-index:var(--z-6);background-color:var(--action-100);box-shadow:var(--shadow-up)}.m-actions button{width:100%;height:3rem;border-radius:0;color:var(--action-contrast);background-color:var(--action-50);border:2px solid var(--action-50)}.m-actions button:focus,.m-actions button:hover{background-color:var(--base);color:var(--contrast)}.m-actions button:first-of-type{border-bottom-left-radius:1rem}.m-actions button:last-of-type{border-bottom-right-radius:1rem}dialog ul{list-style:none}dialog .search-container{padding-top:1rem;width:100%;gap:.5rem}dialog[open].gallery{width:calc(100vw - var(--padding) * 2);height:99vh;background:var(--overlay-heavy)}.gallery .content{position:relative;max-width:100%;max-height:100%;padding:2rem}.gallery .favourite button.favourite{top:unset;bottom:1rem;right:1rem}.gallery .image{max-width:100%;max-height:calc(100vh - 4rem);object-fit:contain}.gallery .cancel{position:absolute;top:1rem;right:1rem;background:0 0;border:none;color:#fff;cursor:pointer;padding:.5rem;z-index:10;transition:color .3s ease}.gallery .cancel:hover{color:var(--action-0)}.gallery .nav{position:absolute;top:50%;height:50%;z-index:5;transform:translateY(-50%);border:none;color:var(--contrast);cursor:pointer;padding:1rem;transition:color .3s ease}.gallery .nav:hover{background-color:var(--overlay-heavy)}.gallery .nav:hover{color:var(--action-0)}.gallery .prev{left:1rem}.gallery .next{right:1rem}.gallery .counter{position:absolute;top:1rem;left:1rem;color:#fff;font-size:.875rem}.gallery .content details{position:absolute;bottom:1rem;left:2rem;width:calc(100% - 4rem);background-color:var(--overlay-light);padding:0}.gallery .content details:hover,.gallery .content details[open]{background-color:var(--overlay-heavy);backdrop-filter:blur(5px)}.gallery .content details[open] summary{background-color:transparent}table{white-space:nowrap;width:100%;display:block;margin:0 0 2rem;border-radius:4px;height:var(--maxHeight);overflow:auto;position:relative}tfoot,thead{position:sticky;z-index:10;background-color:var(--base);text-transform:uppercase;padding:.5rem 0;line-height:2;font-weight:400}tr:nth-of-type(even){background-color:var(--base-200)}tfoot th{vertical-align:middle}tfoot th:first-of-type{text-align:right}tfoot tr,thead tr{background-color:var(--overlay-heavy);box-shadow:var(--shadow)}thead tr{border-bottom:1px solid var(--contrast-200)}tfoot tr{border-top:1px solid var(--contrast-200)}thead{top:0}tfoot{bottom:0}thead th{width:max-content}th p{margin:0!important}td{width:max-content;padding:.5rem 1rem}td .toggle input[type=checkbox]{margin:0}td .field{margin:.25rem 0}td[data-id=actions] label{margin:0;padding:0}td .description{display:none}td input[type=text]{width:fit-content;max-width:40vw;padding:.25em!important;font-size:var(--small)!important}tbody tr{border:2px solid transparent}tbody tr:focus-within{background-color:var(--base-100);border-color:var(--action-50)}[data-stuck]{background-color:var(--overlay-medium);position:sticky;left:-1rem;z-index:15;box-shadow:var(--subtleRight)}tbody [data-stuck]{z-index:5}tfoot [data-stuck],thead [data-stuck]{background:var(--base)}blockquote{padding:var(--outerPadding);border-radius:var(--outerRadius);background-color:var(--base-50)}cite{width:90%;margin:1rem auto}.hide-tooltip.hide-tooltip.hide-tooltip+[role=tooltip],[role=tooltip]{visibility:hidden;position:absolute;bottom:2rem;left:1rem;width:max-content;height:fit-content;max-width:50vw;padding:.5rem;border-radius:var(--innerRadius);box-shadow:var(--shadow);background:var(--action-0);color:var(--action-contrast)}body.menu_item [role=tooltip]{left:auto;right:100%;top:-200%;z-index:var(--z-4)}[role=tooltip] p{margin:0}[role=tooltip] p+p{margin-top:.5rem}.field:has([aria-describedby]:focus) [role=tooltip],[aria-describedby]:focus~.has-tooltip[role=tooltip],[aria-describedby]:hover~.has-tooltip [role=tooltip]{visibility:visible;display:block}.has-tooltip{display:inline-flex;justify-content:flex-end;position:relative;--w:1.5rem}.tt-toggle{cursor:pointer;display:flex;border-radius:50%;background-color:transparent}.tt-toggle:focus,.tt-toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}.tt-toggle:focus+[role=tooltip],.tt-toggle:hover+[role=tooltip]{visibility:visible}dialog[open]#jvb-selector{height:70vh;top:15vh;display:flex}#jvb-selector>.wrap{flex:1}dialog.loading{opacity:0;transition:opacity var(--transition-base)}dialog.loading[open]{opacity:1;transition:opacity var(--transition-base);width:100vw;height:100vh;display:flex;max-width:100%;max-height:100%;border-radius:0;border:none;background-color:transparent;box-shadow:none;--w:3em;justify-content:center;align-items:center}dialog.loading[open]@starting-style{opacity:0}dialog.loading[open]>.col{height:fit-content;width:min(400px,60vw);border-radius:var(--outerRadius);background-color:var(--overlay-medium);padding:2rem;box-shadow:var(--shadow);position:relative}dialog.loading[open] .spinner{position:absolute;top:1rem;width:5rem;height:5rem;border-width:0;border-top-width:4px;animation:spin 1s var(--function) infinite}.loading[open] .icon{color:var(--action-0)}dialog.loading[open] svg{animation:dance 2s ease-in-out infinite;transition:color .3s ease}dialog.loading[open] h3{color:var(--contrast);margin:2rem 1rem auto!important;font-size:var(--large);width:-moz-fit-content;width:fit-content}dialog.loading[open] p{margin:.5rem auto}dialog.loading[open]::after{animation:shimmer 3s ease-in-out infinite;background:linear-gradient(90deg,var(--shimmer));content:"";inset:0;position:absolute;z-index:-1}.spinner{width:12px;height:12px;border:2px solid transparent;border-top:2px solid var(--action-50);border-radius:50%;animation:spin 1s var(--function) infinite}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes shimmer{0%{left:-50%}50%{left:150%}100%{left:-50%}}@keyframes dance{0%,100%{transform:rotate(-5deg) scale(1)}50%{transform:rotate(5deg) scale(1.1)}}@keyframes letterOutline{0%{background-size:1ch 0}100%{background-size:1ch 100%}}@keyframes letterInside{0%,50%{background-position-y:100%,0}100%,50.01%{background-position-y:0,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(--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;border:none;position:relative}.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}.toggle-details{gap:2px}body.menu_item #top{z-index:var(--z-4);position:relative}section .toggle-details{position:absolute;right:0;top:5rem}[data-toggle=all]{position:fixed;bottom:calc(var(--offHeight) + var(--height) + .5rem);right:0;z-index:var(--z-4);background-color:var(--action-0);color:var(--action-contrast)}[data-toggle]{z-index:var(--z-1)}body:has(#queue[hidden]) [data-toggle=all]{left:1rem}dialog:not([open]).col,dialog:not([open]).row{display:none}@media (min-width:768px){section .toggle-details{right:-10%}}.typeText::after{content:'|';display:inline-block;margin-left:0;animation:blink .75s step-end infinite}@keyframes blink{from,to{opacity:1}50%{opacity:0}}aside#cart,aside#queue{position:fixed;top:var(--doubleHeight);bottom:var(--offHeight);width:min(500px,calc(100vw - 2rem));background-color:var(--base);z-index:var(--z-5);box-shadow:var(--shadow);padding-bottom:var(--height);overflow:visible}.create-item,.qtoggle,.toggle-cart{z-index:var(--z-6);position:fixed;bottom:var(--offHeight);width:var(--height);height:var(--height);background-color:var(--overlay-medium);color:var(--contrast);transition:width var(--transition-base),background-color var(--transition-base),color var(--transition-base),left var(--transition-base);box-shadow:var(--shadow)}.create-item:focus,.create-item:hover,.qtoggle:focus,.qtoggle:hover,.toggle-cart:focus,.toggle-cart:hover{background-color:rgba(var(--action-rgb),var(--rgb-heavy));color:var(--action-contrast)}.create-item:disabled,.create-item:disabled:focus,.create-item:disabled:hover,.qtoggle:disabled,.qtoggle:disabled:focus,.qtoggle:disabled:hover,.toggle-cart:disabled,.toggle-cart:disabled:focus,.toggle-cart:disabled:hover{opacity:.5;background-color:var(--overlay-light);color:var(--contrast)}.create-item,.toggle-cart{right:0;border-radius:4px 4px 4px var(--outerRadius)}body:has(#cart.expanded) .toggle-cart{width:min(500px,calc(100vw - 2rem))}body:has(#cart.expanded) .toggle-cart .icon{display:none}aside#cart{overflow:hidden;right:var(--offScreen);border-radius:var(--outerRadius) 0 0 var(--outerRadius);transition:right var(--transition-base);padding-bottom:6rem}aside#cart.expanded{right:0;transition:right var(--transition-base)}#cart form{max-height:100%;overflow:hidden auto}#cart nav.tabs{z-index:var(--z-6);top:0}#cart table{height:auto}#cart th{padding:0 1.5rem}#cart table th:first-of-type{width:100%}#cart nav.tabs{position:sticky;box-shadow:var(--shadow)}#cart button[data-tab]{flex:1;border-radius:0}#cart form>:not(.tabs){max-width:90%;margin:0 auto}#cart form .empty p{margin:.5rem 0!important}#cart .cart-total.cart-total{--gap:0 1rem;padding-right:1rem;position:absolute;bottom:var(--height);width:100%;max-width:100%;background-color:var(--overlay-heavy);z-index:var(--z-6);box-shadow:var(--shadow-up)}.cart-total p{--gap:2rem;max-width:100%;margin:0}.cart-total p span{width:6rem;display:inline-block;text-align:right}.cart-total p+p{font-weight:700}.cart-items .total{font-weight:700}#cart .restored{background-color:rgba(var(--action-rgb),var(--rgb-light));border-radius:var(--outerRadius);padding:1rem}.restored h3{font-size:var(--medium);margin:0}.restored p{margin:0}.restored .row{--gap:0;--wrap:nowrap;--w:1em}.toasts{position:fixed;top:4rem;right:-350px;z-index:1000;width:350px}.toast{background-color:var(--overlay-heavy);border-left:4px solid var(--action-0);padding:1rem;box-shadow:var(--shadow);left:0;position:relative;opacity:0;transition:left .3s,opacity .3s}.toast.success{border-left-color:var(--success)}.toast.error{border-left-color:var(--error)}.toast.info{border-left-color:var(--warning)}.toast.show{left:calc(-350px - 1rem);opacity:1}.toast.hiding{left:0;opacity:0}.toast-content p{margin:0}.close-toast{background:0 0;border:none;font-size:1.25rem;cursor:pointer;opacity:.5;transition:opacity .2s;color:inherit}.close-toast:hover{opacity:1}aside#queue{left:var(--offScreen);border-radius:0 var(--outerRadius) var(--outerRadius) 0;transition:left var(--transition-base);--wrap:nowrap;--align:stretch}aside#queue.expanded{left:0;overflow:hidden auto}.qtoggle{left:0;border-radius:4px 4px var(--outerRadius) 4px}body:has(#queue.expanded) .qtoggle{left:var(--height);width:min(calc(500px - var(--height)),calc(100vw - 2rem - var(--height)))}.qtoggle.saving svg{color:var(--action-0);animation:spin .87s var(--function) infinite}#queue .status-actions{position:absolute;bottom:0;left:0;right:0;z-index:var(--z-2)}#queue .status-actions .popup{position:absolute;z-index:-1;width:max-content;max-width:300px;background-color:var(--action-50);color:var(--action-contrast);border-radius:var(--innerRadius);padding:.25em .75em;top:1rem;left:-100vw;transition:left var(--transition-base)}aside#queue .popup::before{content:'';width:10px;height:10px;transform:rotate(-45deg);background-color:var(--action-50);z-index:-1;left:-5px;position:absolute;top:calc(50% - 5px)}.expanded#queue .status-actions .popup.showing{left:calc(100% + 1em)}#queue .status-actions .popup.showing{left:calc(200vw + var(--offHeight));max-width:75vw}#queue .item .status,.filter .count,.qtoggle .count,.qtoggle .indicator,.refresh .countdown{z-index:var(--z-3);--offset:0;position:absolute;top:var(--offset);background-color:var(--overlay-light)}.expanded+.qtoggle .count,.expanded+.qtoggle .indicator{--offset:.25rem}.qtoggle .indicator{right:var(--offset);width:.75rem;height:.75rem;border-radius:50%}aside#queue.synced+.qtoggle .indicator{background-color:var(--success)}aside#queue.pending+.qtoggle .indicator{background-color:var(--warning);animation:pulse 2s infinite}aside#queue.pending:not(.expanded)+.qtoggle svg{color:var(--error);animation:spin 1s var(--function) infinite}.qtoggle .count{--align:center;--justify:center;left:var(--offset);min-width:1.25rem;height:1.25rem;padding:0 4px;color:var(--contrast);border-radius:var(--innerRadius);font-size:var(--extra-small)}#queue:has(.empty-queue)+.qtoggle .count{display:none}aside#queue .header{padding:15px;border-bottom:1px solid var(--base-200);flex-shrink:0}.qitems{flex:1;overflow:hidden auto;padding:.5rem 2rem;--gap:.5rem}aside#queue h3{margin:0 0 12px 0;font-size:16px;color:var(--contrast)}#queue .filters .filter{background-color:transparent;white-space:nowrap;font-size:var(--small)}#queue .filters .filter.active{background:var(--base-200);border-color:transparent}#queue .filter:focus,#queue .filter:hover{background-color:var(--action-0);color:var(--action-contrast)}.filter .count{--offset:-8px;right:var(--offset);background:var(--base-200);color:var(--contrast-200);border-radius:10px;min-width:18px;height:18px;font-size:10px}.filter .count:empty{display:none}.empty-queue{height:100px;color:var(--contrast-200);font-size:var(--small);font-style:italic}.refresh .countdown:not(.counting),aside#queue:has(.empty-queue) .refresh .count{display:none}#queue .item{padding:15px;background:var(--base-100);border-radius:var(--innerRadius);transition:all .2s ease;box-shadow:var(--shadow-none)}#queue .item:hover{box-shadow:var(--shadow)}#queue .item .header{position:relative}#queue .item .type{font-size:var(--small)}#queue .item .status{--w:1em;--gap:0;--justify:center;--align:center;--offset:-1.2rem;aspect-ratio:1;right:var(--offset);border-radius:50%;color:var(--contrast-200);background-color:var(--base-50);border:1px solid var(--base-200);width:1.25em;height:1.25em}#queue .item .status.pending{background:var(--base-100);color:var(--contrast-200)}#queue .item .status.processing{background:var(--base-200);color:var(--contrast-100);animation:pulse-color 2s infinite}#queue .item .status.completed{background:var(--base-50);color:var(--base-200)}#queue .item .status.completed:hover{color:var(--contrast-200)}#queue .item .status.failed{background:var(--base);color:var(--error)}#queue .item button{font-size:16px;padding:0;line-height:1;opacity:.5;transition:opacity .2s}#queue .item button:hover{opacity:1}#queue .item .info{margin-top:8px;font-size:var(--small)}#queue .item .info .time{--gap:7px;font-size:10px}#queue .item .actions{margin-top:12px;--gap:8px}#queue .item .actions button{padding:6px 12px;font-size:12px;background:var(--base-200);border:none;border-radius:4px;cursor:pointer;transition:all .2s;color:var(--contrast)}#queue .item .actions .retry{background-color:var(--secondary-200);color:var(--secondary-contrast)}#queue .item .actions button:hover{opacity:.9}.queue-actions{padding:15px;border-top:1px solid var(--base-200);flex-shrink:0}.queue-actions button{padding:8px 12px;font-size:var(--small);transition:all .2s}.status-actions>.refresh{position:relative;font-size:var(--small)}.refresh .countdown{--justify:center;--align:center;--offset:0;right:var(--offset);margin:0 3px;border-radius:50%;border:1px solid var(--base-200)}.refreshNow{width:var(--height);height:var(--height)}.refreshNow:hover{background:var(--base-200);color:var(--contrast-200)}.icon.refresh{--w:18px}#queue.pending.expanded .refreshNow svg{animation:spin 1.5s var(--function) infinite}#queue,.item-grid{counter-reset:delay-counter}.item{counter-increment:delay-counter}.item .progress .fill::after{--delay:calc(counter(delay-counter) * .1s)}.progress .bar{height:6px;display:block;border-radius:6px;overflow:hidden;background:var(--base-200);position:relative}.progress .fill{height:100%;background:var(--action-0);border-radius:6px;width:0;transition:width .3s ease}.progress .details{margin-top:5px;font-size:var(--small);color:var(--contrast);text-align:center;padding:.25rem 0}.progress .details:empty{display:none}.pending .fill::after,.processing .fill::after,.queued .fill::after,.uploading .fill::after{--delay:0s;content:'';position:absolute;top:0;left:-50%;width:30%;height:100%;background:linear-gradient(90deg,rgba(255,255,255,0) 0,rgba(255,255,255,.225) 50%,rgba(255,255,255,0) 100%);animation:shimmer 2.5s infinite linear var(--delay)}@keyframes shimmer{0%{left:-50%}50%{left:150%}100%{left:-50%}}@keyframes pulse-color{0%{box-shadow:0 0 0 0 rgba(var(--secondary-rgb),.4)}70%{box-shadow:0 0 0 6px rgba(var(--secondary-rgb),0)}100%{box-shadow:0 0 0 0 rgba(var(--secondary-rgb),0)}}@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(20px)}}@keyframes detect-scroll{from,to{--can-scroll:1}}.menu-items .menu-item{display:grid;grid-template-columns:repeat(3,1fr);gap:0 1rem}.menu-items .menu-item:not(.variable) label{display:none}.menu-items .menu-item .field{margin:0;--wrap:nowrap}.menu-items .menu-item .has-tooltip{position:absolute;right:-2.5rem}.menu-items .menu-item+.menu-item{border-top:1px solid var(--base-200);margin-top:2rem;padding-top:1rem}.menu-items .menu-item .header{grid-column:1/-1}.menu-items .menu-item .description{grid-column:1/3}.menu-items .menu-item .info{grid-column:3/3}.menu-items .menu-item h3{font-size:var(--medium);font-weight:400;margin:0 0 .5rem 0!important}.menu-items .menu-item .info{--gap:1rem}.price>span{vertical-align:super;font-size:12px}body.menu_item section h2{display:inline-block;max-width:var(--content);width:max-content;background-color:var(--base-50);color:var(--action-0);position:relative;z-index:5;padding:0 1rem;margin:var(--mt) auto var(--mb) auto}.menu-section{position:relative}.menu-section hr{position:absolute;width:100%;left:-5%;top:3.5rem;border:none;background-color:var(--action-100);height:2px}details.menu-item summary.row{flex-direction:column;align-items:flex-start}details.menu-item summary .row{width:100%}.menu_item h1:first-of-type{margin-top:10vh!important}@media (min-width:768px){.menu-section hr{width:120%;left:-10%;top:4.25rem}.menu_item section{max-width:var(--content)}}/*!** Forms **!*//*!*.field.time_open,*!*//*!*.field.time_closes,*!*//*!*.field.date_start,*!*//*!*.field.time_start,*!*//*!*.field.time_end {*!*//*!*    margin-bottom: 0;*!*//*!*}*!*//*!*.field.time_open,*!*//*!*.field.time_closes,*!*//*!*.field.time_start,*!*//*!*.field.time_end {*!*//*!*    width: 49%;*!*//*!*    display: inline-block;*!*//*!*    margin-top: 1rem;*!*//*!*}*!*//*!* Style for disabled state *!*//*!** Shop Page **!*//*!** Bio Sections **!*//*!*!* Status notification *!*//*!*.status-notification {*!*//*!*    position: fixed;*!*//*!*    bottom: 20px;*!*//*!*    left: 80px; !* Position to the right of the panel *!*!*//*!*    width: 300px;*!*//*!*    max-width: calc(100vw - 100px);*!*//*!*    border-radius: 8px;*!*//*!*    padding: 15px;*!*//*!*    background: #323232;*!*//*!*    color: white;*!*//*!*    transform: translateY(20px);*!*//*!*    opacity: 0;*!*//*!*    transition: transform .3s, opacity .3s;*!*//*!*    z-index: 10000;*!*//*!*    box-shadow: 0 4px 20px rgba(0, 0, 0, .2);*!*//*!*    pointer-events: none;*!*//*!*}*!*//*!*.status-notification.active {*!*//*!*    transform: translateY(0);*!*//*!*    opacity: 1;*!*//*!*    pointer-events: auto;*!*//*!*}*!*//*!*.status-notification .title {*!*//*!*    font-weight: 600;*!*//*!*    margin-bottom: 5px;*!*//*!*    font-size: 15px;*!*//*!*}*!*//*!*.status-notification .message {*!*//*!*    margin-bottom: 10px;*!*//*!*    font-size: 14px;*!*//*!*}*!*//*!*.status-notification .actions {*!*//*!*    display: flex;*!*//*!*    justify-content: flex-end;*!*//*!*}*!*//*!*.status-notification .actions button {*!*//*!*    padding: 6px 12px;*!*//*!*    background: rgba(255, 255, 255, .2);*!*//*!*    border: none;*!*//*!*    border-radius: 4px;*!*//*!*    color: white;*!*//*!*    cursor: pointer;*!*//*!*    font-size: 13px;*!*//*!*    transition: background .2s;*!*//*!*}*!*//*!*.status-notification .actions button:hover {*!*//*!*    background: rgba(255, 255, 255, .3);*!*//*!*}*!*//*!* Progress containers in notifications *!*//*!* Collapsed state - just show the toggle button *!*//*!***//*!***//*!*.new-term-toggle:disabled + .loader,*!*//*!*.loading .loader {*!*//*!*    width: 50px;*!*//*!*    aspect-ratio: 1;*!*//*!*    display: grid;*!*//*!*    border: 4px solid #0000;*!*//*!*    border-radius: 50%;*!*//*!*    border-right-color: var(--action-0);*!*//*!*    animation: l15 1s infinite linear;*!*//*!*}*!*//*!*.new-term-toggle:disabled + .loader::before,*!*//*!*.new-term-toggle:disabled + .loader::after,*!*//*!*.loading .loader::before,*!*//*!*.loading .loader::after {*!*//*!*    content: "";*!*//*!*    grid-area: 1/1;*!*//*!*    margin: 2px;*!*//*!*    border: inherit;*!*//*!*    border-radius: 50%;*!*//*!*    animation: l15 2s infinite;*!*//*!*}*!*//*!*.new-term-toggle:disabled + .loader::after,*!*//*!*.loading .loader::after {*!*//*!*    margin: 8px;*!*//*!*    animation-duration: 3s;*!*//*!*}*!*//*!*@keyframes l15{*!*//*!*    100%{transform: rotate(1turn)}*!*//*!*}*!*//*!* High contrast mode support *!*//*!** TODO: Verify **!*//*!* Icon styling in form fields *!*//*!* Required field asterisk *!*//*!* Invalid field styling *!*//*!* Frontend Display *!*//*!* Set and Checkbox Field Display *!*//*!* Radio and Select Field Display *!*//*!* True/False Field Display *!*//*!* Group Field Styling *!*//*!* Responsive Design *!*/
diff --git a/assets/js/admin/seo-admin.js b/assets/js/admin/seo-admin.js
new file mode 100644
index 0000000..1cfa2c6
--- /dev/null
+++ b/assets/js/admin/seo-admin.js
@@ -0,0 +1,344 @@
+/**
+ * JVBase SEO Admin Interface
+ *
+ * Handles:
+ * - Tab navigation
+ * - Dynamic schema field loading
+ * - Save/reset functionality
+ * - Repeater field management
+ */
+(function($) {
+	'use strict';
+
+	const SEOAdmin = {
+		config: window.jvbSeoConfig || {},
+
+		init() {
+			this.bindTabs();
+			this.bindSchemaTypeChange();
+			this.bindSaveButtons();
+			this.bindResetButtons();
+			this.bindRepeaterFields();
+			this.bindImageSelectors();
+		},
+
+		/**
+		 * Tab navigation
+		 */
+		bindTabs() {
+			$('.jvb-seo-tabs .tab-btn').on('click', function() {
+				const tab = $(this).data('tab');
+
+				// Update active tab button
+				$('.jvb-seo-tabs .tab-btn').removeClass('active');
+				$(this).addClass('active');
+
+				// Show corresponding content
+				$('.tab-content').removeClass('active');
+				$(`.tab-content[data-tab="${tab}"]`).addClass('active');
+			});
+		},
+
+		/**
+		 * Dynamic schema field loading when type changes
+		 */
+		bindSchemaTypeChange() {
+			$(document).on('change', '.schema-type-select', async function() {
+				const $select = $(this);
+				const type = $select.val();
+				const $fieldset = $select.closest('.jvb-seo-fieldset');
+				const $schemaFields = $fieldset.find('.schema-fields');
+
+				if (!type) {
+					$schemaFields.hide().empty();
+					return;
+				}
+
+				// Fetch fields for this schema type
+				try {
+					const response = await fetch(
+						`${SEOAdmin.config.restUrl}schema-fields/${type}`,
+						{
+							headers: {
+								'X-WP-Nonce': SEOAdmin.config.nonce
+							}
+						}
+					);
+
+					const data = await response.json();
+
+					if (data.fields) {
+						SEOAdmin.renderSchemaFields($schemaFields, data.fields);
+						$schemaFields.show();
+					}
+				} catch (error) {
+					console.error('Failed to load schema fields:', error);
+				}
+			});
+		},
+
+		/**
+		 * Render schema field mappings
+		 */
+		renderSchemaFields($container, fields) {
+			$container.empty();
+
+			Object.entries(fields).forEach(([key, config]) => {
+				const $field = $(`
+                    <div class="form-field schema-field-mapping" data-field="${key}">
+                        <label>${config.label || key}</label>
+                        <input type="text" name="schema_fields[${key}]"
+                               value="" class="regular-text template-field"
+                               placeholder="{{field_name}}">
+                        ${config.description ? `<span class="description">${config.description}</span>` : ''}
+                    </div>
+                `);
+				$container.append($field);
+			});
+		},
+
+		/**
+		 * Save button handlers
+		 */
+		bindSaveButtons() {
+			// Save site config
+			$('#save-site-config').on('click', async function() {
+				const $btn = $(this);
+				$btn.prop('disabled', true).text('Saving...');
+
+				const data = SEOAdmin.collectSiteFormData();
+
+				try {
+					const response = await fetch(`${SEOAdmin.config.restUrl}site`, {
+						method: 'POST',
+						headers: {
+							'Content-Type': 'application/json',
+							'X-WP-Nonce': SEOAdmin.config.nonce
+						},
+						body: JSON.stringify(data)
+					});
+
+					const result = await response.json();
+
+					if (result.success) {
+						SEOAdmin.showNotice('Settings saved successfully.', 'success');
+					} else {
+						SEOAdmin.showNotice(result.message || 'Failed to save.', 'error');
+					}
+				} catch (error) {
+					SEOAdmin.showNotice('Network error. Please try again.', 'error');
+				} finally {
+					$btn.prop('disabled', false).text('Save Site Settings');
+				}
+			});
+
+			// Save content type config
+			$(document).on('click', '.save-content-type', async function() {
+				const $btn = $(this);
+				const $container = $btn.closest('.jvb-seo-content-type');
+				const type = $container.data('type');
+				const objectType = $container.data('object-type');
+
+				$btn.prop('disabled', true).text('Saving...');
+
+				const data = SEOAdmin.collectContentTypeFormData($container);
+
+				try {
+					const response = await fetch(
+						`${SEOAdmin.config.restUrl}content/${objectType}/${type}`,
+						{
+							method: 'POST',
+							headers: {
+								'Content-Type': 'application/json',
+								'X-WP-Nonce': SEOAdmin.config.nonce
+							},
+							body: JSON.stringify(data)
+						}
+					);
+
+					const result = await response.json();
+
+					if (result.success) {
+						SEOAdmin.showNotice('Settings saved.', 'success');
+					} else {
+						SEOAdmin.showNotice(result.message || 'Failed to save.', 'error');
+					}
+				} catch (error) {
+					SEOAdmin.showNotice('Network error.', 'error');
+				} finally {
+					$btn.prop('disabled', false).text('Save');
+				}
+			});
+		},
+
+		/**
+		 * Reset button handlers
+		 */
+		bindResetButtons() {
+			$(document).on('click', '.reset-to-defaults', async function() {
+				if (!confirm('Reset to default settings? This cannot be undone.')) {
+					return;
+				}
+
+				const $btn = $(this);
+				const $container = $btn.closest('.jvb-seo-content-type');
+				const type = $container.data('type');
+				const objectType = $container.data('object-type');
+
+				$btn.prop('disabled', true).text('Resetting...');
+
+				try {
+					const response = await fetch(
+						`${SEOAdmin.config.restUrl}content/${objectType}/${type}/reset`,
+						{
+							method: 'POST',
+							headers: {
+								'X-WP-Nonce': SEOAdmin.config.nonce
+							}
+						}
+					);
+
+					const result = await response.json();
+
+					if (result.success) {
+						// Reload page to show defaults
+						location.reload();
+					} else {
+						SEOAdmin.showNotice(result.message || 'Failed to reset.', 'error');
+					}
+				} catch (error) {
+					SEOAdmin.showNotice('Network error.', 'error');
+				} finally {
+					$btn.prop('disabled', false).text('Reset to Defaults');
+				}
+			});
+		},
+
+		/**
+		 * Repeater field management
+		 */
+		bindRepeaterFields() {
+			// Add row
+			$(document).on('click', '.repeater-field .add-row', function() {
+				const $repeater = $(this).closest('.repeater-field');
+				const fieldName = $repeater.data('field');
+
+				const $row = $(`
+                    <div class="repeater-row">
+                        <input type="url" name="${fieldName}[]" value="" class="regular-text">
+                        <button type="button" class="button remove-row">×</button>
+                    </div>
+                `);
+
+				$repeater.find('.add-row').before($row);
+			});
+
+			// Remove row
+			$(document).on('click', '.repeater-field .remove-row', function() {
+				$(this).closest('.repeater-row').remove();
+			});
+		},
+
+		/**
+		 * WordPress Media Library image selector
+		 */
+		bindImageSelectors() {
+			$(document).on('click', '.select-image', function(e) {
+				e.preventDefault();
+
+				const $button = $(this);
+				const $field = $button.siblings('input[type="hidden"]');
+				const $preview = $button.siblings('.image-preview');
+
+				const frame = wp.media({
+					title: 'Select Image',
+					button: { text: 'Use Image' },
+					multiple: false
+				});
+
+				frame.on('select', function() {
+					const attachment = frame.state().get('selection').first().toJSON();
+					$field.val(attachment.id);
+					$preview.html(`<img src="${attachment.sizes?.thumbnail?.url || attachment.url}" style="max-height:50px;">`);
+				});
+
+				frame.open();
+			});
+		},
+
+		/**
+		 * Collect site form data
+		 */
+		collectSiteFormData() {
+			const $form = $('[data-tab="site"]');
+			const data = {};
+
+			// Single fields
+			$form.find('input[name], textarea[name], select[name]').each(function() {
+				const name = $(this).attr('name');
+				if (!name.endsWith('[]')) {
+					data[name] = $(this).val();
+				}
+			});
+
+			// Repeater fields (sameAs)
+			$form.find('[name="organization_sameas[]"]').each(function() {
+				if (!data.organization_sameas) {
+					data.organization_sameas = [];
+				}
+				const val = $(this).val();
+				if (val) {
+					data.organization_sameas.push(val);
+				}
+			});
+
+			return data;
+		},
+
+		/**
+		 * Collect content type form data
+		 */
+		collectContentTypeFormData($container) {
+			const data = {
+				meta_title: $container.find('[name="meta_title"]').val(),
+				meta_description: $container.find('[name="meta_description"]').val(),
+				schema_type: $container.find('[name="schema_type"]').val(),
+				schema_fields: {}
+			};
+
+			// Collect schema field mappings
+			$container.find('[name^="schema_fields"]').each(function() {
+				const match = $(this).attr('name').match(/schema_fields\[(\w+)\]/);
+				if (match) {
+					data.schema_fields[match[1]] = $(this).val();
+				}
+			});
+
+			return data;
+		},
+
+		/**
+		 * Show notice message
+		 */
+		showNotice(message, type = 'info') {
+			const $notice = $(`
+                <div class="notice notice-${type} is-dismissible">
+                    <p>${message}</p>
+                </div>
+            `);
+
+			// Remove existing notices
+			$('.jvb-seo-admin .notice').remove();
+
+			// Add new notice
+			$('.jvb-seo-admin').prepend($notice);
+
+			// Auto-dismiss after 5 seconds
+			setTimeout(() => $notice.fadeOut(), 5000);
+		}
+	};
+
+	// Initialize when DOM is ready
+	$(document).ready(() => SEOAdmin.init());
+
+})(jQuery);
diff --git a/assets/js/dash/A11yHelper.js b/assets/js/concise/A11yHelper.js
similarity index 100%
rename from assets/js/dash/A11yHelper.js
rename to assets/js/concise/A11yHelper.js
diff --git a/assets/js/concise/AuthManager.js b/assets/js/concise/AuthManager.js
new file mode 100644
index 0000000..dfad8bf
--- /dev/null
+++ b/assets/js/concise/AuthManager.js
@@ -0,0 +1,290 @@
+/**
+ * AuthManager - Handles user authentication state
+ *
+ * Responsibilities:
+ * - Fetch and cache authentication state from /auth/status
+ * - Store auth data in sessionStorage to reduce API requests
+ * - Invalidate cache when WordPress cookie changes
+ * - Provide auth data through class properties
+ * - Emit events for auth state changes
+ */
+class AuthManager {
+	constructor() {
+		this.initialized = false;
+		this.isAuthenticating = false;
+		this.authenticated = false;
+		this.user = false;
+		this.nonces = {};
+
+		this.subscribers = new Set();
+		this.storageKey = 'jvb_auth_state';
+		this.cacheMetaKey = 'jvb_auth_meta';
+		this.cacheExpiry = 5 * 60 * 1000; // 5 minutes
+
+		this.init();
+	}
+
+	/**
+	 * Initialize authentication
+	 */
+	async init() {
+		if (this.isAuthenticating) {
+			// Wait for existing auth to complete
+			return new Promise(resolve => {
+				const checkAuth = setInterval(() => {
+					if (this.initialized) {
+						clearInterval(checkAuth);
+						resolve();
+					}
+				}, 50);
+			});
+		}
+
+		this.isAuthenticating = true;
+
+		try {
+			// Check if we have cached auth and cookie hasn't changed
+			const cached = this.getCachedAuth();
+			if (cached) {
+				this.setAuthData(cached);
+				this.initialized = true;
+				this.isAuthenticating = false;
+				this.notify('auth-loaded', { fromCache: true });
+				return;
+			}
+
+			// Fetch fresh auth data
+			await this.fetchAuth();
+
+		} catch (error) {
+			console.error('Failed to initialize auth:', error);
+			this.clearAuthData();
+			this.initialized = true;
+			this.isAuthenticating = false;
+			this.notify('auth-error', { error });
+		}
+	}
+
+	/**
+	 * Fetch authentication status from API
+	 */
+	async fetchAuth() {
+		const response = await fetch(`${jvbSettings.api}auth/status`, {
+			method: 'GET',
+			credentials: 'same-origin',
+			headers: {
+				'Content-Type': 'application/json'
+			}
+		});
+
+		if (!response.ok) {
+			throw new Error('Auth check failed');
+		}
+
+		const authData = await response.json();
+
+		// Check if session changed (e.g., logout in another tab)
+		const cachedMeta = sessionStorage.getItem(this.cacheMetaKey);
+		if (cachedMeta) {
+			const meta = JSON.parse(cachedMeta);
+			if (meta.session_id && meta.session_id !== authData.session_id) {
+				this.clearCachedAuth();
+				this.notify('session-changed', {});
+			}
+		}
+
+		this.cacheAuth(authData);
+		this.setAuthData(authData);
+		this.initialized = true;
+		this.isAuthenticating = false;
+		this.notify('auth-loaded', { fromCache: false });
+	}
+
+	/**
+	 * Set authentication data
+	 */
+	setAuthData(authData) {
+		this.authenticated = authData.authenticated || false;
+		this.user = authData.user || false;
+		this.nonces = authData.nonces || {};
+	}
+
+	/**
+	 * Clear authentication data
+	 */
+	clearAuthData() {
+		this.authenticated = false;
+		this.user = null;
+		this.nonces = {};
+
+		sessionStorage.removeItem(this.storageKey);
+		sessionStorage.removeItem(this.cacheMetaKey );
+	}
+
+	/**
+	 * Get cached auth data (only if cookie matches)
+	 */
+	getCachedAuth() {
+		try {
+			const cachedAuth = sessionStorage.getItem(this.storageKey);
+			const cacheMeta = sessionStorage.getItem(this.cacheMetaKey);
+
+			if (!cachedAuth || !cacheMeta) {
+				return null;
+			}
+
+			const meta = JSON.parse(cacheMeta);
+			const authData = JSON.parse(cachedAuth);
+
+			// Time-based expiry (nonce freshness)
+			if (Date.now() - meta.timestamp > this.cacheExpiry) {
+				this.clearCachedAuth();
+				return null;
+			}
+
+			// Session changed (login/logout in another tab/window)
+			// We'll verify this on next fetch and clear if mismatched
+
+			return authData;
+
+		} catch (error) {
+			console.error('Error reading cached auth:', error);
+			return null;
+		}
+	}
+
+	/**
+	 * Cache auth data in sessionStorage
+	 */
+	cacheAuth(authData) {
+		try {
+			sessionStorage.setItem(this.storageKey, JSON.stringify(authData));
+			sessionStorage.setItem(this.cacheMetaKey, JSON.stringify({
+				session_id: authData.session_id || null,
+				timestamp: Date.now()
+			}));
+		} catch (error) {
+			console.error('Error caching auth:', error);
+		}
+	}
+
+	clearCachedAuth() {
+		sessionStorage.removeItem(this.storageKey);
+		sessionStorage.removeItem(this.cacheMetaKey);
+	}
+
+	/**
+	 * Refresh authentication (force new fetch)
+	 */
+	async refresh() {
+		this.isAuthenticating = true;
+		this.initialized = false;
+
+		try {
+			await this.fetchAuth();
+			this.notify('auth-refreshed', {});
+		} catch (error) {
+			console.error('Failed to refresh auth:', error);
+			this.clearAuthData();
+			this.initialized = true;
+			this.isAuthenticating = false;
+			this.notify('auth-error', { error });
+		}
+	}
+
+	/**
+	 * Get nonce for a specific action
+	 */
+	getNonce(action = 'wp_rest') {
+		return this.nonces[action] || '';
+	}
+
+	getUser() {
+		return this.user;
+	}
+
+	isAuthenticated() {
+		return this.authenticated;
+	}
+
+	/**
+	 * Handle successful login (call after login completes)
+	 */
+	async handleLogin(authData = null) {
+		// Clear old cache
+		sessionStorage.removeItem(this.storageKey);
+		sessionStorage.removeItem(this.cacheMetaKey);
+
+		// If auth data provided, use it directly
+		if (authData) {
+			this.cacheAuth(authData);
+			this.setAuthData(authData);
+			this.initialized = true;
+			this.isAuthenticating = false;
+			this.notify('auth-loaded', { fromCache: false, fromLogin: true });
+			return;
+		}
+
+		// Otherwise fetch fresh (for backward compatibility)
+		await this.refresh();
+	}
+
+	/**
+	 * Handle logout
+	 */
+	handleLogout() {
+		this.clearAuthData();
+		this.notify('logged-out', {});
+	}
+
+	/**
+	 * Subscribe to auth events
+	 */
+	subscribe(callback) {
+		this.subscribers.add(callback);
+
+		// If already initialized, immediately notify
+		if (this.initialized) {
+			callback('auth-loaded', {
+				fromCache: false,
+				immediate: true
+			});
+		}
+
+		return () => this.subscribers.delete(callback);
+	}
+
+	/**
+	 * Notify subscribers of events
+	 */
+	notify(event, data) {
+		this.subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
+			}
+		});
+	}
+
+	/**
+	 * Wait for auth to be ready
+	 */
+	ready() {
+		if (this.initialized) {
+			return Promise.resolve();
+		}
+
+		return new Promise(resolve => {
+			const unsubscribe = this.subscribe((event) => {
+				if (event === 'auth-loaded' || event === 'auth-error') {
+					unsubscribe();
+					resolve();
+				}
+			});
+		});
+	}
+}
+
+// Initialize global instance
+window.auth = new AuthManager();
diff --git a/assets/js/dash/BioManager.js b/assets/js/concise/BioManager.js
similarity index 94%
rename from assets/js/dash/BioManager.js
rename to assets/js/concise/BioManager.js
index c29d3c5..cd058a2 100644
--- a/assets/js/dash/BioManager.js
+++ b/assets/js/concise/BioManager.js
@@ -19,7 +19,7 @@
 		if (data === null) {
 			return;
 		}
-        data.user = jvbSettings.currentUser;
+        data.user = window.auth.getUser();
 
 		if (Object.hasOwn(data, 'term_name') && data['term_name'] === ''){
 			delete data['term_name'];
diff --git a/assets/js/dash/CRUD.js b/assets/js/concise/CRUD.js
similarity index 93%
rename from assets/js/dash/CRUD.js
rename to assets/js/concise/CRUD.js
index 0a2144b..f6eee56 100644
--- a/assets/js/dash/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -7,7 +7,7 @@
 		this.config = config;
 		this.content = config.content || false;
 		this.settings = window.jvbUserSettings;
-
+		this.a11y = window.jvbA11y;
 		if (!this.content) {
 			return;
 		}
@@ -25,7 +25,7 @@
 				keyPath: 'id',
 				endpoint: 'content',
 				headers: {
-					'action_nonce': jvbSettings.dash,
+					'action_nonce': window.auth.getNonce('dash'),
 				},
 				indexes: [
 					{name: 'id', keyPath: 'id'},
@@ -36,7 +36,7 @@
 				],
 				filters: {
 					content: this.content,
-					user: jvbSettings.currentUser,
+					user: window.auth.getUser(),
 					page: 1,
 					status: 'all',
 					orderby: 'modified', //or title
@@ -92,7 +92,6 @@
 
 		this.queue.subscribe((event, data) => {
 			if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'content') return;
-			console.log('Queue Subscription in CRUD.js: ', data);
 			if (event === 'operation-completed') {
 				this.handleQueueSuccess(event, data);
 			} else if (event === 'operation-failed-permanent') {
@@ -168,7 +167,7 @@
 			}
 		}
 
-		if (window.isEmptyObject(theChanges)) {
+		if (Object.keys(theChanges).length === 0) {
 			return;
 		}
 
@@ -181,7 +180,7 @@
 	}
 
 	savePosts(changes, title) {
-		if (window.isEmptyObject(changes)) {
+		if (Object.keys(changes).length === 0) {
 			return;
 		}
 
@@ -194,7 +193,7 @@
 		let operation = {
 			endpoint: 'content',
 			headers: {
-				'action_nonce': jvbSettings.dash,
+				'action_nonce': window.auth.getNonce('dash'),
 			},
 			data: {
 				posts: changes,
@@ -238,11 +237,12 @@
 		this.isTimeline = !!document.querySelector('[data-timeline]');
 	}
 	init() {
-		this.settings.addSetting(this.ui.uploader, 'open');
-		this.ui.uploader.addEventListener('toggle', (e) =>{
-			this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
-		});
-
+		if (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');
+			});
+		}
 
 		// Set up filter controls
 		this.filterHandler = this.handleFilterChange.bind(this);
@@ -558,7 +558,7 @@
 		this.viewController.clearSelection();
 
 
-		if (!window.isEmptyObject(changes)) {
+		if (Object.keys(changes).length !== 0) {
 			this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`);
 		}
 	}
@@ -579,7 +579,7 @@
 	}
 	updateBulkOptions(status = 'all') {
 		if (status === 'trash') {
-			if (this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
+			if (this.ui.bulkSelectActions?.querySelector('[value="edit"]')) {
 				window.removeChildren(this.ui.bulkSelectActions);
 				let options = window.getTemplate('trashOptions');
 				options.querySelectorAll('option').forEach((option, index) => {
@@ -590,7 +590,7 @@
 				});
 			}
 		} else {
-			if (!this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
+			if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
 				window.removeChildren(this.ui.bulkSelectActions);
 
 				let options = window.getTemplate('notTrashOptions');
@@ -599,7 +599,9 @@
 				});
 			}
 		}
-		this.ui.bulkSelectActions.value = '';
+		if (this.ui.bulkSelectActions) {
+			this.ui.bulkSelectActions.value = '';
+		}
 	}
 
 	populateBulkEdit() {
@@ -685,12 +687,15 @@
 }
 
 // Initialize when ready
-document.addEventListener('DOMContentLoaded', () => {
-	let container = document.querySelector('[data-content]');
-	if (container) {
-		window.crudManager = new CRUDManager({
-			content: container.dataset.content,
-		});
-	}
-
+document.addEventListener('DOMContentLoaded', async function()  {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			let container = document.querySelector('[data-content]');
+			if (container && !Object.hasOwn(container.dataset, 'ignore')) {
+				window.crudManager = new CRUDManager({
+					content: container.dataset.content,
+				});
+			}
+		}
+	});
 });
diff --git a/assets/js/dash/ContentManager.js b/assets/js/concise/ContentManager.js
similarity index 98%
rename from assets/js/dash/ContentManager.js
rename to assets/js/concise/ContentManager.js
index 18c71ed..c8bf5a1 100644
--- a/assets/js/dash/ContentManager.js
+++ b/assets/js/concise/ContentManager.js
@@ -142,7 +142,7 @@
         });
 
         const operation = {
-            user: jvbSettings.currentUser,
+            user: window.auth.getUser(),
             type: 'content_update',
             data: {
                 posts: posts
@@ -342,7 +342,7 @@
             params.set('type', this.config.content);
             params.set('page', this.queue[status].page);
             params.set('filters', JSON.stringify(this.state.filters));
-			params.set('user', jvbSettings.currentUser);
+			params.set('user', window.auth.getUser());
 
             if (reset) {
                 this.queue[status].page = 1;
@@ -360,12 +360,12 @@
                     method: 'GET',
                     headers: {
                         'Content-Type': 'application/json',
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.dash,
+                        'X-WP-Nonce': window.auth.getNonce(),
+                        'action_nonce': window.auth.getNonce('dash'),
                     },
                 },
                 {
-                    context: jvbSettings.currentUser+'-'+this.config.content,
+                    context: window.auth.getUser()+'-'+this.config.content,
                     forceRefresh: false,
                 }
             );
@@ -373,8 +373,8 @@
             //     method: 'GET',
             //     headers: {
             //         'Content-Type': 'application/json',
-            //         'X-WP-Nonce': jvbSettings.nonce,
-            //         'action_nonce': jvbSettings.dash,
+            //         'X-WP-Nonce': window.auth.getNonce(),
+            //         'action_nonce': window.auth.getNonce('dash'),
             //     }
             // });
             // const data = await response.json();
@@ -1057,7 +1057,7 @@
                     });
                     submit.taxonomies = taxonomies;
                     for(let [key, value] of Object.entries(submit)){
-                        if(value === '' || window.isEmptyObject(value)){
+                        if(value === '' || Object.keys(value).length === 0){
                             delete submit[key];
                         }
                     }
diff --git a/assets/js/dash/CopyHours.js b/assets/js/concise/CopyHours.js
similarity index 100%
rename from assets/js/dash/CopyHours.js
rename to assets/js/concise/CopyHours.js
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index ffe1891..3d46224 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -110,7 +110,7 @@
 			};
 
 			store.config.headers = {
-				'X-WP-Nonce': jvbSettings?.nonce,
+				'X-WP-Nonce': window.auth.getNonce(),
 				...store.config.headers
 			};
 
@@ -183,49 +183,6 @@
 	}
 
 	/**
-	 * Normalize data before saving - convert Sets/Maps automatically
-	 */
-	normalizeForStorage(obj) {
-		if (obj === null || obj === undefined) return obj;
-
-		// Convert Set to Array
-		if (obj instanceof Set) {
-			return Array.from(obj);
-		}
-
-		// Convert Map to Object
-		if (obj instanceof Map) {
-			return Object.fromEntries(obj);
-		}
-
-		// Preserve ArrayBuffer and TypedArrays (needed for blob storage)
-		if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
-			return obj;
-		}
-
-		// Preserve Date objects
-		if (obj instanceof Date) {
-			return obj;
-		}
-
-		// Handle Arrays
-		if (Array.isArray(obj)) {
-			return obj.map(item => this.normalizeForStorage(item));
-		}
-
-		// Handle Objects
-		if (typeof obj === 'object') {
-			const normalized = {};
-			for (const [key, value] of Object.entries(obj)) {
-				normalized[key] = this.normalizeForStorage(value);
-			}
-			return normalized;
-		}
-
-		return obj;
-	}
-
-	/**
 	 * Convert FormData to plain object for storage
 	 */
 	formDataToObject(formData) {
@@ -286,63 +243,6 @@
 	}
 
 	/**
-	 * Strip DOM references from object
-	 */
-	stripDOMReferences(obj, visited = new WeakSet()) {
-		if (obj === null || obj === undefined) return obj;
-
-		const type = typeof obj;
-		if (type === 'string' || type === 'number' || type === 'boolean') {
-			return obj;
-		}
-
-		// Prevent circular references
-		if (type === 'object' && visited.has(obj)) {
-			return '[Circular]';
-		}
-
-		// Remove DOM elements
-		if (obj instanceof HTMLElement ||
-			obj instanceof NodeList ||
-			obj instanceof HTMLCollection ||
-			obj.nodeType !== undefined) {
-			return null;
-		}
-
-		// ✅ PRESERVE ArrayBuffer and TypedArrays (needed for blob storage)
-		if (obj instanceof ArrayBuffer ||
-			ArrayBuffer.isView(obj)) {
-			return obj;
-		}
-
-		// Handle Date
-		if (obj instanceof Date) {
-			return obj;
-		}
-
-		// Handle Arrays
-		if (Array.isArray(obj)) {
-			visited.add(obj);
-			return obj.map(item => this.stripDOMReferences(item, visited)).filter(v => v !== null);
-		}
-
-		// Handle Objects
-		if (type === 'object') {
-			visited.add(obj);
-			const cleaned = {};
-			for (const [key, value] of Object.entries(obj)) {
-				const cleanedValue = this.stripDOMReferences(value, visited);
-				if (cleanedValue !== null) {
-					cleaned[key] = cleanedValue;
-				}
-			}
-			return cleaned;
-		}
-
-		return obj;
-	}
-
-	/**
 	 * Initialize database for a specific store
 	 */
 	async initDB(name) {
@@ -644,15 +544,37 @@
 				signal: controller.signal
 			});
 
-			if (response.status === 304 && cached) {
+			if (response.status === 304) {
+				// 304 means "Not Modified" - use cached data if available
+				if (cached) {
+					this.notify(name, 'data-loaded', {
+						cached: true,
+						notModified: true,
+						items: cached.items || []
+					});
+					return cached;
+				}
+
+				// No cached data but server says not modified - return empty result
+				// This can happen on first load when cache headers exist but data doesn't
 				this.notify(name, 'data-loaded', {
-					cached: true,
+					cached: false,
 					notModified: true,
-					items: cached.items || []
+					items: []
 				});
-				return cached;
+
+				// Initialize empty lastResponse
+				store.lastResponse = {
+					has_more: false,
+					total: 0,
+					pages: 1,
+					queue_stats: {}
+				};
+
+				return { items: [] };
 			}
 
+			// Now check for other non-OK responses
 			if (!response.ok) {
 				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 			}
@@ -662,7 +584,6 @@
 			if (store.config.useHttpCaching) {
 				this.storeResponseHeaders(name, cacheKey, response);
 			}
-
 			await this.processFetchedData(name, data, cacheKey);
 
 			this.notify(name, 'data-loaded', {
@@ -711,8 +632,30 @@
 		const store = this.stores.get(name);
 		const items = data.items || [];
 
-		for (const item of items) {
-			await this.save(name, item);
+		// Batch process all items in a single transaction
+		if (store.db && items.length > 0) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+
+			for (const item of items) {
+				const result = this.processForStorage(item, store.config.validateData);
+				if (result.valid) {
+					const key = this.getItemKey(result.data, store.config.keyPath);
+
+					// Store in memory
+					store.data.set(key, item);
+
+					// Queue for batch write
+					await objectStore.put(result.data);
+				}
+			}
+
+			// Wait for transaction to complete
+			await new Promise((resolve, reject) => {
+				tx.oncomplete = () => resolve();
+				tx.onerror = () => reject(tx.error);
+			});
+
 		}
 
 		const cacheEntry = {
@@ -727,9 +670,11 @@
 		await this.saveToCache(name, cacheKey, cacheEntry);
 
 		store.lastResponse = {
+			...data,
 			has_more: data.has_more || false,
 			total: data.total || items.length,
-			pages: data.pages || 1
+			pages: data.pages || 1,
+			queue_stats: data.queue_stats || {}
 		};
 	}
 
@@ -740,26 +685,11 @@
 	async save(name, item) {
 		const store = this.stores.get(name);
 
-		// Auto-normalize Sets/Maps
-		let processed = this.normalizeForStorage(item);
-
-		if (processed.data instanceof FormData) {
-			processed = {
-				...processed,
-				data: this.formDataToObject(processed.data)
-			};
+		const result = this.processForStorage(item, store.config.validateData);
+		if (!result.valid) {
+			throw new Error(`Non-serializable data: ${result.error}`);
 		}
-
-		processed = this.stripDOMReferences(processed);
-
-		// Validate data is serializable
-		if (store.config.validateData) {
-			const validation = this.validateSerializable(processed);
-			if (!validation.valid) {
-				console.error(`Cannot save non-serializable data to store "${name}":`, validation.error);
-				throw new Error(`Non-serializable data: ${validation.error}`);
-			}
-		}
+		const processed = result.data;
 
 		const key = this.getItemKey(processed, store.config.keyPath);
 
@@ -777,102 +707,74 @@
 		return key;
 	}
 
-	/**
-	 * Validate that data is IndexedDB-serializable
-	 * Rejects: DOM elements, FormData, Blobs, Functions, etc.
-	 */
-	validateSerializable(obj, path = 'root') {
-		// Primitives are fine
-		if (obj === null || obj === undefined) {
-			return { valid: true };
-		}
+	processForStorage(obj, validate = true, path = 'root') {
+		if (obj === null || obj === undefined) return { valid: true, data: obj };
 
 		const type = typeof obj;
-		if (type === 'string' || type === 'number' || type === 'boolean') {
-			return { valid: true };
+
+		// Handle primitives
+		if (['string', 'number', 'boolean'].includes(type)) {
+			return { valid: true, data: obj };
 		}
 
-		// Functions cannot be serialized
+		// Reject functions
 		if (type === 'function') {
-			return {
-				valid: false,
-				error: `Function at ${path}`
-			};
+			return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
 		}
 
-		// Date is serializable
-		if (obj instanceof Date) {
-			return { valid: true };
+		// DOM elements
+		if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
+			return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
 		}
 
-		if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
-			return { valid: true };
-		}
-
-		// Reject DOM elements
-		if (obj instanceof HTMLElement ||
-			obj instanceof NodeList ||
-			obj instanceof HTMLCollection ||
-			(obj.nodeType !== undefined)) {
-			return {
-				valid: false,
-				error: `DOM element at ${path}`
-			};
-		}
-
-		// Reject FormData
+		// FormData - convert and continue
 		if (obj instanceof FormData) {
-			return {
-				valid: false,
-				error: `FormData at ${path}. Convert to object first.`
-			};
+			return validate
+				? { valid: false, error: `FormData at ${path}` }
+				: { valid: true, data: this.formDataToObject(obj) };
 		}
 
-		// Reject Blobs/Files
-		if (obj instanceof Blob || obj instanceof File) {
-			return {
-				valid: false,
-				error: `Blob/File at ${path}. Handle file uploads separately.`
-			};
+		// Preserve safe types
+		if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+			return { valid: true, data: obj };
+		}
+
+		// Convert Sets to Arrays
+		if (obj instanceof Set) {
+			const arr = Array.from(obj);
+			return this.processForStorage(arr, validate, path);
+		}
+
+		// Convert Maps to Objects
+		if (obj instanceof Map) {
+			obj = Object.fromEntries(obj);
 		}
 
 		// Arrays
 		if (Array.isArray(obj)) {
+			const processed = [];
 			for (let i = 0; i < obj.length; i++) {
-				const result = this.validateSerializable(obj[i], `${path}[${i}]`);
+				const result = this.processForStorage(obj[i], validate, `${path}[${i}]`);
 				if (!result.valid) return result;
+				if (result.data !== null) processed.push(result.data);
 			}
-			return { valid: true };
+			return { valid: true, data: processed };
 		}
 
-		// Plain objects
+		// Objects
 		if (type === 'object') {
-			// Check for Sets/Maps (IndexedDB doesn't support them)
-			if (obj instanceof Set) {
-				return {
-					valid: false,
-					error: `Set at ${path}. Convert to Array first: Array.from(set)`
-				};
-			}
-			if (obj instanceof Map) {
-				return {
-					valid: false,
-					error: `Map at ${path}. Convert to Object first: Object.fromEntries(map)`
-				};
-			}
-
-			// Check all properties
+			const processed = {};
 			for (const [key, value] of Object.entries(obj)) {
-				const result = this.validateSerializable(value, `${path}.${key}`);
+				const result = this.processForStorage(value, validate, `${path}.${key}`);
 				if (!result.valid) return result;
+				if (result.data !== null) processed[key] = result.data;
 			}
-			return { valid: true };
+			return { valid: true, data: processed };
 		}
 
-		return {
-			valid: false,
-			error: `Unknown type at ${path}: ${type}`
-		};
+		return validate
+			? { valid: false, error: `Unknown type at ${path}` }
+			: { valid: true, data: null };
 	}
 
 	async delete(name, id) {
@@ -1094,7 +996,6 @@
 				acc[key] = filters[key];
 				return acc;
 			}, {});
-
 		return JSON.stringify(normalized);
 	}
 
@@ -1144,6 +1045,10 @@
 }
 
 // Initialize singleton on DOMContentLoaded
-document.addEventListener('DOMContentLoaded', function() {
-	window.jvbStore = new DataStore();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbStore = new DataStore();
+		}
+	});
 });
diff --git a/assets/js/concise/DataStoreOld.js b/assets/js/concise/DataStoreOld.js
deleted file mode 100644
index dfb1f8b..0000000
--- a/assets/js/concise/DataStoreOld.js
+++ /dev/null
@@ -1,1158 +0,0 @@
-/**
- * ExtendedDataStore - A flexible IndexedDB wrapper with HTTP caching
- *
- * Configuration-based approach for different storage needs:
- * - Configurable endpoint, keyPath, and indexes
- * - Built-in ETag and If-Modified-Since support
- * - Automatic DOM reference stripping
- * - TTL-based cache invalidation
- *
- * All notifications:
- *
-  		this.store.subscribe((event, data) => {
-  			switch (event) {
-  				case 'data-loaded':
-  					break;
-  				case 'item-saved':
-  					break;
-  				case 'items-saved':
-  					break;
-  				case 'item-deleted':
-  					break;
-  				case 'data-cleared':
-  					break;
-  				case 'filters-changed':
-  					break;
-  				case 'filters-cleared':
-  					break;
-  				case 'cache-cleared':
-  					break;
-  			}
-  		});
- */
-class DataStore {
-	constructor(config = {}) {
-		// Core configuration with sensible defaults
-		this.config = {
-			// Storage configuration
-			name: 'default',
-			version: 1,
-			storeName: 'items',
-			keyPath: 'id',
-			indexes: [], // Array of {name, keyPath, unique}
-
-			// API configuration
-			endpoint: null,
-			saveToServer: false,
-			apiBase: jvbSettings.api,
-			headers: {},
-			filters: {},
-			required:  null, //any required filters before fetching
-			icon: null,
-			getBlobs: null,
-
-			// Cache configuration
-			TTL: 3600000, // 1 hour default
-			useHttpCaching: true, // ETag and If-Modified-Since
-			cacheKeyStrategy: 'filters', // How to generate cache keys
-
-			// UI configuration
-			showLoading: true,
-
-			// Features
-			stripDOMReferences: true,
-			storeBlobs: false,
-			delayFetch: false,
-
-			...config
-		};
-
-		// Initialize base properties
-		this.db = null;
-		this.data = new Map();
-		this.cache = new Map();
-		this.isFetching = false;
-		this.pendingFetch = null;
-		this.httpHeaders = new Map();
-		this.subscribers = new Set();
-		this.currentRequest = null;
-		this.filters = this.config.filters??{};
-
-		// Set up headers
-		this.headers = {
-			'X-WP-Nonce': jvbSettings?.nonce,
-			...this.config.headers
-		};
-
-		this.body = document.body;
-		this.loading = document.querySelector('dialog.loading');
-
-		this._initialized = false;
-		// Cleanup on page unload
-		window.addEventListener('beforeunload', () => this.destroy());
-	}
-
-	async init() {
-		if (this._initialized) return;
-		await this.initDB();
-		this._initialized = true;
-	}
-
-	/**
-	 * Initialize IndexedDB with configurable schema
-	 */
-	async initDB() {
-		if (!('indexedDB' in window)) {
-			console.warn('IndexedDB not supported');
-			return;
-		}
-
-		const dbName = `jvb_${this.config.name}_db`;
-		const request = indexedDB.open(dbName, this.config.version);
-
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-
-			// Create main store with configurable keyPath
-
-			if (!db.objectStoreNames.contains(this.config.storeName)) {
-				const store = db.createObjectStore(this.config.storeName, {
-					keyPath: this.config.keyPath
-				});
-
-				// Add configured indexes
-				this.config.indexes.forEach(index => {
-					store.createIndex(
-						index.name,
-						index.keyPath || index.name,
-						{ unique: index.unique || false }
-					);
-				});
-			}
-
-			// Cache store for HTTP responses
-			if (this.config.endpoint && !db.objectStoreNames.contains('cache')) {
-				const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
-				cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
-				cacheStore.createIndex('endpoint', 'endpoint', { unique: false });
-				cacheStore.createIndex('filters', 'filters', { unique: false });
-			}
-
-			// HTTP headers store for ETag/If-Modified-Since
-			if (this.config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
-				db.createObjectStore('headers', { keyPath: 'key' });
-			}
-
-			if (this.config.storeBlobs && !db.objectStoreNames.contains('blobs')) {
-				db.createObjectStore('blobs', { keyPath: 'uploadId' });
-			}
-
-			// Call optional schema extension
-			if (this.config.onUpgrade) {
-				this.config.onUpgrade(db, e.oldVersion, e.newVersion);
-			}
-
-		};
-
-		request.onsuccess = async (e) => {
-			this.db = e.target.result;
-
-			// Load in background without blocking
-			this.loadInBackground();
-
-			this.notify('db-init');
-
-			// Only fetch if explicitly needed
-			if (this.config.endpoint && !this.config.delayFetch) {
-				requestIdleCallback(() => this.fetch(), { timeout: 2000 });
-			}
-		};
-
-		request.onerror = (e) => {
-			console.error(`IndexedDB error for ${dbName}:`, e);
-			if (this.config.onError) {
-				this.config.onError(e);
-			}
-		};
-	}
-
-	loadInBackground() {
-		// Non-blocking background load
-		Promise.all([
-			this.loadFromDB(),
-			this.loadCache(),
-			this.loadHeaders()
-		]).then(() => {
-			this.notify('data-ready');
-		}).catch(console.error);
-	}
-
-	/**
-	 * Load all data from IndexedDB
-	 */
-	async loadFromDB() {
-		if (!this.db) return;
-
-		return new Promise(async (resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.getAll();
-
-			request.onsuccess = async (e) => {
-				const items = e.target.result;
-				console.log('fetched from cache');
-				for (const item of items) {
-					if (item.data?._isFormData && this.config.getBlobs) {
-						item.data = await this.objectToFormData(item.data);
-					}
-					const key = this.getItemKey(item);
-					this.data.set(key, item);
-				}
-
-				this.notify('data-loaded', { count: items.length });
-				resolve(items);
-			};
-
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-
-
-	/**
-	 * Load main data from IndexedDB
-	 */
-	async loadData() {
-		if (!this.db) return;
-
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					const key = this.getItemKey(item);
-					this.data.set(key, item);
-				});
-				resolve();
-			};
-
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-	/**
-	 * Strip DOM references from an object (recursive)
-	 */
-	stripDOMReferences(obj) {
-		if (!obj || typeof obj !== 'object') return obj;
-
-		// Handle arrays
-		if (Array.isArray(obj)) {
-			return obj.map(item => this.stripDOMReferences(item));
-		}
-
-		// Handle objects
-		const cleaned = {};
-		for (const [key, value] of Object.entries(obj)) {
-			// Skip DOM-related properties
-			if (this.isDOMReference(key, value)) {
-				continue;
-			}
-
-			// Handle Set/Map collections
-			if (value instanceof Set) {
-				cleaned[key] = Array.from(value);
-			} else if (value instanceof Map) {
-				cleaned[key] = Object.fromEntries(value);
-			} else if (typeof value === 'object' && value !== null) {
-				cleaned[key] = this.stripDOMReferences(value);
-			} else {
-				cleaned[key] = value;
-			}
-		}
-
-		return cleaned;
-	}
-
-	/**
-	 * Check if a property is a DOM reference
-	 */
-	isDOMReference(key, value) {
-		// Check value types
-		if (value instanceof HTMLElement ||
-			value instanceof NodeList ||
-			value instanceof HTMLCollection ||
-			(value && value.nodeType !== undefined)) {
-			return true;
-		}
-
-		// Check key names - use exact match or word boundaries
-		const domKeys = ['element', 'el', 'dom', 'node', 'ui', 'container', 'wrapper'];
-		const lowerKey = key.toLowerCase();
-
-		// Only match if it's the exact key OR starts/ends with the pattern
-		if (domKeys.includes(lowerKey) ||
-			domKeys.some(k => lowerKey === k || lowerKey.startsWith(k + '_') || lowerKey.endsWith('_' + k))) {
-			return true;
-		}
-
-		return false;
-	}
-
-	/**
-	 * Get the key for an item based on configured keyPath
-	 */
-	getItemKey(item) {
-		if (typeof this.config.keyPath === 'function') {
-			return this.config.keyPath(item);
-		}
-
-		// Support nested keypaths like 'meta.id'
-		const keys = this.config.keyPath.split('.');
-		let value = item;
-
-		for (const key of keys) {
-			value = value?.[key];
-		}
-
-		return value;
-	}
-
-	/**
-	 * Save a single item
-	 */
-	/**
-	 * Save a single item
-	 */
-	async save(item) {
-		const key = this.getItemKey(item);
-
-		// Keep ORIGINAL item in memory (with FormData intact)
-		this.data.set(key, item);  // ← Store original
-
-		// Create cleaned version ONLY for IndexedDB
-		let cleaned = { ...item };
-		if (cleaned.data instanceof FormData) {
-			cleaned.data = this.formDataToObject(cleaned.data);
-		}
-
-		if (this.config.stripDOMReferences) {
-			cleaned = this.stripDOMReferences(cleaned);
-		}
-
-		// Persist cleaned version to IndexedDB
-		await this.saveToDB(cleaned);
-
-		if(this.config.saveToServer && this.config.endpoint){
-			this.saveToServer(item);
-		}
-
-		this.notify('item-saved', { item: cleaned, key });
-
-		return cleaned;
-	}
-
-	/**
-	 * Convert FormData to plain object for storage
-	 */
-	formDataToObject(formData) {
-		const obj = {
-			_isFormData: true, // Flag to reconstruct later
-			entries: {}
-		};
-
-		for (const [key, value] of formData.entries()) {
-			// Skip File/Blob objects - they're stored separately
-			if (value instanceof File || value instanceof Blob) {
-				continue;
-			}
-
-			// Handle multiple values for same key
-			if (obj.entries[key]) {
-				if (!Array.isArray(obj.entries[key])) {
-					obj.entries[key] = [obj.entries[key]];
-				}
-				obj.entries[key].push(value);
-			} else {
-				obj.entries[key] = value;
-			}
-		}
-
-		return obj;
-	}
-
-	/**
-	 * Convert stored object back to FormData
-	 */
-	async objectToFormData(obj) {
-		if (!obj._isFormData) return obj;
-
-		const formData = new FormData();
-
-		for (const [key, value] of Object.entries(obj.entries)) {
-			if (Array.isArray(value)) {
-				value.forEach(v => formData.append(key, v));
-			} else {
-				formData.append(key, value);
-			}
-		}
-		// Restore files from external blob store (UploadManager)
-		if (this.config.getBlobs && obj.entries.upload_ids) {
-			const uploadIds = JSON.parse(obj.entries.upload_ids);
-			const blobs = await this.config.getBlobs(uploadIds);  // ← Await here
-
-			for (const blobData of blobs) {
-				if (blobData) {
-					const file = new File(
-						[blobData.data],
-						blobData.name,
-						{ type: blobData.type, lastModified: blobData.lastModified }
-					);
-					formData.append('files[]', file);
-				}
-			}
-		}
-
-		return formData;
-	}
-
-	/**
-	 * Save item to IndexedDB
-	 */
-	async saveToDB(item) {
-		if (!this.db) return;
-
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readwrite');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.put(item);
-
-			request.onsuccess = () => resolve();
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-	/**
-	 * Batch save multiple items
-	 */
-	async saveMany(items) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction([this.config.storeName], 'readwrite');
-		const store = tx.objectStore(this.config.storeName);
-
-		const promises = items.map(item => {
-			const cleaned = this.config.stripDOMReferences
-				? this.stripDOMReferences(item)
-				: item;
-
-			const key = this.getItemKey(cleaned);
-			this.data.set(key, cleaned);
-
-			return store.put(cleaned);
-		});
-
-		await Promise.all(promises);
-		this.notify('items-saved', { count: items.length });
-	}
-
-	/**
-	 * Get a single item
-	 */
-	get(key) {
-		return this.data.get(key);  // ← Returns original with FormData
-	}
-
-	/**
-	 * Get all items
-	 */
-	getAll() {
-		return Array.from(this.data.values());
-	}
-
-	/**
-	 * Delete an item
-	 */
-	async delete(key, storeName = null) {
-		this.data.delete(key);
-
-		if (!storeName) {
-			storeName = this.config.storeName;
-		}
-		if (this.db) {
-			const tx = this.db.transaction([storeName], 'readwrite');
-			const store = tx.objectStore(storeName);
-			await store.delete(key);
-		}
-
-		this.notify('item-deleted', { key });
-	}
-
-	async saveBlob(key, blob) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['blobs'], 'readwrite');
-		const store = tx.objectStore('blobs');
-		await store.put({
-			uploadId: key,  // Match keyPath
-			data: blob,
-			type: blob.type,
-			name: blob.name,
-			lastModified: blob.lastModified || Date.now()
-		});
-	}
-
-	async getBlob(key) {
-		if (!this.db) return null;
-
-		return new Promise(resolve => {
-			const tx = this.db.transaction(['blobs'], 'readonly');
-			const request = tx.objectStore('blobs').get(key);
-			request.onsuccess = () => resolve(request.result);
-			request.onerror = () => resolve(null);
-		});
-	}
-
-	/**
-	 * Clear all data
-	 */
-	async clear() {
-		this.data.clear();
-		this.cache.clear();
-		this.httpHeaders.clear();
-
-		if (this.domCache) {
-			this.domCache.clear();
-		}
-
-		if (this.db) {
-			const stores = [this.config.storeName];
-			if (this.config.endpoint) stores.push('cache');
-			if (this.config.useHttpCaching) stores.push('headers');
-
-			const tx = this.db.transaction(stores, 'readwrite');
-			stores.forEach(storeName => {
-				if (this.db.objectStoreNames.contains(storeName)) {
-					tx.objectStore(storeName).clear();
-				}
-			});
-		}
-
-		this.notify('data-cleared');
-	}
-
-	/**
-	 * Fetch data from server with HTTP caching
-	 */
-	async fetch(options = {}) {
-		if (!this.config.endpoint) {
-			throw new Error('No endpoint configured for fetch');
-		}
-		await this.init(); // Lazy init
-		const {
-			filters = this.filters,
-			headers = {},
-		} = options;
-
-		if (this.config.required && this.filters[this.config.required] === ''){
-			console.log(this.config.storeName+ ': Not fetch as we don\'t have the required items');
-			return;
-		}
-
-		// PREVENT CONCURRENT FETCHES FOR SAME DATA
-		const cacheKey = this.generateCacheKey(filters);
-
-		// If already fetching this exact query, return a promise that resolves when done
-		if (this.isFetching && this.currentCacheKey === cacheKey) {
-			return new Promise((resolve) => {
-				// Store multiple waiting promises if needed
-				if (!this.pendingFetches) {
-					this.pendingFetches = [];
-				}
-				this.pendingFetches.push(resolve);
-			});
-		}
-
-
-		this.isFetching = true;
-		this.currentCacheKey = cacheKey;
-		let fetchResult = null; // Capture result for pending fetches
-
-		if (this.config.showLoading) {
-			this.setLoading(true);
-		}
-
-		//Check Cached data
-		const cachedData = this.cache.get(cacheKey);
-		if (cachedData && this.isCacheValid(cachedData)) {
-			this.isFetching = false;
-
-			if (this.config.showLoading) {
-				this.setLoading(false);
-			}
-			this.notify('data-loaded', cachedData.data);
-			return cachedData.data;
-		}
-
-		// Build request headers with HTTP caching
-		const requestHeaders = {
-			...this.headers,
-			...headers
-		};
-
-		if (this.config.useHttpCaching) {
-			const httpCache = this.httpHeaders.get(cacheKey);
-			if (httpCache) {
-				if (httpCache.etag) {
-					requestHeaders['If-None-Match'] = httpCache.etag;
-				}
-				if (httpCache.lastModified) {
-					requestHeaders['If-Modified-Since'] = httpCache.lastModified;
-				}
-			}
-		}
-
-		// Build URL with filters
-		const cleanedFilters = this.cleanFilters(filters);
-		const params = new URLSearchParams(cleanedFilters);
-		const url = `${this.config.apiBase}${this.config.endpoint}${params.toString() ? '?' + params : ''}`;
-
-		try {
-			const response = await fetch(url, {
-				method: 'GET',
-				headers: requestHeaders
-			});
-
-			// Handle 304 Not Modified
-			if (response.status === 304 && cachedData) {
-				console.log('304 response');
-				// Update timestamp but keep existing data
-				cachedData.timestamp = Date.now();
-				cachedData.fromCache = true;
-				cachedData.isError = false;
-				this.saveCache(cacheKey, cachedData);
-
-				this.lastResponse = cachedData;
-				this.notify('data-loaded', cachedData);
-				fetchResult = cachedData.data;
-				return cachedData.data;
-			}
-
-			if (!response.ok) {
-				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
-			}
-
-			const data = await response.json();
-
-			//Store full response for accesss to metadata, like stats
-			this.lastResponse = data;
-
-			// Store HTTP caching headers
-			if (this.config.useHttpCaching) {
-				this.storeResponseHeaders(cacheKey, response);
-			}
-
-			// Cache the response
-			const cacheEntry = {
-				key: cacheKey,
-				items: data.items.map(item => item.id),
-				total: data.total,
-				maxPages: data['total_pages'],
-				timestamp: Date.now(),
-				endpoint: this.config.endpoint,
-				filters: filters
-			};
-
-			this.cache.set(cacheKey, cacheEntry);
-			this.saveCache(cacheKey, cacheEntry);
-
-			let items = (Array.isArray(data)) ? data : data.items;
-			await this.saveMany(items);
-
-			this.notify('data-loaded', {
-				data: {
-					items: items,
-					...data
-				},
-				count: items.length,
-				filters: filters,
-				fromCache: false,
-				isError: false
-			});
-
-			fetchResult = data;
-			return data;
-
-		} catch (error) {
-			console.error('Fetch error:', error);
-
-			// Return cached data if available, even if expired
-			if (cachedData) {
-				console.warn('Using stale cache due to fetch error');
-				cachedData.isError = true;
-				this.notify('data-loaded', cachedData);
-				fetchResult = cachedData.data;
-				return cachedData.data;
-			}
-
-			throw error;
-		} finally {
-			if (this.config.showLoading) {
-				this.setLoading(false);
-			}
-
-			this.isFetching = false;
-			this.currentCacheKey = null;
-
-			// Resolve any pending fetches that were waiting
-			if (this.pendingFetches && this.pendingFetches.length > 0) {
-				this.pendingFetches.forEach(resolve => resolve(fetchResult));
-				this.pendingFetches = [];
-			}
-		}
-	}
-
-	/**
-	 * Fetch data from server with HTTP caching
-	 */
-	async saveToServer(item) {
-		if (!this.config.saveToServer || !jvbSettings.currentUser) {
-			return;
-		}
-		if (!this.config.endpoint && this.config.saveToServer) {
-			throw new Error('No endpoint configured for saving to server');
-		}
-
-		let requestBody;
-		let headers = this.config.headers;
-		headers['X-WP-Nonce'] = jvbSettings.nonce;
-		if (item instanceof FormData) {
-			item.append('user', jvbSettings.currentUser);
-			requestBody = item;
-
-			// console.log('Sending formData: ');
-			// for (const pair of requestBody.entries()) {
-			// 	console.log(pair[0], pair[1]);
-			// }
-		} else {
-			requestBody = JSON.stringify({
-				...item,
-				user: jvbSettings.currentUser
-			});
-			// console.log('Sending data: ', {
-			// 	...operation.data,
-			// 	id: operation.id,
-			// 	user: this.user
-			// });
-
-			headers['Content-Type'] = 'application/json';
-		}
-
-		const response = await fetch(
-			`${this.config.apiBase}${this.config.endpoint}`,
-			{
-				method: 'POST',
-				headers: headers,
-				body: requestBody
-			}
-		);
-
-		const result = await response.json();
-		this.notify(
-			'saved-to-server',
-			{
-				success: result.ok && result.success
-			}
-		);
-	}
-
-	cleanFilters(filters) {
-		const cleaned = {};
-		Object.entries(filters).forEach(([key, value]) => {
-			if (value !== null && value !== undefined && value !== '') {
-				// Handle special cases based on existing patterns
-				if (key === 'taxonomies' && typeof value === 'object') {
-					Object.entries(value).forEach(([taxName, terms]) => {
-						if (Array.isArray(terms) && terms.length > 0) {
-							cleaned[`tax_${taxName}`] = terms.join(',');
-						} else if (terms) {
-							cleaned[`tax_${taxName}`] = terms;
-						}
-					});
-				} else if (key === 'date' && typeof value === 'object') {
-					if (value.after) cleaned.after = value.after;
-					if (value.before) cleaned.before = value.before;
-				} else {
-					cleaned[key] = value;
-				}
-			}
-		});
-		return cleaned;
-	}
-
-	/**
-	 * Generate cache key from filters
-	 */
-	generateCacheKey(filters) {
-		if (this.config.cacheKeyStrategy === 'custom' && this.config.generateCacheKey) {
-			return this.config.generateCacheKey(filters);
-		}
-
-		// Default strategy: sort keys and create string
-		const sorted = Object.keys(filters)
-			.sort()
-			.reduce((acc, key) => {
-				acc[key] = filters[key];
-				return acc;
-			}, {});
-
-		return JSON.stringify(sorted);
-	}
-
-	setFilter(key, value) {
-		if (!this.filters) {
-			this.filters = {};
-		}
-		const oldValue = this.filters[key];
-		if (oldValue === value) {
-			return;
-		}else if (value === '' || value === null || value === undefined) {
-			delete this.filters[key];
-		} else {
-			this.filters[key] = value;
-		}
-
-		this.notify('filters-changed', {
-			filters: this.filters,
-			changed: { key, oldValue, newValue: value }
-		});
-
-		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint !== null) {
-			this.fetch();
-		}
-	}
-
-
-	/**
-	 * Remove a filter
-	 */
-	removeFilter(key) {
-		const oldValue = this.filters[key];
-
-		if (oldValue !== undefined) {
-			delete this.filters[key];
-			this.notify('filters-changed', {
-				filters: this.filters,
-				removed: { key, oldValue }
-			});
-
-			// Auto-fetch if endpoint is configured
-			if (this.config.endpoint) {
-				this.fetch();
-			}
-		}
-	}
-
-	/**
-	 * Clear all filters
-	 */
-	clearFilters() {
-		const oldFilters = { ...this.filters };
-		//Restore baseline filters
-		this.filters = this.config.filters;
-
-		this.notify('filters-cleared', {
-			oldFilters,
-			filters: this.filters
-		});
-
-		// Auto-fetch if endpoint is configured
-		if (this.config.endpoint) {
-			this.fetch();
-		}
-	}
-
-	/**
-	 * Set multiple filters at once
-	 */
-	async setFilters(filters) {
-		const hasChanges = Object.keys(filters).some(
-			key => this.filters[key] !== filters[key]
-		);
-
-		if (!hasChanges) {
-			return;
-		}
-
-		this.filters = { ...this.filters, ...filters };
-
-		this.notify('filters-changed', {
-			filters: this.filters,
-			changed: filters,
-		});
-
-		// Only fetch if endpoint configured
-		if (this.config.endpoint) {
-			this.fetch();
-		}
-	}
-
-	getFiltered() {
-		const cacheKey = this.generateCacheKey(this.filters);
-		const cacheEntry = this.cache.get(cacheKey);
-
-		if (cacheEntry && cacheEntry.items) {
-			return cacheEntry.items.reduce((acc, id) => {
-				const item = this.data.get(id);
-				if (item) acc.push(item);
-				return acc;
-			}, []);
-		}
-
-		return Array.from(this.data.values());
-	}
-
-	/**
-	 * Check if cache entry is still valid
-	 */
-	isCacheValid(cacheEntry) {
-		if (!cacheEntry || !cacheEntry.timestamp) return false;
-
-		const age = Date.now() - cacheEntry.timestamp;
-		return age < this.config.TTL;
-	}
-
-	/**
-	 * Store HTTP response headers for caching
-	 */
-	storeResponseHeaders(key, response) {
-		const headers = {
-			key,
-			etag: response.headers.get('ETag'),
-			lastModified: response.headers.get('Last-Modified'),
-			timestamp: Date.now()
-		};
-
-		this.httpHeaders.set(key, headers);
-
-		if (this.db && this.db.objectStoreNames.contains('headers')) {
-			const tx = this.db.transaction(['headers'], 'readwrite');
-			const store = tx.objectStore('headers');
-			store.put(headers);
-		}
-	}
-
-	/**
-	 * Clear HTTP cache headers for a specific cache key or all
-	 */
-	clearHttpHeaders(cacheKey = null) {
-		if (cacheKey) {
-			this.httpHeaders.delete(cacheKey);
-
-			if (this.db && this.db.objectStoreNames.contains('headers')) {
-				const tx = this.db.transaction(['headers'], 'readwrite');
-				const store = tx.objectStore('headers');
-				store.delete(cacheKey);
-			}
-		} else {
-			// Clear all
-			this.httpHeaders.clear();
-
-			if (this.db && this.db.objectStoreNames.contains('headers')) {
-				const tx = this.db.transaction(['headers'], 'readwrite');
-				const store = tx.objectStore('headers');
-				store.clear();
-			}
-		}
-	}
-
-	/**
-	 * Save cache entry to IndexedDB
-	 */
-	async saveCache(key, data) {
-		if (!this.db || !this.db.objectStoreNames.contains('cache')) return;
-
-		const tx = this.db.transaction(['cache'], 'readwrite');
-		const store = tx.objectStore('cache');
-		await store.put(data);
-	}
-
-	getCurrentRequest() {
-		return this.lastResponse;
-	}
-
-	/**
-	 * Load cache from IndexedDB
-	 */
-	async loadCache() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['cache'], 'readonly');
-			const store = tx.objectStore('cache');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					if (this.isCacheValid(item)) {
-						this.cache.set(item.key, item);
-					}
-				});
-				resolve();
-			};
-		});
-	}
-
-	/**
-	 * Load HTTP headers from IndexedDB
-	 */
-	async loadHeaders() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['headers'], 'readonly');
-			const store = tx.objectStore('headers');
-			const request = store.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(header => {
-					this.httpHeaders.set(header.key, header);
-				});
-				resolve();
-			};
-		});
-	}
-
-
-	/**
-	 * Subscribe to store events
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	/**
-	 * Notify subscribers of events
-	 */
-	notify(event, data = {}) {
-		this.subscribers.forEach(callback => {
-			try {
-				callback(event, data);
-			} catch (error) {
-				console.error('Subscriber error:', error);
-			}
-		});
-	}
-
-	/**
-	 * Check if store has items matching a specific filter
-	 * @param {string} filterName - The filter to check
-	 * @param {*} filterValue - The value to match
-	 * @returns {boolean}
-	 */
-	hasItemsForFilter(filterName, filterValue) {
-		if (!this.data || this.data.size === 0) return false;
-
-		return Array.from(this.data.values()).some(item => {
-			return item[filterName] === filterValue;
-		});
-	}
-
-	/**
-	 * Query items using an index
-	 */
-	async query(indexName, value) {
-		if (!this.db) return [];
-
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-
-			if (!store.indexNames.contains(indexName)) {
-				reject(new Error(`Index ${indexName} does not exist`));
-				return;
-			}
-
-			const index = store.index(indexName);
-			const request = value !== undefined
-				? index.getAll(value)
-				: index.getAll();
-
-			request.onsuccess = (e) => {
-				const results = e.target.result.map(item => {
-					return this.config.stripDOMReferences
-						? this.stripDOMReferences(item)
-						: item;
-				});
-				resolve(results);
-			};
-
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-	/**
-	 * Count items in store
-	 */
-	async count() {
-		if (!this.db) return this.data.size;
-
-		return new Promise((resolve, reject) => {
-			const tx = this.db.transaction([this.config.storeName], 'readonly');
-			const store = tx.objectStore(this.config.storeName);
-			const request = store.count();
-
-			request.onsuccess = (e) => resolve(e.target.result);
-			request.onerror = (e) => reject(e);
-		});
-	}
-
-
-	setLoading(on) {
-		this.body.classList.toggle('loading', on);
-		if (on) {
-			this.loading.showModal();
-		} else {
-			this.loading.close();
-		}
-
-	}
-
-	/**
-	 * Cleanup and destroy
-	 */
-	destroy() {
-		if (this.currentRequest) {
-			this.currentRequest.abort();
-		}
-
-		this.subscribers.clear();
-		this.data.clear();
-		this.cache.clear();
-		this.httpHeaders.clear();
-
-		if (this.db) {
-			this.db.close();
-			this.db = null;
-		}
-	}
-
-	clearCache() {
-		this.cache.clear();
-
-		if (this.db) {
-			const tx = this.db.transaction(['cache'], 'readwrite');
-			const store = tx.objectStore('cache');
-			store.clear();
-		}
-
-		this.notify('cache-cleared');
-	}
-}
-
-// Export for use
-window.jvbStore = DataStore;
diff --git a/assets/js/dash/ErrorHandler.js b/assets/js/concise/ErrorHandler.js
similarity index 81%
rename from assets/js/dash/ErrorHandler.js
rename to assets/js/concise/ErrorHandler.js
index 7de1073..029b37b 100644
--- a/assets/js/dash/ErrorHandler.js
+++ b/assets/js/concise/ErrorHandler.js
@@ -144,37 +144,66 @@
         return defaultMessages[type] || defaultMessages.unknown;
     }
 
-    /**
-     * Log error to server
-     */
-    async logErrorToServer(type, message, context) {
-        try {
-            if (!this.options.apiUrl) return;
+	/**
+	 * Log error to server with enhanced context
+	 */
+	async logErrorToServer(type, message, context) {
+		try {
+			if (!this.options.apiUrl) return;
 
-            const data = new FormData();
-            data.append('error_type', type);
-            data.append('message', message);
-            data.append('context', JSON.stringify({
-                ...context,
-                url: window.location.href,
-                userAgent: navigator.userAgent,
-                timestamp: new Date().toISOString()
-            }));
+			// Enhanced context with component tracking
+			const enhancedContext = {
+				...context,
+				url: window.location.href,
+				pathname: window.location.pathname,
+				userAgent: navigator.userAgent,
+				timestamp: new Date().toISOString(),
+				viewport: `${window.innerWidth}x${window.innerHeight}`,
+				component: context.component || this.extractComponentFromStack(context.stack),
+				method: context.method || this.extractMethodFromStack(context.stack),
+				stack: context.stack || (context.error?.stack),
+				isLoggedIn: window.auth.isAuthenticated(),
+				source: 'frontend'
+			};
 
-            // Use fetch with no-cors to ensure this always succeeds
-            // even if there are CORS issues
-            await fetch(`${this.options.apiUrl}errors/log`, {
-                method: 'POST',
-                headers: {
-                    'X-WP-Nonce': window.feedSettings?.nonce || ''
-                },
-                body: data
-            });
-        } catch (e) {
-            // Silently fail - we don't want errors in error reporting
-            console.warn('Failed to log error to server', e);
-        }
-    }
+			const data = new FormData();
+			data.append('error_type', type);
+			data.append('message', message);
+			data.append('context', JSON.stringify(enhancedContext));
+
+			await fetch(`${this.options.apiUrl}errors/log`, {
+				method: 'POST',
+				headers: {
+					'X-WP-Nonce': window.auth.getNonce()
+				},
+				body: data
+			});
+		} catch (e) {
+			console.warn('Failed to log error to server', e);
+		}
+	}
+
+	/**
+	 * Extract component name from error stack
+	 */
+	extractComponentFromStack(stack) {
+		if (!stack) return 'Unknown';
+
+		// Try to extract class/component name from stack trace
+		const match = stack.match(/at\s+(\w+)\./);
+		return match ? match[1] : 'Unknown';
+	}
+
+	/**
+	 * Extract method name from error stack
+	 */
+	extractMethodFromStack(stack) {
+		if (!stack) return null;
+
+		// Try to extract method name
+		const match = stack.match(/at\s+\w+\.(\w+)\s+/);
+		return match ? match[1] : null;
+	}
 
     /**
      * Display error notification
@@ -213,8 +242,8 @@
      */
     handleAuthError() {
         // Redirect to login page if user isn't logged in
-        if (window.feedSettings && window.feedSettings.loginUrl) {
-            window.location.href = window.feedSettings.loginUrl;
+        if (window.jvbSettings && window.jvbSettings.loginUrl) {
+            window.location.href = window.jvbSettings.loginUrl;
             return;
         }
 
@@ -345,14 +374,19 @@
         });
     }
 }
-document.addEventListener('DOMContentLoaded', function () {
-	window.jvbError = new ErrorHandler({
-		api: jvbSettings.api,
-		logToServer: true,
-		displayNotifications: true,
-		notificationDuration: 5000,
-		retryEnabled: true,
-		maxRetries: 3
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbError = new ErrorHandler({
+				api: jvbSettings.api,
+				logToServer: true,
+				displayNotifications: true,
+				notificationDuration: 5000,
+				retryEnabled: true,
+				maxRetries: 3
+			});
+		}
 	});
+
 });
 
diff --git a/assets/js/dash/FavouritesManager.js b/assets/js/concise/FavouritesManager.js
similarity index 98%
rename from assets/js/dash/FavouritesManager.js
rename to assets/js/concise/FavouritesManager.js
index e204a04..63a1542 100644
--- a/assets/js/dash/FavouritesManager.js
+++ b/assets/js/concise/FavouritesManager.js
@@ -334,8 +334,8 @@
                 {
                     method: 'GET',
                     headers: {
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.favourites,
+                        'X-WP-Nonce': window.auth.getNonce(),
+                        'action_nonce': window.auth.getNonce('favourites'),
                     }
                 },{
                     context: 'favouritesManager',
@@ -1022,8 +1022,8 @@
                 {
                     method: 'GET',
                     headers: {
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.favourites
+                        'X-WP-Nonce': window.auth.getNonce(),
+                        'action_nonce': window.auth.getNonce('favourites')
                     }
                 },
                 {
@@ -1186,8 +1186,8 @@
                 {
                     method: 'GET',
                     headers: {
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.favourites
+                        'X-WP-Nonce': window.auth.getNonce(),
+                        'action_nonce': window.auth.getNonce('favourites')
                     }
                 },
                 {
@@ -1600,8 +1600,8 @@
                 {
                     method: 'GET',
                     headers: {
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.favourites
+                        'X-WP-Nonce': window.auth.getNonce(),
+                        'action_nonce': window.auth.getNonce('favourites')
                     }
                 },
                 {
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 0e53476..38c1c22 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -8,6 +8,7 @@
 			collectFormData: false,
 			... config
 		}
+		this.isRestoring = false;
 		const store = window.jvbStore.register(
 			'forms',
 			{
@@ -57,17 +58,14 @@
 			remove: 800,
 			reorder: 1000
 		};
-		this.isTimeline = false;
-		if (window.crudManager && window.crudManager.isTimeline) {
-			this.isTimeline = true;
-		}
+
+		this.isTimeline = window.crudManager && window.crudManager.isTimeline;
 
 		// Bind handlers
 		this.clickHandler = this.handleClick.bind(this);
 		this.changeHandler = this.handleChange.bind(this);
 		this.submitHandler = this.handleSubmit.bind(this);
 		this.inputHandler = this.handleInput.bind(this);
-		this.focusHandler = this.handleFocus.bind(this);
 		this.blurHandler = this.handleBlur.bind(this);
 		//Processors
 		this.processRepeaterField = this.processRepeaterField.bind(this);
@@ -127,35 +125,68 @@
 		}
 	}
 
-	checkPendingForms() {
-		// No async needed - data is already loaded in memory
-		const allForms = this.store.getAll();
-		const pendingForms = allForms.filter(form => form.status === 'draft');
+	/**
+	 * Check for pending forms from current page
+	 */
+	async checkPendingForms() {
+		const allForms = await this.store.getAll();
+		const currentPath = window.location.pathname;
+
+		const pendingForms = allForms.filter(form => {
+			if (form.status !== 'draft') return false;
+
+			// Check if form is from current page
+			const formPath = form.data?._wp_http_referer;
+			return formPath === currentPath;
+		});
 
 		pendingForms.forEach(item => {
-			const form = this.forms.get(item.formId);
-			if (form?.element) {
-				const restoreBtn = form.element.querySelector('.restore-form');
-				if (restoreBtn) {
-					restoreBtn.hidden = false;
-				}
-				new this.populateForm(form.element, item.data);
+			const formElement = this.findFormElement(item);
+			if (!formElement) return;
+
+			// Register form if not already registered
+			let formConfig = this.forms.get(item.formId);
+			if (!formElement.dataset.formId) {
+				formConfig = this.registerForm(formElement);
+			}
+
+			// Set flag to prevent event handlers from firing
+			this.isRestoring = true;
+			// Auto-populate the form
+			new this.populateForm(formElement, item.data);
+
+			// Reset flag after a tick (gives DOM time to settle)
+			setTimeout(() => {
+				this.isRestoring = false;
+			}, 0);
+
+			// Show restore status
+			this.showFormStatus(item.formId, 'restored');
+
+			if (window.jvbA11y) {
+				window.jvbA11y.announce('Your previous entry has been restored');
 			}
 		});
 	}
+
 	/**
-	 * Check for pending operations from previous session
+	 * Find form element that matches the cached data
 	 */
-	async checkPendingOperations() {
-		const pendingForms = await this.store.query('status', 'pending');
+	findFormElement(formData) {
+		// Try by form_id first (hidden field)
+		if (formData.data?.form_id) {
+			const form = document.querySelector(`[name="form_id"][value="${formData.data.form_id}"]`)?.closest('form');
+			if (form) return form;
+		}
 
-		if (pendingForms.length === 0) return;
+		// Try by form_type
+		if (formData.data?.form_type) {
+			const form = document.querySelector(`[name="form_type"][value="${formData.data.form_type}"]`)?.closest('form');
+			if (form) return form;
+		}
 
-		// Group by form type or page
-		const grouped = this.groupPendingForms(pendingForms);
-
-		// Show consolidated notification
-		this.showPendingNotification(grouped);
+		// Fallback: try by formId (if it was already registered)
+		return document.querySelector(`[data-form-id="${formData.formId}"]`);
 	}
 
 	/**
@@ -236,7 +267,6 @@
 		if (!this.globalHandlersAdded) {
 			document.addEventListener('click', this.clickHandler);
 			document.addEventListener('change', this.changeHandler);
-			document.addEventListener('focus', this.focusHandler, true);
 			document.addEventListener('blur', this.blurHandler, true);
 			document.addEventListener('input', this.inputHandler);
 			this.globalHandlersAdded = true;
@@ -260,7 +290,7 @@
 			options: {
 				autosave: 'autosave' in formElement.dataset,
 				saveDelay: this.autoSaveDefaults.delay,
-				endpoint: formElement.dataset.save??'',
+				endpoint: formElement.dataset.save ?? '',
 				formStatus: true,
 				cache: true,
 				...options
@@ -269,17 +299,14 @@
 			data: this.collectFormData(formElement, true),
 		};
 
-		// Initialize special fields
 		this.initializeFormFields(formElement, formConfig);
-
-		// Store form config
 		this.forms.set(formId, formConfig);
 
-		// Check for pending data
+		// Check for pending data - FIXED
 		if (this.store && formConfig.options.cache) {
 			const cached = this.store.get(formId);
-			if (cached && cached.formData) {
-				this.showPendingNotification(cached);
+			if (cached && cached.data) {
+				this.showPendingNotification(formId, cached.data);
 			}
 		}
 
@@ -296,6 +323,8 @@
 		// Initialize repeater fields
 		this.initRepeaterFields(form, formConfig);
 
+		this.initTagListFields(form, formConfig);
+
 		// Initialize conditional fields
 		if (formConfig) {
 			this.initConditionalFields(form, formConfig);
@@ -586,6 +615,231 @@
 	}
 
 	/**
+	 * Initialize tag list fields
+	 */
+	initTagListFields(form, formConfig) {
+		form.querySelectorAll('.field.tag-list').forEach(field => {
+			const inputRow = field.querySelector('.tag-input-row');
+			const addButton = field.querySelector('.add-tag-item');
+			const tagsContainer = field.querySelector('.tag-items');
+			const template = field.querySelector('.tag-template');
+			const fieldName = field.dataset.field;
+			const tagFormat = field.dataset.tagFormat || 'first_field';
+
+			if (!inputRow || !addButton || !tagsContainer || !template) return;
+
+			// Get all input fields in the input row (excluding the button)
+			const getInputFields = () => {
+				return Array.from(inputRow.querySelectorAll('input, select, textarea'))
+					.filter(input => !input.closest('button'));
+			};
+
+			// Add tag handler
+			const addTag = () => {
+				const inputs = getInputFields();
+				const data = {};
+				let hasValue = false;
+
+				// Collect values from inputs
+				inputs.forEach(input => {
+					const fieldName = input.name.replace('new_', '');
+					const value = this.getFieldValue(input);
+
+					if (value) hasValue = true;
+					data[fieldName] = value;
+				});
+
+				if (!hasValue) {
+					if (window.jvbA11y) {
+						window.jvbA11y.announce('Please fill in at least one field', 'error');
+					}
+					inputs[0].focus();
+					return;
+				}
+
+				// Validate required fields using data-required attribute
+				const invalidField = inputs.find(input => {
+					const isRequired = ('required' in input.dataset && input.dataset.required === '1');
+					const value = this.getFieldValue(input);
+					return isRequired && !value;
+				});
+
+				if (invalidField) {
+					const fieldWrapper = invalidField.closest('.field');
+					const fieldLabel = fieldWrapper?.querySelector('label')?.textContent || 'This field';
+					this.showError(fieldWrapper, `${fieldLabel} is required.`);
+
+					invalidField.focus();
+					return;
+				}
+
+				for (let input of inputs) {
+					let wrapper = field.closest('.field');
+					if (!this.validateField(input, wrapper)){
+						input.focus();
+						return;
+					}
+				}
+
+				// Clone template and populate
+				const index = tagsContainer.children.length;
+				const newTag = template.content.cloneNode(true).firstElementChild;
+				newTag.dataset.index = index;
+
+				// Update tag label
+				const tagLabel = newTag.querySelector('.tag-label');
+				if (tagLabel) {
+					tagLabel.textContent = this.getTagDisplayText(data, tagFormat);
+				}
+
+				// Update hidden inputs
+				newTag.querySelectorAll('input[type="hidden"]').forEach(input => {
+					const fieldKey = input.dataset.field;
+					input.name = `${fieldName}:${index}:${fieldKey}`;
+					input.value = data[fieldKey] || '';
+				});
+
+				tagsContainer.appendChild(newTag);
+
+				// Clear inputs
+				inputs.forEach(input => {
+					if (input.type === 'checkbox' || input.type === 'radio') {
+						input.checked = false;
+					} else {
+						input.value = '';
+					}
+					let field = input.closest('.field');
+					this.clearValidation(field);
+				});
+
+				// Focus first input
+				if (inputs.length > 0) {
+					inputs[0].focus();
+				}
+
+
+				// Schedule save
+				if (formConfig) {
+					this.scheduleSave(formConfig, {
+						type: 'tag_list',
+						action: 'add',
+						fieldName: fieldName,
+						delay: this.autoSaveDefaults.delay
+					});
+				}
+
+				if (window.jvbA11y) {
+					window.jvbA11y.announce('Item added');
+				}
+			};
+
+			// Add button click
+			addButton.addEventListener('click', addTag);
+
+			// Enter key support on last input
+			const inputs = getInputFields();
+			if (inputs.length > 0) {
+				// Tab through inputs, Enter on last one adds the tag
+				inputs[inputs.length - 1].addEventListener('keypress', (e) => {
+					if (e.key === 'Enter') {
+						e.preventDefault();
+						addTag();
+					}
+				});
+
+				// Enter on other inputs moves to next field
+				inputs.slice(0, -1).forEach((input, i) => {
+					input.addEventListener('keypress', (e) => {
+						if (e.key === 'Enter') {
+							e.preventDefault();
+							inputs[i + 1].focus();
+						}
+					});
+				});
+			}
+
+			// Remove tag handler
+			tagsContainer.addEventListener('click', (e) => {
+				if (e.target.closest('.remove-tag')) {
+					const tag = e.target.closest('.tag-item');
+					const tagText = tag.querySelector('.tag-label')?.textContent || 'Item';
+
+					tag.remove();
+
+					// Reindex remaining tags
+					this.reindexTagList(tagsContainer, fieldName);
+
+					// Schedule save
+					if (formConfig) {
+						this.scheduleSave(formConfig, {
+							type: 'tag_list',
+							action: 'remove',
+							fieldName: fieldName,
+							delay: this.autoSaveDefaults.delay
+						});
+					}
+
+					if (window.jvbA11y) {
+						window.jvbA11y.announce(`${tagText} removed`);
+					}
+				}
+			});
+		});
+	}
+
+	/**
+	 * Reindex tag list items
+	 */
+	reindexTagList(container, baseFieldName) {
+		Array.from(container.children).forEach((tag, index) => {
+			tag.dataset.index = index;
+
+			tag.querySelectorAll('input[type="hidden"]').forEach(input => {
+				const fieldKey = input.dataset.field;
+				input.name = `${baseFieldName}:${index}:${fieldKey}`;
+			});
+		});
+	}
+
+	/**
+	 * Get display text for tag based on format
+	 */
+	getTagDisplayText(data, format) {
+		const values = Object.values(data).filter(v => v);
+
+		if (values.length === 0) return 'New Item';
+
+		switch (format) {
+			case 'first_field':
+				return values[0];
+
+			case 'all_fields':
+				return values.join(', ');
+
+			default:
+				// Template format like "{name} ({email})"
+				if (format.includes('{')) {
+					let text = format;
+					for (const [key, value] of Object.entries(data)) {
+						text = text.replace(`{${key}}`, value);
+					}
+					return text;
+				}
+				// Use specific field
+				return data[format] || values[0];
+		}
+	}
+
+	/**
+	 * HTML escape helper
+	 */
+	escapeHtml(text) {
+		const div = document.createElement('div');
+		div.textContent = text;
+		return div.innerHTML;
+	}
+
+	/**
 	 * Initialize conditional fields
 	 */
 	initConditionalFields(form, formConfig) {
@@ -630,8 +884,8 @@
 		const requiredStr = String(requiredValue || '');
 
 		switch (operator) {
-			case '==': return fieldStr == requiredStr;
-			case '!=': return fieldStr != requiredStr;
+			case '==': return fieldStr === requiredStr;
+			case '!=': return fieldStr !== requiredStr;
 			case '>': return parseFloat(fieldStr) > parseFloat(requiredStr);
 			case '<': return parseFloat(fieldStr) < parseFloat(requiredStr);
 			case '>=': return parseFloat(fieldStr) >= parseFloat(requiredStr);
@@ -639,7 +893,7 @@
 			case 'contains': return fieldStr.includes(requiredStr);
 			case 'empty': return fieldStr === '';
 			case 'not_empty': return fieldStr !== '';
-			default: return fieldStr == requiredStr;
+			default: return fieldStr === requiredStr;
 		}
 	}
 
@@ -712,8 +966,8 @@
 
 	async handleSubmit(event) {
 		const form = event.target;
-		if (!form.dataset.formId) return;
 
+		if (!form.dataset.formId) return;
 		const formConfig = this.forms.get(form.dataset.formId);
 
 		// Handle subscriber-based forms
@@ -787,7 +1041,7 @@
 			form.insertBefore(successBox, form.firstChild);
 		}
 
-		// ✅ DELETE CACHED FORM DATA ON SUCCESS
+		//  DELETE CACHED FORM DATA ON SUCCESS
 		if (form.dataset.formId) {
 			this.store.delete(form.dataset.formId).catch(err => {
 				console.warn('Failed to clear form cache:', err);
@@ -882,17 +1136,25 @@
 			let container = window.targetCheck(e, 'div.quantity');
 			this.handleNumberClick(e, container.querySelector('input'));
 		} else if (window.targetCheck(e, '[data-action]')) {
-			let action = window.targetCheck(e, '[data-action]');
-			action = action.dataset.action;
+			let actionEl = window.targetCheck(e, '[data-action]');
+			let action = actionEl.dataset.action;
+			let form = actionEl.closest('form');
+
 			switch (action) {
 				case 'clear-form':
-					let form = e.target.closest('form');
-					this.store.delete(form.dataset.formId);
-					form?.reset();
-					e.target.closest('.restore-form').hidden = true;
+					if (form?.dataset.formId) {
+						this.store.delete(form.dataset.formId);
+						form.reset();
+						// Hide the status message
+						form.querySelector('.fstatus').hidden = true;
+					}
+					if (window.jvbA11y) {
+						window.jvbA11y.announce('Form cleared, starting fresh');
+					}
 					break;
+
 				case 'dismiss-restore':
-					e.target.closest('.restore-form').hidden = true;
+					form.querySelector('.fstatus').hidden = true;
 					break;
 			}
 		}
@@ -954,7 +1216,7 @@
 	}
 
 	handleChange(event) {
-		if (event.target.closest('[data-ignore]')) {
+		if (event.target.closest('[data-ignore]') || this.isRestoring) {
 			return;
 		}
 		const target = event.target;
@@ -978,16 +1240,8 @@
 		}
 	}
 
-	handleFocus(event) {
-		const target = event.target;
-		if (target.matches('input, textarea, select')) {
-			// Track focus for better UX
-			this.currentFocus = target;
-		}
-	}
-
 	handleBlur(e) {
-		if (e.target.closest('[data-ignore]')) {
+		if (e.target.closest('[data-ignore]') || this.isRestoring) {
 			return;
 		}
 		const target = e.target;
@@ -1023,7 +1277,7 @@
 	}
 
 	handleInput(e) {
-		if (e.target.closest('[data-ignore]') || ! e.target.closest('form')) {
+		if (e.target.closest('[data-ignore]') || ! e.target.closest('form') || this.isRestoring) {
 			return;
 		}
 		const input = e.target.closest('input, textarea, select');
@@ -1043,7 +1297,7 @@
 		if (this.shouldDebounce(input)){
 			window.debouncer.schedule(
 				`validate_${fieldName}`,
-				(input, fieldWrapper) => this.validateField.bind(this),
+				() => this.validateField.bind(this),
 				500
 			)
 		}
@@ -1063,7 +1317,7 @@
 			},
 			url: {
 				pattern: /^https?:\/\/.+\..+/,
-				message: 'Please enter a valid URL starting with http:// or https://'
+				message: 'Please enter a valid URL starting with https://'
 			},
 			phone: {
 				pattern: /^[\d\s\-\+\(\)\.]+$/,
@@ -1191,23 +1445,7 @@
 		return true;
 	}
 
-	/**
-	 * Get field value (handles different input types)
-	 */
-	getFieldValue(input) {
-		if (!input) return '';
 
-		if (input.type === 'checkbox') {
-			return input.checked ? input.value || '1' : '';
-		} else if (input.type === 'radio') {
-			const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
-			return checked ? checked.value : '';
-		} else if (input.type === 'select-multiple') {
-			return Array.from(input.selectedOptions).map(o => o.value);
-		}
-
-		return input.value?.trim() || '';
-	}
 
 	/**
 	 * Show success state (green checkmark)
@@ -1550,9 +1788,8 @@
 	}
 
 	showFormStatus(formID, status, message='') {
-		// Remove existing status
 		let form = this.forms.get(formID);
-		if (!form.options.formStatus) {
+		if (!form?.options.formStatus) {
 			return;
 		}
 
@@ -1562,12 +1799,12 @@
 
 		form.status = status;
 
-		// Add new status
 		const statusWrap = form.element.querySelector('.fstatus');
 		statusWrap.hidden = false;
 		const statusElement = statusWrap.querySelector('.message');
 		statusElement.textContent = '';
 		statusWrap.querySelector('.icon')?.remove();
+		statusWrap.querySelector('.actions')?.remove(); // Clear old actions
 
 		const messages = {
 			'saving': 'Saving changes...',
@@ -1575,12 +1812,15 @@
 			'uploading': 'Uploading your form to server',
 			'submitted': 'Successfully sent to server',
 			'pending': 'Unsaved changes',
+			'restored': 'Welcome back! We\'ve restored your previous entry.',
 			'error': 'Failed to save changes. Refresh and try again?',
 			'offline': 'Changes will be saved when online'
 		};
+
 		const icons = {
 			'autosaved': 'check-circle',
 			'submitted': 'check-circle',
+			'restored': 'history',
 			'error': 'close-circle',
 			'offline': 'cloud-slash',
 			'pending': 'exclamation-mark'
@@ -1590,12 +1830,27 @@
 		if (icon) {
 			statusWrap.prepend(icon);
 		}
+
 		if (message === '') {
 			message = messages[status] || status;
 		}
 		statusElement.textContent = message;
 		statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
 
+		// Add action buttons for certain statuses
+		if (status === 'restored') {
+			const actions = document.createElement('div');
+			actions.className = 'actions';
+			actions.innerHTML = `
+            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>
+            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>
+        `;
+			statusWrap.appendChild(actions);
+
+			// Auto-dismiss after 10 seconds
+			setTimeout(() => statusWrap.hidden = true, 10000);
+		}
+
 		// Auto-hide success messages
 		if (status === 'submitted') {
 			setTimeout(() => statusWrap.hidden = true, 3000);
@@ -1640,7 +1895,7 @@
 			const processor = this.getFieldProcessor(key);
 			processor(key, value, data, repeaterData, postData, form);
 		}
-		if (!window.isEmptyObject(postData)) {
+		if (Object.keys(postData).length !== 0) {
 			data = this.mergeRepeaterData(data, repeaterData);
 			return this.mergePostData(data, postData);
 		}
@@ -1810,19 +2065,22 @@
 		}
 	}
 
-	getFieldValue(field) {
-		if (!field) return '';
+	/**
+	 * Get field value (handles different input types)
+	 */
+	getFieldValue(input) {
+		if (!input) return '';
 
-		if (field.type === 'checkbox') {
-			return field.checked ? field.value || '1' : '';
-		} else if (field.type === 'radio') {
-			const checked = field.form.querySelector(`[name="${field.name}"]:checked`);
+		if (input.type === 'checkbox') {
+			return input.checked ? input.value || '1' : '';
+		} else if (input.type === 'radio') {
+			const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
 			return checked ? checked.value : '';
-		} else if (field.type === 'select-multiple') {
-			return Array.from(field.selectedOptions).map(o => o.value);
-		} else {
-			return field.value;
+		} else if (input.type === 'select-multiple') {
+			return Array.from(input.selectedOptions).map(o => o.value);
 		}
+
+		return input.value?.trim() || '';
 	}
 
 	getChangedFields(original, current) {
@@ -1841,16 +2099,8 @@
 
 		const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
 		const summary = window.getTemplate('formSummary');
-
-		const [
-			title,
-			resultWrapper,
-			resultTemplate
-		] = [
-			summary.querySelector('h2'),
-			summary.querySelector('.summary'),
-			summary.querySelector('.result')
-		];
+		if (!summary) return;
+		const wrapper = summary.querySelector('.result');
 
 		// Fields to skip in summary
 		const skipFields = ['sendAll', ...this.ignore];
@@ -1864,23 +2114,26 @@
 
 			// Get field info from form
 			const fieldInfo = this.getFieldInfo(form, key);
+
 			if (!fieldInfo.label) continue; // Skip if no label found
 
-			// Create result element
-			const resultEl = this.createResultElement(
-				resultTemplate,
-				fieldInfo,
-				value,
-				form
-			);
+			let field = wrapper.cloneNode(true);
+			let title = field.querySelector('h3');
+			let p = field.querySelector('p');
 
-			if (resultEl) {
-				resultWrapper.appendChild(resultEl);
+			title.textContent = fieldInfo.label;
+			let formatted = this.formatFieldValue(value, fieldInfo.type);
+			if (this.isHtmlContent(formatted)) {
+				p.innerHTML = formatted;
+			} else {
+				p.textContent = formatted;
 			}
+
+			summary.append(field);
 		}
 
 		// Remove template
-		resultTemplate.remove();
+		wrapper.remove();
 
 		// Insert summary and hide form
 		clear = (clear !== 'form') ? form.closest(clear)??form : form;
@@ -1912,8 +2165,8 @@
 	getFieldInfo(form, fieldName) {
 		// Try to find label by 'for' attribute (exact match)
 		let label = form.querySelector(`label[for="${fieldName}"]`);
-		let input = null;
-		let fieldWrapper = null;
+		let input = form.querySelector(`[name=${fieldName}]`);
+		let fieldWrapper = input?.closest('.field');
 
 		// Try to find the input field - check multiple patterns
 		if (!input) {
@@ -1973,32 +2226,6 @@
 	}
 
 	/**
-	 * Create a result element for a field
-	 */
-	createResultElement(template, fieldInfo, value, form) {
-		const resultEl = template.cloneNode(true);
-		const titleEl = resultEl.querySelector('h4');
-		const valueEl = resultEl.querySelector('p');
-
-		// Set label
-		titleEl.textContent = fieldInfo.label;
-
-		// Format value based on field type
-		const formattedValue = this.formatFieldValue(value, fieldInfo.type, form);
-
-		// Determine how to set the value
-		if (this.isHtmlContent(formattedValue)) {
-			// HTML content - use innerHTML
-			valueEl.innerHTML = formattedValue;
-		} else {
-			// Plain text - use textContent for safety
-			valueEl.textContent = formattedValue;
-		}
-
-		return resultEl;
-	}
-
-	/**
 	 * Check if content should be treated as HTML
 	 */
 	isHtmlContent(content) {
@@ -2346,7 +2573,6 @@
 		// Remove global handlers
 		if (this.globalHandlersAdded) {
 			document.removeEventListener('change', this.changeHandler);
-			document.removeEventListener('focus', this.focusHandler, true);
 			document.removeEventListener('blur', this.blurHandler, true);
 			document.removeEventListener('input', this.inputHandler, true);
 		}
diff --git a/assets/js/concise/FrontendFavourites.js b/assets/js/concise/FrontendFavourites.js
index b558f5f..94e2920 100644
--- a/assets/js/concise/FrontendFavourites.js
+++ b/assets/js/concise/FrontendFavourites.js
@@ -13,7 +13,7 @@
 				TTL: 6 * 60 * 1000,
 				showLoading: false,
 				filters: {
-					user: jvbSettings.currentUser,
+					user: window.auth.getUser(),
 					content: 'all',
 					order: 'desc',
 					orderby: 'date',
@@ -51,7 +51,7 @@
 	}
 
 	toggleFavourite(button) {
-		if (!jvbSettings.currentUser) {
+		if (!window.auth.getUser()) {
 			window.location.href = jvbSettings.redirect + '&action=register&type=favourites';
 			return;
 		}
@@ -183,7 +183,7 @@
 }
 document.addEventListener('DOMContentLoaded', function() {
 	window.jvbFavourites = false;
-	if (jvbSettings.currentUser !== '') {
+	if (window.auth.getUser() !== '') {
 		window.jvbFavourites = new FrontendFavourites();
 	}
 });
diff --git a/assets/js/concise/FrontendVotes.js b/assets/js/concise/FrontendVotes.js
index fefc95a..5300550 100644
--- a/assets/js/concise/FrontendVotes.js
+++ b/assets/js/concise/FrontendVotes.js
@@ -11,7 +11,7 @@
 	}
 
 	handleVote(button) {
-		if (!jvbSettings.currentUser) {
+		if (!window.auth.getUser()) {
 			window.location.href = jvbSettings.redirect + '&action=register&type=vote';
 			return;
 		}
@@ -42,7 +42,7 @@
 	}
 
 	isFavourited(content, id){
-		if(!jvbSettings.currentUser){
+		if(!window.auth.getUser()){
 			return false;
 		}
 		let item = this.store.getItem(id);
@@ -50,7 +50,7 @@
 	}
 }
 window.jvbVotes = false;
-if (jvbSettings.currentUser !== '') {
+if (window.auth.getUser() !== '') {
 	window.jvbVotes = new FrontendFavourites();
 }
 
diff --git a/assets/js/concise/Gallery.js b/assets/js/concise/Gallery.js
index 07eaef1..9c3e0bd 100644
--- a/assets/js/concise/Gallery.js
+++ b/assets/js/concise/Gallery.js
@@ -33,7 +33,7 @@
 	 *********************************************************************/
 	initElements() {
 		this.elements = {
-			imageSelector: 'a.open-gallery',
+			imageSelector: 'img[data-gallery]',
 			gallery: {
 				modal: 'dialog.gallery',
 				wrap: '.wrap',
@@ -63,18 +63,17 @@
 		});
 	}
 	buildGalleryItems(filtered = null) {
-		let selector = filtered ? `[data-opens="${filtered}"]` : this.elements.imageSelector;
+		let selector = filtered ? `[data-gallery="${filtered}"]` : this.elements.imageSelector;
 		this.items = Array.from(document.querySelectorAll(selector))
 			.map((img, index) => {
-				let image = img.querySelector('img');
-
 				return {
 					id: img.dataset.id||index,
-					small: image.dataset.small || img.src,
-					medium: image.dataset.medium || img.src,
-					full: image.dataset.full || img.src,
-					alt: image.alt || '',
-					element: image
+					srcset: img.srcset || img.src, // Clone the srcset from page
+					sizes: img.sizes || '100vw',
+					src: img.currentSrc || img.src, // Fallback
+					full: img.dataset.full || img.src,
+					alt: img.alt || '',
+					element: img
 				};
 			});
 	}
@@ -95,9 +94,10 @@
 		let target = window.targetCheck(e, this.elements.imageSelector);
 		if (target && !this.modal.isOpen) {
 			e.preventDefault();
-			this.buildGalleryItems((Object.hasOwn(target.dataset, 'opens')) ? target.dataset.opens : null);
+			this.buildGalleryItems(target.dataset.gallery || null);
 
-			this.index = this.items.findIndex(item => item.element === target.querySelector('img'));
+			// Target is now the img element itself
+			this.index = this.items.findIndex(item => item.element === target);
 			this.toggleGallery(true);
 		} else if (this.modal.isOpen) {
 			if (window.targetCheck(e, this.elements.gallery.nextButton)) {
@@ -140,6 +140,9 @@
 	}
 
 	onPointerDown(e) {
+		// Always prevent default to stop browser's native image drag
+		e.preventDefault();
+
 		this.swipe.startX = e.clientX;
 		this.swipe.startY = e.clientY;
 		this.ui.gallery.image.setPointerCapture(e.pointerId);
@@ -177,6 +180,8 @@
 			this.zoom.panning = true;
 			this.zoom.startX = e.clientX - this.zoom.x;
 			this.zoom.startY = e.clientY - this.zoom.y;
+			// Change cursor to grabbing
+			this.ui.gallery.image.style.cursor = 'grabbing';
 		}
 	}
 
@@ -217,6 +222,7 @@
 			this.pinchStartDist = 0;
 		}
 
+		// Only check for swipe if we weren't panning and no more active pointers
 		if (!this.zoom.panning && this.activePointers.size === 0) {
 			// End of tap or swipe - detect swipe
 			this.swipe.endX = e.clientX;
@@ -233,8 +239,13 @@
 					this.nextElement();
 				}
 			}
+		}
 
+		// Reset panning state when all pointers are released
+		if (this.activePointers.size === 0) {
 			this.zoom.panning = false;
+			// Reset cursor based on zoom state
+			this.ui.gallery.image.style.cursor = this.zoom.scale > 1 ? 'grab' : 'default';
 		}
 	}
 
@@ -321,6 +332,8 @@
 		// this.clampPan();
 		const img = this.ui.gallery.image;
 		img.style.transform = `translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`;
+		// Update cursor based on zoom level
+		img.style.cursor = this.zoom.scale > 1 ? 'grab' : 'default';
 	}
 	resetZoom() {
 		this.zoom.scale = 1;
@@ -348,6 +361,10 @@
 	 */
 	toggleGallery(open, index= null) {
 		if (open) {
+			// Disable native image dragging
+			this.ui.gallery.image.draggable = false;
+			this.ui.gallery.image.style.userSelect = 'none';
+
 			this.ui.gallery.image.addEventListener("pointerdown", this.pointerDownHandler);
 			this.ui.gallery.image.addEventListener("pointermove", this.pointerMoveHandler);
 			this.ui.gallery.image.addEventListener("pointerup", this.pointerUpHandler);
@@ -406,8 +423,30 @@
 	updateDisplay() {
 		const item = this.items[this.index];
 		if (!item) return;
-		this.ui.gallery.image.src = item.full;
-		this.ui.gallery.image.alt = item.alt;
+
+		const galleryImg = this.ui.gallery.image;
+
+		// Set srcset first - browser uses cached version instantly (no wait)
+		if (item.srcset) {
+			galleryImg.srcset = item.srcset;
+			galleryImg.sizes = item.sizes;
+		}
+		galleryImg.src = item.src; // Fallback
+		galleryImg.alt = item.alt;
+
+		// ALWAYS load full resolution for zoom quality
+		if (item.full && item.full !== item.src) {
+			const fullImg = new Image();
+			fullImg.onload = () => {
+				if (this.items[this.index] === item) {
+					galleryImg.src = item.full;
+					galleryImg.removeAttribute('srcset'); // Switch to full res directly
+					galleryImg.removeAttribute('sizes');
+				}
+			};
+			fullImg.src = item.full;
+		}
+
 		this.ui.gallery.counter.textContent = `${this.index + 1} / ${this.items.length}`;
 
 		this.ui.gallery.prevButton.disabled = this.items.length <= 1;
diff --git a/assets/js/dash/GoogleMaps.js b/assets/js/concise/GoogleMaps.js
similarity index 100%
rename from assets/js/dash/GoogleMaps.js
rename to assets/js/concise/GoogleMaps.js
diff --git a/assets/js/dash/Integrations.js b/assets/js/concise/Integrations.js
similarity index 95%
rename from assets/js/dash/Integrations.js
rename to assets/js/concise/Integrations.js
index e0d9ed6..0beba0b 100644
--- a/assets/js/dash/Integrations.js
+++ b/assets/js/concise/Integrations.js
@@ -163,7 +163,6 @@
 			return;
 		}
 
-		console.log('Clicked!');
 		if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
 			e.preventDefault();
 			let target = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button');
@@ -299,7 +298,7 @@
 			const data = {
 				service: service,
 				action: action,
-				user_id: jvbSettings.currentUser,
+				user_id: window.auth.getUser(),
 				data: {}
 			};
 			if (!isButton) {
@@ -315,15 +314,13 @@
 				}
 			}
 
-			console.log('Sending Data:', data);
-
 			// Make API request
 			const response = await fetch(
 				jvbSettings.api + 'integrations', {
 				method: 'POST',
 				headers: {
 					'Content-Type': 'application/json',
-					'X-WP-Nonce': jvbSettings.nonce
+					'X-WP-Nonce': window.auth.getNonce()
 				},
 				body: JSON.stringify(data)
 			});
@@ -340,7 +337,6 @@
 						this.showNotification('Settings saved successfully', 'success');
 						break;
 				}
-				console.log(result);
 				this.updateUI(form, status);
 
 				if (result.reload) {
@@ -349,7 +345,6 @@
 					}, 50);
 				}
 			} else {
-				console.log (result);
 				this.updateUI(form, 'error', result.message??'');
 				this.showNotification(result.message || 'Operation failed', 'error');
 			}
@@ -366,7 +361,6 @@
 	{
 		let allowed = ['connected', 'disconnected', 'hasChanges', 'syncing', 'error'];
 		if (!allowed.includes(state)) {
-			console.log('Invalid state: ', state);
 			return;
 		}
 		let defaults = {
@@ -391,9 +385,7 @@
 
 		form.classList.remove(...allowed);
 		form.classList.add(state, 'flash');
-		console.log(form);
 		let status = form.querySelector('.setup .text');
-		console.log(status);
 		status.textContent = message;
 		// Enable/disable buttons
 		if (state === 'syncing') {
@@ -415,7 +407,6 @@
 	// Add popup indicator to URL
 	url += (url.indexOf('?') > -1 ? '&' : '?') + 'popup=1';
 
-	console.log('Opening OAuth popup for', service, 'with URL:', url);
 
 	const popup = window.open(
 		url,
@@ -430,8 +421,6 @@
 
 	// Set up listener for OAuth completion
 	window.jvbOAuthComplete = function(completedService, success, message) {
-		console.log('OAuth complete:', completedService, success, message);
-
 		if (completedService === service) {
 			if (success) {
 				// Show success message
@@ -459,7 +448,6 @@
 		try {
 			if (popup.closed) {
 				clearInterval(checkPopup);
-				console.log('OAuth popup closed');
 				// Refresh anyway in case auth completed
 				setTimeout(() => {
 					jvbRefreshIntegration(service);
@@ -475,14 +463,13 @@
 
 // Refresh integration display
 window.jvbRefreshIntegration = function(service) {
-	console.log('Refreshing integration:', service);
 
 	// Use your REST API to check connection status
 	fetch(jvbSettings.api + 'integrations', {
 		method: 'POST',
 		headers: {
 			'Content-Type': 'application/json',
-			'X-WP-Nonce': jvbSettings.nonce
+			'X-WP-Nonce': window.auth.getNonce()
 		},
 		body: JSON.stringify({
 			service: service,
@@ -521,5 +508,11 @@
 			location.reload();
 		});
 };
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.integrations = new IntegrationsManager();
+		}
+	});
+});
 
-window.integrations = new IntegrationsManager();
diff --git a/assets/js/concise/JVBase.js b/assets/js/concise/JVBase.js
deleted file mode 100644
index 50c452a..0000000
--- a/assets/js/concise/JVBase.js
+++ /dev/null
@@ -1,388 +0,0 @@
-class JVB {
-	constructor(config = {}) {
-		this.config = config;
-		this.content = null;
-
-		this.resetFilters();
-
-		this.initLoading();
-		this.events = new Map();
-
-		//ICONS
-		this.icons = new Map();
-		this.templates = new Map();
-		//DEBOUNCER
-		this.timeouts = new Map();
-		window.addEventListener('beforeunload', () => this.cleanup());
-
-
-		this.loadTemplates();
-	}
-
-
-
-	/********************************************
-	 * FILTERS
-	********************************************/
-	resetFilters(elements = false) {
-		this.filters = {
-			content: this.content,
-			status: 'all',
-			taxonomies: {},
-			page: 1,
-			order: 'DESC',
-			orderby: 'date',
-			... this.config.filters
-		}
-
-		if (elements) {
-			let checks = [this.filters.status, this.filters.order, this.filters.orderby];
-			checks.forEach(check => {
-				let item = this.elements.filters.querySelector(`[data-filter][value="${check}"]`);
-				if (item) {
-					item.checked = true;
-				}
-			});
-
-			this.elements.filters.querySelectorAll('select').forEach(select => {
-				select.value = '';
-			});
-			this.updateClearFiltersButton();
-			this.hasMore = true;
-			this.loadContent(true);
-		}
-	}
-	/********************************************
-	 * EVENTS
-	********************************************/
-	on(elem, event, handler) {
-		if (!this.events.has(elem)) {
-			this.events.set(elem, new Map());
-		}
-		this.events.get(elem).set(event, handler);
-		elem.addEventListener(event, handler);
-	}
-	off(elem, event, handler) {
-		this.events.get(elem).delete(event);
-		elem.removeEventListener(event, handler);
-	}
-	/********************************************
-	 * LOADING
-	********************************************/
-	initLoading() {
-		this.isLoading = false;
-		this.canLoad = true;
-		let overlay = document.querySelector('.loading-overlay');
-		if (!overlay) {
-			this.canLoad = false;
-			return;
-		}
-		this.loading = {
-			overlay: overlay,
-			message: overlay.querySelector('.message'),
-			title: overlay.querySelector('h3'),
-			iconContainer: overlay.querySelector('div.icon'),
-			icon: (this.content !== '') ? this.content : 'logo',
-			quipInterval: null
-		}
-		this.quips = [
-			'Loading',
-			'Hang in there',
-			'Getting things together'
-		];
-	}
-	setLoading(isLoading) {
-		if (!this.canLoad) {
-			return;
-		}
-		this.isLoading = isLoading;
-		isLoading ? this.showLoading() : this.hideLoading();
-	}
-	showLoading(message = null, title = 'Loading') {
-		this.isLoading = true;
-		this.loading.title.textContent = title;
-		if (message) {
-			this.loading.message.textContent = message;
-		}
-
-		document.body.classList.add('loading');
-		document.body.style.overflow = 'hidden';
-		this.startQuips();
-	}
-	hideLoading() {
-		document.body.classList.remove('loading');
-		document.body.style.overflow = '';
-		this.stopQuips();
-		this.isLoading = false;
-	}
-
-	startQuips() {
-		if (this.loading.quipInterval) {
-			clearInterval(this.loading.quipInterval);
-		}
-		let quips = this.shuffleArray(this.quips);
-		let index = 1;
-		let content = quips[0];
-		let lastContent = quips[0];
-		this.loading.message.textContent = content;
-		this.loading.message.classList.remove('changing');
-		this.loading.quipInterval = setInterval(
-			() => {
-				this.loading.message.classList.add('changing');
-
-				setTimeout(() => {
-					index = (index + 1) % quips.length;
-					content = quips[index];
-					this.removeChildren(this.loading.iconContainer);
-					this.loading.iconContainer.append(this.getIcon(this.loading.icon));
-					this.typeLoop(
-						this.loading.message,
-						content
-					);
-					this.loading.message.classList.remove('changing');
-					lastContent = content;
-				});
-			},
-			2000
-		);
-	}
-	stopQuips() {
-		if (this.loading.quipInterval) {
-			clearInterval(this.loading.quipInterval);
-			this.loading.quipInterval = null;
-		}
-	}
-	/********************************************
-	 * TEMPLATES
-	********************************************/
-	loadTemplates() {
-		document.querySelectorAll('template').forEach(template => {
-			const classes = Array.from(template.classList);
-			if (classes.length > 0) {
-				const item = template.content.cloneNode(true).firstElementChild;
-				classes.forEach(key => {
-					if (!this.templates.has(key)) {
-						this.templates.set(key, item);
-					}
-				});
-			}
-		});
-	}
-	getTemplate(template) {
-		if (this.templates.size === 0) {
-			this.loadTemplates();
-		}
-		if (window.templates.has(template)) {
-			return window.templates.get(template).cloneNode(true);
-		}
-		return false;
-	}
-	/********************************************
-	 * ICONS
-	********************************************/
-	getIcon(icon) {
-		console.log('Getting Icon: '+icon);
-		if (typeof icon === 'undefined') {
-			return '';
-		}
-		if (!this.icons.has(icon) && jvbSettings.icons[icon]) {
-			let temp = document.createElement('div');
-			temp.innerHTML = jvbSettings.icons[icon];
-			this.icons.set(icon, temp.firstElementChild.cloneNode(true));
-			temp.remove();
-		}
-		return this.icons.get(icon)?.cloneNode(true);
-	}
-	/********************************************
-	 * UTILITY
-	********************************************/
-	shuffleArray(array) {
-		for (let i = array.length - 1; i > 0; i--) {
-			const j = Math.floor(Math.random() * (i + 1));
-			[array[i], array[j]] = [array[j], array[i]];
-		}
-		return array;
-	}
-	isEmptyObject(obj) {
-		return Object.keys(obj).length === 0;
-	}
-	ucFirst(string) {
-		return string.charAt(0).toUpperCase() + string.slice(1);
-	}
-
-	escapeHtml(text) {
-		if (!text) return '';
-		// Convert to string if it's not already a string
-		if (typeof text !== "string" && !(text instanceof String)) {
-			text = String(text);
-		}
-		return text
-			.replace(/&/g, "&amp;")
-			.replace(/</g, "&lt;")
-			.replace(/>/g, "&gt;")
-			.replace(/"/g, "&quot;")
-			.replace(/'/g, "&#039;");
-	}
-
-	sanitizeHtml(text) {
-		let div = this.getIcon('back');
-		div.textContent = text;
-		return div.innerHTML;
-	}
-	limitText(text, length = 100) {
-		if (!text || text.length <= length) return text;
-		return text.substring(0, length) + '...';
-	}
-
-	removeChildren(node) {
-		if (node.children.length === 0) {
-			return;
-		}
-		while (node.firstChild) {
-			node.removeChild(node.firstChild);
-		}
-	}
-
-	typeText (container, text, speed = 50) {
-		container.classList.add('typeText');
-		return new Promise((resolve) => {
-			let index = 0;
-			container.textContent = '';
-
-			const interval = setInterval(() => {
-				if (index < text.length) {
-					container.textContent += text.charAt(index);
-					index++;
-				} else {
-					clearInterval(interval);
-					resolve();
-				}
-			}, speed);
-		});
-	}
-	eraseText (container, speed = 10) {
-		return new Promise((resolve) => {
-			let text = container.textContent;
-			let index = text.length;
-
-			const interval = setInterval(() => {
-				if (index > 0) {
-					index--;
-					container.textContent = text.substring(0, index);
-				} else {
-					clearInterval(interval);
-					resolve();
-				}
-			}, speed);
-		});
-	}
-	typeLoop(container, text, typeSpeed = 50, eraseSpeed = 10) {
-		let isRunning = true;
-
-		async function loop() {
-			while (isRunning) {
-				// Type the text
-				await window.typeText(container, text, typeSpeed);
-
-				// Wait 1 second
-				await new Promise(resolve => setTimeout(resolve, pauseAfterType));
-
-				// Erase the text
-				await window.eraseText(container, eraseSpeed);
-
-				// Wait 0.25 seconds before next iteration
-				await new Promise(resolve => setTimeout(resolve, pauseAfterErase));
-			}
-		}
-
-		// Start the loop
-		loop();
-
-		// Return a function to stop the loop
-		return function stopLoop() {
-			isRunning = false;
-		};
-	}
-
-	targetCheck(e, selector) {
-		if (typeof selector !== 'string') {
-			return false;
-		}
-		return e.target.closest(selector)??false;
-	}
-	/********************************************
-	 * REQUESTS
-	********************************************/
-	async getRequest(endpoint, params, headers = {}, reset = false, force = false) {
-		if (this.isLoading || !this.hasMore) return;
-
-		try {
-			this.setLoading(true);
-			if (reset) {
-				this.filters.page = 1;
-				this.clearContent();
-			}
-
-			const filters = this.buildFilters();
-			const data = await this.cache.fetchWithCache(
-				`${jvbSettings.api}${endpoint}?${filters.toString()}`,
-				{
-					method: 'GET',
-					headers: {
-						'X-WP-Nonce': jvbSettings.nonce,
-						... headers
-					}
-				},
-				{
-					context: this.content,
-					forceRefresh: force
-				}
-			);
-		}
-	}
-
-	//Overridden by child classes
-	buildParams() {
-		return '';
-	}
-
-	setRequest() {
-
-	}
-	/********************************************
-	 * DEBOUNCER
-	********************************************/
-	schedule(key, callback, delay = 1000) {
-		this.cancel(key);
-		this.timeouts.set(key, setTimeout(
-			() => {
-				callback();
-				this.timeouts.delete(key);
-			}, delay
-		));
-	}
-	cancel(key) {
-		if (this.timeouts.has(key)) {
-			clearTimeout(this.timeouts.get(key));
-			this.timeouts.delete(key);
-		}
-	}
-
-	/*******************************************
-	 * CLEANUP
-	*******************************************/
-	cleanup() {
-		for (let [elem, value] of this.events) {
-			for (let [event, handler] of value) {
-				elem.removeEventListener(event, handler);
-			}
-		}
-
-		for (let timeout of this.timeouts.values()) {
-			clearTimeout(timeout);
-		}
-		this.timeouts.clear();
-	}
-}
-
-window.JVB = new JVB();
diff --git a/assets/js/concise/Loader.js b/assets/js/concise/Loader.js
deleted file mode 100644
index e69de29..0000000
--- a/assets/js/concise/Loader.js
+++ /dev/null
diff --git a/assets/js/concise/Media.js b/assets/js/concise/Media.js
deleted file mode 100644
index a9e1340..0000000
--- a/assets/js/concise/Media.js
+++ /dev/null
@@ -1,98 +0,0 @@
-class Media {
-	constructor() {
-		this.currentWidth = window.innerWidth;
-		this.images = document.querySelectorAll('.wp-site-blocks img[data-small]');
-
-		if (this.images.length === 0) return;
-
-		// Immediately load visible images
-		this.loadVisibleImages();
-
-		this.initListeners();
-	}
-
-	loadVisibleImages() {
-		// Load first image immediately, plus any in viewport
-		this.images.forEach((img, index) => {
-			const rect = img.getBoundingClientRect();
-			const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
-
-			// Always load first image, or if currently visible
-			if (index === 0 || isVisible) {
-				this.loadAppropriateImage(img);
-				img.dataset.loaded = 'true'; // Mark so we don't observe it
-			}
-		});
-	}
-
-	initListeners() {
-		this.resizeHandler = this.handleResize.bind(this);
-		window.addEventListener('resize', this.resizeHandler);
-
-		// Only observe images that weren't immediately loaded
-		this.observer = new IntersectionObserver((entries) => {
-			entries.forEach(entry => {
-				if (entry.isIntersecting) {
-					this.loadAppropriateImage(entry.target);
-					this.observer.unobserve(entry.target);
-				}
-			});
-		}, {
-			rootMargin: '50px',
-			threshold: 0.1
-		});
-
-		this.images.forEach(img => {
-			if (!img.dataset.loaded) {
-				this.observer.observe(img);
-			}
-		});
-	}
-
-	handleResize() {
-		window.debouncer.schedule('image-resize', () => {
-			const newWidth = window.innerWidth;
-			if (Math.abs(newWidth - this.currentWidth) > 100) {
-				this.currentWidth = newWidth;
-				this.updateVisibleImages();
-			}
-		}, 150);
-	}
-
-	updateVisibleImages() {
-		this.images.forEach(img => {
-			const rect = img.getBoundingClientRect();
-			if (rect.top < window.innerHeight && rect.bottom > 0) {
-				this.loadAppropriateImage(img, true);
-			}
-		});
-	}
-
-	loadAppropriateImage(img, forceUpdate = false) {
-		const targetSize = this.getTargetSize();
-		const newSrc = img.dataset[targetSize];
-
-		if (newSrc && (forceUpdate || newSrc !== img.currentSrc)) {
-			img.src = newSrc;
-		}
-	}
-
-	getTargetSize() {
-		if (this.currentWidth < 768) return 'medium';
-		if (this.currentWidth < 1200) return 'large';
-		return 'full';
-	}
-
-	cleanup() {
-		this.observer?.disconnect();
-		window.removeEventListener('resize', this.resizeHandler);
-	}
-}
-
-window.isLoaded = false;
-document.addEventListener('readystatechange', () => {
-	if (!window.isLoaded && document.querySelector('.wp-site-blocks img')) {
-		window.jvbMedia = new Media();
-		window.isLoaded = true;
-	}
-});
diff --git a/assets/js/dash/Modal.js b/assets/js/concise/Modal.js
similarity index 100%
rename from assets/js/dash/Modal.js
rename to assets/js/concise/Modal.js
diff --git a/assets/js/dash/NewsManager.js b/assets/js/concise/NewsManager.js
similarity index 97%
rename from assets/js/dash/NewsManager.js
rename to assets/js/concise/NewsManager.js
index a4e03de..ae6e1e4 100644
--- a/assets/js/dash/NewsManager.js
+++ b/assets/js/concise/NewsManager.js
@@ -17,7 +17,7 @@
                 console.log('switching to mine tab');
                 this.activeTab = 'own';
                 this.resetFilters();
-                this.filters.artist = jvbSettings.currentUser;
+                this.filters.artist = window.auth.getUser();
                 this.loadItems(true).then(()=>{});
             },
             'watching': () => {
@@ -181,7 +181,7 @@
     async saveModal(form){
         const formData = new FormData(this.addModal.modal.querySelector('form'));
 
-        formData.append('user', jvbSettings.currentUser);
+        formData.append('user', window.auth.getUser());
         this.queue.addToQueue({
             type: 'new_news',
             data: formData,
@@ -212,8 +212,8 @@
                 {
                     method: 'GET',
                     headers: {
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.dash,
+                        'X-WP-Nonce': window.auth.getNonce(),
+                        'action_nonce': window.auth.getNonce('dash'),
                     }
                 },{
                     context: 'news',
@@ -447,7 +447,7 @@
         const modal = this.replyModal.modal;
 
         let data = {
-            user: jvbSettings.currentUser,
+            user: window.auth.getUser(),
             item_id: modal.dataset.id,
             response: modal.querySelector('.ql-editor').innerHTML,
             content: modal.dataset.type,
diff --git a/assets/js/dash/NotificationManager.js b/assets/js/concise/NotificationManager.js
similarity index 97%
rename from assets/js/dash/NotificationManager.js
rename to assets/js/concise/NotificationManager.js
index 2d3ebcc..c13c775 100644
--- a/assets/js/dash/NotificationManager.js
+++ b/assets/js/concise/NotificationManager.js
@@ -86,7 +86,7 @@
 				{
 					method: 'GET',
 					headers: {
-						'X-WP-Nonce': jvbSettings.nonce,
+						'X-WP-Nonce': window.auth.getNonce(),
 						'action_nonce': jvbAdmin.nonce,
 					}
 				},{
@@ -136,7 +136,7 @@
 			}
 		}
 		temp.context = 'admin';
-		temp.user = jvbSettings.currentUser;
+		temp.user = window.auth.getUser();
 
 		return new URLSearchParams(temp);
 	}
diff --git a/assets/js/Notifications.js b/assets/js/concise/Notifications.js
similarity index 93%
rename from assets/js/Notifications.js
rename to assets/js/concise/Notifications.js
index 77fbd23..66bac6f 100644
--- a/assets/js/Notifications.js
+++ b/assets/js/concise/Notifications.js
@@ -82,7 +82,7 @@
             this.isLoading = true;
 
             const params = new URLSearchParams({
-                user: jvbSettings.currentUser,
+                user: window.auth.getUser(),
                 status: 'unread',
                 limit: 5,
             });
@@ -92,8 +92,8 @@
                 {
                     method: 'GET',
                     headers: {
-                    'X-WP-Nonce': jvbSettings.nonce,
-                    'action_nonce': jvbSettings.notifications
+                    'X-WP-Nonce': window.auth.getNonce(),
+                    'action_nonce': window.auth.getNonce('notifications')
                     }
                 }, {
                     context: 'notifications',
@@ -101,8 +101,6 @@
                 }
             );
 
-            console.log(data);
-
             this.renderPreviewNotifications(data.notifications);
             this.updateUnreadCount(data.total);
             this.notificationsLoaded = true;
@@ -279,12 +277,12 @@
                 `${jvbSettings.api}notifications`, {
                     method: 'POST',
                     headers: {
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.dash,
+                        'X-WP-Nonce': window.auth.getNonce(),
+                        'action_nonce': window.auth.getNonce('dash'),
                     },
                     body: {
                         notification: notificationId,
-                        user: jvbSettings.currentUser,
+                        user: window.auth.getUser(),
                     }
                 }
             );
@@ -335,13 +333,13 @@
     async checkNotifications() {
         try {
             const params = new URLSearchParams({
-                user: jvbSettings.currentUser,
+                user: window.auth.getUser(),
                 status: 'unread',
             });
             const response = await fetch(`${jvbSettings.api}notifications?${params.toString()}`, {
                 headers: {
-                    'X-WP-Nonce': jvbSettings.nonce,
-                    'action_nonce': jvbSettings.dash,
+                    'X-WP-Nonce': window.auth.getNonce(),
+                    'action_nonce': window.auth.getNonce('dash'),
                     'If-Modified-Since': this.lastCheck,
                 }
             });
@@ -366,12 +364,16 @@
 }
 
 // Initialize when DOM is ready
-document.addEventListener('DOMContentLoaded', () => {
-    window.jvbNotifications = new NotificationManager({
-        position: 'bottom-right',
-        maxVisibleNotifications: 5,
-        displayDuration: 5000
-    });
+document.addEventListener('DOMContentLoaded', async function(){
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbNotifications = new NotificationManager({
+				position: 'bottom-right',
+				maxVisibleNotifications: 5,
+				displayDuration: 5000
+			});
+		}
+	});
 });
 
 function handleNotificationAction(button) {
diff --git a/assets/js/dash/PostSelector.js b/assets/js/concise/PostSelector.js
similarity index 99%
rename from assets/js/dash/PostSelector.js
rename to assets/js/concise/PostSelector.js
index 3c07850..232abc7 100644
--- a/assets/js/dash/PostSelector.js
+++ b/assets/js/concise/PostSelector.js
@@ -56,7 +56,7 @@
 // 				method: 'GET',
 // 				headers: {
 // 					'Content-Type': 'application/json',
-// 					'X-WP-Nonce': jvbSettings.nonce
+// 					'X-WP-Nonce': window.auth.getNonce()
 // 				}
 // 			}, {
 // 				content: `posts_${this.selector.currentConfig.postType}`,
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 297885d..4590141 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -14,11 +14,35 @@
 			endpoint: 'queue',
 			...config
 		};
-		this.user = jvbSettings.currentUser;
 
+		// Queue state
+		this.isProcessing = false;
+		this.isPolling = false;
+		this.subscribers = new Set();
+
+		// Status definitions
+		this.statuses = [
+			'queued',
+			'localProcessing',
+			'uploading',
+			'pending',
+			'processing',
+			'completed',
+			'failed',
+			'failed_permanent'
+		];
+
+		this.user = window.auth.getUser();
+
+		if (!this.user) {
+			console.log('Queue: User not logged in, queue disabled');
+			this.store = null;
+			this.canUpdateUI = false;
+			return;
+		}
 
 		this.headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
+			'X-WP-Nonce': window.auth.getNonce(),
 			...config.headers
 		};
 
@@ -46,22 +70,7 @@
 			'pending'
 		];
 
-		// Queue state
-		this.isProcessing = false;
-		this.isPolling = false;
-		this.subscribers = new Set();
 
-		// Status definitions
-		this.statuses = [
-			'queued',
-			'localProcessing',
-			'uploading',
-			'pending',
-			'processing',
-			'completed',
-			'failed',
-			'failed_permanent'
-		];
 
 		// Initialize
 		this.initUI();
@@ -73,13 +82,8 @@
 				name: 'Queue Panel',
 			});
 		}
-
+		this.updateUI = () => window.debouncer.schedule('queue-ui-update', this._updateUI.bind(this), 100);
 		this.initQueue();
-
-		if (this.user) {
-			this.ui.toggle.hidden = false;
-			this.ui.panel.hidden = false;
-		}
 	}
 
 	async initQueue() {
@@ -214,15 +218,16 @@
 
 	}
 
+
 	setQueue(item) {
-		this.store.save(item);  // Remove first parameter
+		this.store.save(item);
 	}
 
 	updateOperationStatus(itemID, status) {
 		let item = this.store.get(itemID);
-		if (!item){
-			return;
-		}
+		if (!item) return;
+
+		// Update status
 		item.status = status;
 
 		this.notify('operation-status', item);
@@ -234,6 +239,7 @@
 	}
 
 	clearQueue(itemID) {
+		const item = this.store.get(itemID);
 		this.store.delete(itemID);
 	}
 
@@ -274,6 +280,15 @@
 		}
 	}
 
+	hideQueue(){
+		this.ui.panel.hidden = true;
+		this.ui.toggle.hidden = true;
+	}
+	showQueue() {
+		this.ui.panel.hidden = false;
+		this.ui.toggle.hidden = false;
+	}
+
 	setProcessing(on) {
 		this.isProcessing = on;
 		this.ui.toggle.classList.toggle('saving', on);
@@ -303,6 +318,9 @@
 		const pending = this.getOperationsByStatus(['queued', 'completed', 'failed_permanent'], false);
 		if (pending.length > 0) {
 			this.startPolling();
+			this.showQueue();
+		} else {
+			this.hideQueue();
 		}
 	}
 
@@ -348,7 +366,7 @@
 					if (existingOp) {
 						// Merge data from both operations
 						existingOp.data = window.deepMerge(existingOp.data, operation.data);
-						existingOp.status = 'pending';
+						existingOp.status = result.status || 'pending';
 						existingOp.serverData = result;
 						this.updateOperationStatus(existingOp.id, existingOp.status);
 						// Update the existing operation
@@ -363,16 +381,16 @@
 						// Update the ID and continue
 						this.clearQueue(operation.id);
 						operation.id = result.id;
-						operation.status = 'pending';
+						operation.status = result.status || 'pending';
 						operation.serverData = result;
 						this.updateOperationStatus(operation.id, operation.status);
 						this.setQueue(operation);
 					}
 				} else {
 					// Normal processing - no merge
-					operation.status = 'pending';
+					operation.status = result.status || 'pending';
 					operation.serverData = result;
-					this.updateOperationStatus(operation.id, 'pending');
+					this.updateOperationStatus(operation.id, operation.status);
 					this.setQueue(operation);
 				}
 
@@ -445,41 +463,29 @@
 	 * @returns {Promise<void>}
 	 */
 	async updateServerOperations(ids, action) {
-		//ensure ids are in an array
-		ids = Array.isArray(ids) ? ids : ((ids.includes(',')) ? ids.split(',') : [ids]);
-		ids = ids.filter((id) => {
+		ids = Array.isArray(ids) ? ids : (ids.includes(',') ? ids.split(',') : [ids]);
+		ids = ids.filter(id => {
 			let item = this.getQueue(id);
 			return this.getAllowedActions(item.status).includes(action);
 		});
 
-		if (ids.length === 0) {
-			return;
-		}
+		if (ids.length === 0) return;
 
-		if (['cancel', 'dismiss'].includes(action)) {
-			ids.forEach(id => {
-				this.removeOperationFromUI(id);
-			});
+		// SINGLE place to handle UI removal
+		const shouldRemove = ['cancel', 'dismiss'].includes(action);
+		if (shouldRemove) {
+			ids.forEach(id => this.removeOperationFromUI(id));
 		}
 
 		try {
-			const url = `${this.config.apiBase}${this.config.endpoint}`;
-
-			const response = await fetch(
-				url,
-				{
-					method: 'POST',
-					headers: {
-						'Content-Type': 'application/json',
-						...this.headers
-					},
-					body: JSON.stringify({ids,action, user: jvbSettings.currentUser})
-				}
-			);
+			const response = await fetch(`${this.config.apiBase}${this.config.endpoint}`, {
+				method: 'POST',
+				headers: { 'Content-Type': 'application/json', ...this.headers },
+				body: JSON.stringify({ ids, action, user: window.auth.getUser() })
+			});
 
 			if (!response.ok) {
-				const errorData = await response.json().catch(()=>{});
-				throw new Error(errorData.message || `${action} failed: ${response.status}`);
+				throw new Error(`${action} failed: ${response.status}`);
 			}
 
 			const result = await response.json();
@@ -487,41 +493,40 @@
 				throw new Error(result.message || `${action} operation failed`);
 			}
 
-			if (['cancel', 'dismiss'].includes(action)) {
-				ids.forEach(id => {
-					let item = this.getQueue(id);
-					this.notify(`${action}-operation`, item);
-					this.clearQueue(id);
-				});
-			} else {
-				ids.forEach(id => {
-					let item = this.getQueue(id);
-					this.notify(`${action}-operation`, item);
+			// SINGLE place to handle store updates
+			ids.forEach(id => {
+				let item = this.getQueue(id);
+				this.notify(`${action}-operation`, item);
 
+				if (shouldRemove) {
+					this.clearQueue(id);
+				} else {
 					item.status = 'queued';
 					item.retries = 0;
 					this.setQueue(item);
 					this.updateOperationStatus(item.id, item.status);
-				});
+				}
+			});
+
+			if (action === 'retry') {
 				this.startActivityTracking();
 			}
-			this.updateUI();
 
+			this.updateUI();
 			return result;
+
 		} catch (error) {
-			const result = await window.jvbError.log(error, {
+			// Log and let jvbError handle retry
+			await window.jvbError.log(error, {
 				component: 'QueueManager',
 				operation: 'performQueueAction',
 				action: action,
 				operationIds: ids,
 				itemCount: ids.length
-			}, () => this.updateServerOperations(ids, action)); // Retry callback
+			}, () => this.updateServerOperations(ids, action));
 
-			if (result.retried) {
-				return result; // Return successful retry result
-			} else {
-				throw error; // Re-throw if not retried
-			}
+			// Don't re-throw - error is logged and handled
+			return { success: false, error: error.message };
 		}
 	}
 
@@ -544,10 +549,8 @@
 	*********************************************/
 	initListeners() {
 		this.clickHandler = this.handleClick.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
 
 		document.addEventListener('click', this.clickHandler);
-		this.ui.panel?.addEventListener('change', this.changeHandler);
 
 		this.handleOnline = () => {
 			this.updateStatusPanel();
@@ -601,9 +604,6 @@
 
 	}
 
-	handleChange(e) {
-	}
-
 	/*********************************************
 	UI
 	 *********************************************/
@@ -637,33 +637,13 @@
 			}
 		};
 
-		this.ui = {
-			panel: document.querySelector(this.selectors.panel),
-			toggle: document.querySelector(this.selectors.toggle),
-			count: document.querySelector(this.selectors.count),
-			indicator: document.querySelector(this.selectors.indicator),
-		};
+		this.ui = window.uiFromSelectors(this.selectors);
 		if (!this.ui.panel) {
 			this.canUpdateUI = false;
-			return;
-		}
-
-		for (let [key, selector] of Object.entries(this.selectors)) {
-			if (['panel', 'toggle', 'count', 'indicator'].includes(key)) {
-				continue;
-			}
-			if (typeof selector === 'object') {
-				this.ui[key] = {};
-				for (let [k, s] of Object.entries(selector)) {
-					this.ui[key][k] = this.ui.panel.querySelector(s);
-				}
-			}else {
-				this.ui[key] = this.ui.panel.querySelector(selector);
-			}
 		}
 	}
 
-	updateUI() {
+	_updateUI() {
 		if (!this.canUpdateUI) {
 			return;
 		}
@@ -905,25 +885,6 @@
 		}
 	}
 
-	updateCountdown() {
-		if (!this.ui.countdown || !this.isPolling) return;
-
-		let seconds = this.config.pollInterval / 1000;
-
-		this.countdownTimer = setInterval(() => {
-			seconds--;
-
-			this.ui.countdown.textContent = seconds;
-
-			if (seconds <= 0) {
-				clearInterval(this.countdownTimer);
-				if (this.isPolling) {
-					setTimeout(() => this.updateCountdown(), 100);
-				}
-			}
-		}, 1000);
-	}
-
 	updateStatusPanel(status) {
 		this.ui.panel?.classList.remove(...this.classes);
 		if (!this.classes.includes(status)) {
@@ -951,23 +912,6 @@
 	}
 
 	/**************************************************************************
-	 NOTIFICATIONS
-	**************************************************************************/
-	showPopup(message, type = 'success') {
-		if (!this.ui.popup) return;
-
-		const span = this.ui.popup.querySelector('span');
-		if (span) {
-			span.textContent = message;
-		}
-
-		this.ui.popup.className = `popup ${type} show`;
-
-		setTimeout(() => {
-			this.ui.popup.classList.remove('show');
-		}, 3000);
-	}
-	/**************************************************************************
 	 HELPERS
 	**************************************************************************/
 	getOperationsByStatus(status, include = true) {
@@ -983,6 +927,9 @@
 		return this.getOperationsByStatus('queued').length > 0;
 	}
 	subscribe(callback) {
+		if (!this.subscribers) {
+			return;
+		}
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
 	}
@@ -1010,6 +957,10 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', function() {
-	window.jvbQueue = new QueueManager();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbQueue = new QueueManager();
+		}
+	});
 });
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index 9ec7702..d3d1a4b 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -5,7 +5,7 @@
 
 class Referral {
 	constructor() {
-		this.container = document.querySelector('.jvb-referral');
+		this.container = document.querySelector('aside.referral');
 		if (!this.container) {
 			return;
 		}
@@ -13,15 +13,12 @@
 		this.a11y = window.jvbA11y;
 		this.toggle = document.querySelector('button[data-action="toggle-referral"]');
 
+		this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
 		this.initElements();
+		this.storesInited = false;
+		this.initStore();
 		this.initListeners();
 		this.checkForReferral();
-
-		// Load additional data for logged-in users
-		if (this.isLoggedIn()) {
-			this.loadStats();
-			this.loadRecentReferrals();
-		}
 	}
 
 	initElements() {
@@ -29,6 +26,17 @@
 			copyBtn: '.copy-btn',
 			checkCode: '.check-code-btn',
 			submit: '[type=submit]',
+			recentList: '.recent-referrals-list',
+			invite: 'form.invite',
+			adminList: '.items-list.referral',
+			dash: '.replace .referral-dashboard',
+			stats: {
+				codeUsed: '[data-stat="code_used"]',
+				consultations: '[data-stat="consultations"]',
+				treatments: '[data-stat="treatments"]',
+				rewards: '[data-stat="total_rewards"]'
+			},
+			list: '.referrals-list'
 		};
 
 		this.forms = this.container.querySelectorAll('form');
@@ -45,11 +53,120 @@
 		});
 
 		this.tabs = null;
+
 		if (this.container.querySelector('nav.tabs')) {
 			this.tabs = new window.jvbTabs(this.container, {updateURL: false});
 		}
 
-		this.ui = window.uiFromSelectors(this.selectors, this.container);
+
+		this.ui = window.uiFromSelectors(this.selectors);
+
+		this.dashTabs = null;
+		if (this.ui.dash) {
+			this.dashTabs = new window.jvbTabs(this.ui.dash);
+		}
+
+		if (!this.hasCopy) {
+			document.querySelectorAll(this.selectors.copyBtn).forEach(btn => {
+				btn.remove();
+			});
+		}
+		this.formController = null;
+
+		if (this.ui.invite) {
+			this.formController = new window.jvbForm();
+			this.formController.registerForm(
+				this.ui.invite,
+				{
+					autosave: true,
+					endpoint: 'referrals',
+					formStatus: false,
+				}
+			);
+
+			this.formController.subscribe((event, data) => {
+				if (event === 'form-submit') {
+					data = data.fullData;
+					data.action = 'invite';
+					window.jvbQueue.addToQueue(
+						{
+							endpoint: 'referrals',
+							data: data,
+							title: 'Submitting invitations',
+						}
+					);
+				}
+			});
+		}
+	}
+
+	initStore() {
+		if (!this.isLoggedIn()) return;
+
+		const stores = window.jvbStore.register(
+			'referrals',
+			[
+				// Dashboard stats store
+				{
+					storeName: 'stats',
+					keyPath: 'user_id',
+					endpoint: 'referrals/stats',
+					TTL: 5 * 60 * 1000,
+					showLoading: false,
+					delayFetch: false,
+					filters: {
+						type: 'dashboard',
+						user: window.auth.getUser()
+					}
+				},
+				// Referrals list store
+				{
+					storeName: 'list',
+					keyPath: 'id',
+					endpoint: 'referrals',
+					TTL: 10 * 60 * 1000,
+					showLoading: false,
+					delayFetch: false,
+					filters: {
+						user: window.auth.getUser(),
+						status: 'all',
+						limit: 50,
+						offset: 0
+					}
+				}
+			]
+		);
+
+		this.statsStore = stores.stats;
+		this.listStore = stores.list;
+
+		// Subscribe to store events
+		if (this.statsStore) {
+			this.statsStore.subscribe(this.handleStatsEvent.bind(this));
+		}
+		if (this.listStore) {
+			this.listStore.subscribe(this.handleListEvent.bind(this));
+		}
+
+		if (this.ui.dash) {
+			this.initViewController();
+		}
+	}
+
+	initViewController() {
+		if (!this.listStore || !this.ui.adminList) return;
+
+		this.view = new window.jvbViews(this.ui.adminList, this.listStore);
+		this.view.subscribe((event, data) => {
+			switch(event) {
+				case 'item-action':
+					this.handleItemAction(data);
+					break;
+				case 'bulk-action':
+					this.handleBulkAction(data);
+					break;
+			}
+		});
 	}
 
 	initListeners() {
@@ -70,9 +187,151 @@
 	}
 
 	isLoggedIn() {
-		return Boolean(jvbSettings.currentUser);
+		return Boolean(window.auth.getUser());
 	}
 
+	/**
+	 * Handle DataStore stats events
+	 */
+	handleStatsEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				if (data.items && data.items.length > 0) {
+					this.updateStatsDisplay();
+				}
+				break;
+			case 'fetch-error':
+				console.error('Error loading stats:', data.error);
+				break;
+		}
+	}
+
+	/**
+	 * Handle DataStore list events
+	 */
+	handleListEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				// Let ViewController handle main list rendering
+				// Only update sidebar preview if it exists
+				if (this.ui.recentList) {
+					this.renderRecentReferrals();
+				}
+				break;
+			case 'fetch-error':
+				console.error('Error loading referrals:', data.error);
+				break;
+		}
+	}
+
+	/**
+	 * Update stats display
+	 */
+	updateStatsDisplay() {
+		if (!this.statsStore.data.size === 0) return;
+		let stats = this.statsStore.data.get(parseInt(window.auth.getUser()));
+		const updates = {
+			total: stats['code_used'] || 0,
+			treated: stats.treatments || 0,
+			pending: stats.pending || 0,
+			rewards: '$' + parseFloat(stats['total_rewards'] || 0).toFixed(2)
+		};
+
+		Object.entries(updates).forEach(([key, value]) => {
+			const element = this.container.querySelector(`[data-stat="${key}"]`);
+			if (element) {
+				element.textContent = value;
+			}
+		});
+
+		// Also update stat cards if on dashboard
+		const statCards = this.container.querySelectorAll('.stats .card');
+		if (statCards.length >= 4) {
+			statCards[0].querySelector('.stat-number').textContent = updates.code_used;
+			statCards[1].querySelector('.stat-number').textContent = updates.consultations;
+			statCards[2].querySelector('.stat-number').textContent = updates.treatments;
+			statCards[3].querySelector('.stat-number').textContent = updates.total_rewards;
+		}
+	}
+
+	/**
+	 * Handle item actions (remove, resend)
+	 */
+	handleItemAction(data) {
+		const { action, itemId } = data;
+
+		switch(action) {
+			case 'remove':
+				this.removeReferral(itemId);
+				break;
+			case 'resend':
+				this.resendInvite(itemId);
+				break;
+		}
+	}
+
+	/**
+	 * Remove referral from list
+	 */
+	async removeReferral(id) {
+		if (!confirm('Remove this referral from your list?')) return;
+
+		try {
+			const response = await fetch(`${jvbSettings.api}referrals`, {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+					'X-WP-Nonce': window.auth.getNonce()
+				},
+				body: JSON.stringify({
+					action: 'remove',
+					referral_id: id
+				})
+			});
+
+			const result = await response.json();
+
+			if (result.success) {
+				// Refresh DataStore
+				if (this.listStore) this.listStore.fetch();
+				if (this.statsStore) this.statsStore.fetch();
+				this.a11y?.announce('Referral removed');
+			}
+		} catch (error) {
+			console.error('Error removing referral:', error);
+		}
+	}
+
+	/**
+	 * Resend invite email
+	 */
+	async resendInvite(id) {
+		try {
+			const response = await fetch(`${jvbSettings.api}referrals`, {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+					'X-WP-Nonce': window.auth.getNonce()
+				},
+				body: JSON.stringify({
+					action: 'resend',
+					referral_id: id
+				})
+			});
+
+			const result = await response.json();
+
+			if (result.success) {
+				this.a11y?.announce('Invitation resent');
+			} else {
+				alert(result.message || 'Cannot resend yet. Wait 7 days between invites.');
+			}
+		} catch (error) {
+			console.error('Error resending invite:', error);
+		}
+	}
+
+
 	handleClick(e) {
 		const target = e.target.closest('.copy-btn, .check-code-btn, .attn');
 		if (!target) return;
@@ -98,7 +357,7 @@
 		const text = codeElement.textContent.trim();
 
 		// Try clipboard API first
-		if (navigator.clipboard && navigator.clipboard.writeText) {
+		if (this.hasCopy) {
 			navigator.clipboard.writeText(text).then(() => {
 				this.showCopySuccess(button);
 			}).catch(() => {
@@ -106,10 +365,6 @@
 				this.selectText(codeElement);
 				this.showCopyFallback(button);
 			});
-		} else {
-			// Fallback to selection
-			this.selectText(codeElement);
-			this.showCopyFallback(button);
 		}
 	}
 
@@ -226,15 +481,19 @@
 	 * Check for ?ref parameter in URL and pre-fill code
 	 */
 	async checkForReferral() {
-		const isLoggedIn = this.getUrlParameter('seeReferral');
 		const refCode = this.getUrlParameter('ref');
+		const refName = this.getUrlParameter('rname');
+		const refEmail = this.getUrlParameter('remail');
+		const seeReferral = this.getUrlParameter('seeReferral');
 
-		if (!isLoggedIn && !refCode) {
+		if (!refCode && !seeReferral) {
 			return;
 		}
 
-		if (!refCode) {
+		// If logged in user just wants to see referral popup
+		if (seeReferral && !refCode) {
 			this.popup.openPopup();
+			this.removeUrlParameter('seeReferral');
 			return;
 		}
 
@@ -248,7 +507,21 @@
 		codeInput.value = code;
 		codeInput.readOnly = true;
 
-		this.popup.togglePopup();
+		// If we have token data, prefill name and email too
+		if (refName || refEmail) {
+			const nameInput = this.container.querySelector('[name="referral_name"]');
+			if (nameInput) {
+				nameInput.value = refName;
+			}
+
+			const emailInput = this.container.querySelector('[name="referral_email"]');
+			if (emailInput) {
+				emailInput.value = refEmail;
+			}
+		}
+
+		// Open the sidebar popup
+		this.popup.openPopup();
 
 		// Validate the code immediately
 		try {
@@ -264,9 +537,9 @@
 					);
 				}
 
-				// Focus on name input
+				// Focus on name input if not prefilled
 				const nameInput = this.container.querySelector('[name="referral_name"]');
-				if (nameInput) {
+				if (nameInput && !nameInput.value) {
 					nameInput.focus();
 				}
 			} else {
@@ -280,6 +553,8 @@
 
 		// Clean up URL
 		this.removeUrlParameter('ref');
+		this.removeUrlParameter('rname');
+		this.removeUrlParameter('remail');
 	}
 
 	getUrlParameter(name) {
@@ -297,11 +572,11 @@
 	 * Validate code without registering
 	 */
 	async validateCodeOnly(code) {
-		const response = await fetch(`${jvbSettings.api}referrals/check-code`, {
+		const response = await fetch(`${jvbSettings.api}referrals/code`, {
 			method: 'POST',
 			headers: {
 				'Content-Type': 'application/json',
-				'X-WP-Nonce': jvbSettings.nonce
+				'X-WP-Nonce': window.auth.getNonce()
 			},
 			body: JSON.stringify({ code: code })
 		});
@@ -317,8 +592,8 @@
 		if (!statsContainer) return;
 
 		try {
-			const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${jvbSettings.currentUser}`, {
-				headers: { 'X-WP-Nonce': jvbSettings.nonce }
+			const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${window.auth.getUser()}`, {
+				headers: { 'X-WP-Nonce': window.auth.getNonce() }
 			});
 
 			const data = await response.json();
@@ -330,6 +605,23 @@
 		}
 	}
 
+	async loadSidebarStats() {
+		try {
+			const response = await fetch(
+				`${jvbSettings.api}referrals/stats?user=${window.auth.getUser()}&type=quick`,
+				{ headers: { 'X-WP-Nonce': window.auth.getNonce() } }
+			);
+
+			const data = await response.json();
+			if (data.success && data.stats) {
+				this.updateSidebarStats(data.stats);
+			}
+		} catch (error) {
+			console.error('Error loading sidebar stats:', error);
+		}
+	}
+
+
 	/**
 	 * Update stats display
 	 */
@@ -350,49 +642,25 @@
 	}
 
 	/**
-	 * Load recent referrals (last 5)
-	 */
-	async loadRecentReferrals() {
-		const container = this.container.querySelector('.recent-referrals-list');
-		if (!container) return;
-
-		try {
-			const response = await fetch(`${jvbSettings.api}referrals/my-referrals?limit=5&user=${jvbSettings.currentUser}`, {
-				headers: { 'X-WP-Nonce': jvbSettings.nonce }
-			});
-
-			const data = await response.json();
-			if (data.success && data.referrals) {
-				this.renderRecentReferrals(container, data.referrals);
-			} else {
-				container.innerHTML = '<p class="no-referrals">No referrals yet</p>';
-			}
-		} catch (error) {
-			console.error('Error loading referrals:', error);
-			container.innerHTML = '<p class="error">Failed to load referrals</p>';
-		}
-	}
-
-	/**
 	 * Render recent referrals list
 	 */
-	renderRecentReferrals(container, referrals) {
+	renderRecentReferrals() {
+		let container = this.ui.recentList;
+		let referrals = Array.from(this.listStore.data.values());
 		if (!referrals || referrals.length === 0) {
 			container.innerHTML = '<p class="no-referrals">Share your code to get started!</p>';
 			return;
 		}
 
-		const html = referrals.map(ref => `
+		container.innerHTML = referrals.map(ref => `
 			<div class="referral-item">
 				<div class="referral-info">
 					<strong>${window.escapeHtml(ref.referee_name)}</strong>
-					<span class="status-badge ${ref.status}">${ref.status}</span>
+					<span class="status-badge">${ref.referral_status}</span>
 				</div>
-				<div class="referral-date">${this.formatDate(ref.referred_at)}</div>
+				<div class="referral-date">${window.formatTimeAgo(ref.referred_at)}</div>
 			</div>
 		`).join('');
-
-		container.innerHTML = html;
 	}
 
 	/**
@@ -420,23 +688,23 @@
 		const form = event.target;
 		const formData = new FormData(form);
 
-		// Disable form
 		this.setFormLoading(true, form);
 
 		try {
 			let result = { success: false, message: '' };
 
 			if (form.id === 'referral-code-form') {
+				// Registration with referral code - goes to LoginRoutes
 				const data = {
 					name: formData.get('referral_name'),
 					email: formData.get('referral_email'),
-					code: formData.get('referral_code')
+					referral_code: formData.get('referral_code')
 				};
 
-				if (!data.name || !data.email || !data.code) {
+				if (!data.name || !data.email || !data.referral_code) {
 					result.message = 'Please fill in all fields';
 				} else {
-					result = await this.makeRequest('referrals/register', data);
+					result = await this.makeRequest('auth/register', data); // UPDATED endpoint
 				}
 			} else if (form.id === 'login-form') {
 				const data = {
@@ -465,8 +733,7 @@
 	async makeRequest(endpoint, data) {
 		const validEndpoints = [
 			'magic',
-			'referrals/register',
-			'referrals/check-code'
+			'auth/register'
 		];
 
 		if (!validEndpoints.includes(endpoint)) {
@@ -477,11 +744,22 @@
 			method: 'POST',
 			headers: {
 				'Content-Type': 'application/json',
-				'X-WP-Nonce': jvbSettings.nonce,
+				'X-WP-Nonce': window.auth.getNonce(),
 			},
 			body: JSON.stringify(data)
 		});
 
+		// Add error handling to see the actual response
+		if (!response.ok) {
+			const errorText = await response.text();
+			console.error('Error response:', response.status, errorText);
+			try {
+				return JSON.parse(errorText);
+			} catch {
+				return { success: false, message: 'Server error' };
+			}
+		}
+
 		return await response.json();
 	}
 
@@ -565,6 +843,10 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbReferral = new Referral();
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbReferral = new Referral();
+		}
+	});
 });
diff --git a/assets/js/concise/SchemaManager.js b/assets/js/concise/SchemaManager.js
new file mode 100644
index 0000000..bad3fa4
--- /dev/null
+++ b/assets/js/concise/SchemaManager.js
@@ -0,0 +1,459 @@
+/**
+ * SEO Admin Page Controller
+ * Handles schema type switching, form initialization, and tabs
+ * Works with FormController for unified form handling
+ */
+class SchemaManager {
+	constructor() {
+		this.formController = null;
+		this.tabsInstance = null;
+		this.queue = window.jvbQueue;
+		this.a11y = window.jvbA11y;
+
+		this.init();
+	}
+
+	init() {
+		// Initialize FormController
+		if (window.jvbForm && !window.formController) {
+			this.formController = new window.jvbForm();
+			window.formController = this.formController;
+		} else if (window.formController) {
+			this.formController = window.formController;
+		}
+
+		// Initialize main Tabs (they're outside forms, so FormController won't handle them)
+		if (window.jvbTabs) {
+			const tabContainer = document.querySelector('.jvb-seo-admin');
+			if (tabContainer) {
+				this.tabsInstance = new window.jvbTabs(tabContainer);
+			}
+		}
+
+		// Subscribe to FormController events
+		if (this.formController) {
+			this.formController.subscribe((event, data) => {
+				if (event === 'form-submit') {
+					this.handleFormSubmit(data);
+				}
+			});
+		}
+
+		// Subscribe to Queue events
+		if (this.queue) {
+			this.queue.subscribe((event, data) => {
+				if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'seo') return;
+
+				if (event === 'operation-completed') {
+					this.handleQueueSuccess(event, data);
+				} else if (event === 'operation-failed-permanent') {
+					this.handleQueueFailure(event, data);
+				}
+			});
+		}
+
+		// Initialize all SEO forms
+		this.initializeForms();
+
+		// Add preserved field styling
+		this.addPreservedFieldStyles();
+	}
+
+	/**
+	 * Initialize all SEO forms
+	 */
+	initializeForms() {
+		const forms = document.querySelectorAll('form[data-save="seo"]');
+
+		forms.forEach(form => {
+			// Register with FormController
+			if (this.formController) {
+				this.formController.registerForm(form, {
+					endpoint: 'seo',
+					autosave: false,
+					formStatus: false
+				});
+			}
+
+			// Set up type switching
+			this.initializeTypeSwitch(form);
+
+			// Set up reset button
+			const resetBtn = form.querySelector('[data-action="reset"]');
+			if (resetBtn) {
+				resetBtn.addEventListener('click', () => this.handleReset(form));
+			}
+		});
+	}
+
+	/**
+	 * Handle form submission via Queue
+	 */
+	handleFormSubmit(data) {
+		const form = data.config.element;
+		const context = form.dataset.content;
+		const formData = data.fullData;
+
+		// Build operation for queue
+		const operation = {
+			endpoint: 'seo',
+			headers: {
+				'X-WP-Nonce': window.auth.getNonce()
+			},
+			data: {
+				context: context,
+				action: 'save',
+				...formData
+			},
+			popup: 'Saving SEO configuration',
+			title: `Saving ${context} settings`
+		};
+
+		this.queue.addToQueue(operation);
+	}
+
+	/**
+	 * Handle reset button
+	 */
+	async handleReset(form) {
+		const context = form.dataset.content;
+
+		if (!confirm('Reset to default settings? This cannot be undone.')) {
+			return;
+		}
+
+		const operation = {
+			endpoint: 'seo',
+			headers: {
+				'X-WP-Nonce': window.auth.getNonce()
+			},
+			data: {
+				context: context,
+				action: 'reset'
+			},
+			popup: 'Resetting configuration',
+			title: `Resetting ${context} to defaults`
+		};
+
+		this.queue.addToQueue(operation);
+	}
+
+	/**
+	 * Handle queue success
+	 */
+	handleQueueSuccess(event, data) {
+		console.log('SEO save successful:', data);
+
+		if (this.a11y && typeof this.a11y.announce === 'function') {
+			this.a11y.announce('Configuration saved successfully');
+		}
+
+		// If this was a reset, reload the form data
+		if (data.operation?.data?.action === 'reset' && data.response?.schema) {
+			this.reloadFormData(data.operation.data.context, data.response);
+		}
+	}
+
+	/**
+	 * Handle queue failure
+	 */
+	handleQueueFailure(event, data) {
+		console.error('SEO operation failed permanently:', data);
+
+		if (this.a11y && typeof this.a11y.announce === 'function') {
+			this.a11y.announce(`Error: ${data.error_message || 'Operation failed'}`);
+		}
+	}
+
+	/**
+	 * Reload form data after reset
+	 */
+	reloadFormData(context, response) {
+		const form = document.querySelector(`form[data-content="${context}"]`);
+		if (!form) return;
+
+		const schema = response.schema || {};
+
+		// Update form fields with reset values
+		Object.keys(schema).forEach(key => {
+			const field = form.querySelector(`[name="${key}"]`);
+			if (field) {
+				if (field.type === 'checkbox') {
+					field.checked = !!schema[key];
+				} else {
+					field.value = schema[key] || '';
+				}
+			}
+		});
+
+		if (this.a11y && typeof this.a11y.announce === 'function') {
+			this.a11y.announce('Form reset to defaults');
+		}
+	}
+
+	/**
+	 * Initialize schema type switching for a form
+	 */
+	initializeTypeSwitch(form) {
+		const typeSelect = form.querySelector('select[name="type"]');
+		if (!typeSelect) return;
+
+		// Handle type change with confirmation
+		typeSelect.addEventListener('change', (e) => {
+			const oldType = form.dataset.currentType || typeSelect.dataset.initialValue;
+			const newType = e.target.value;
+
+			// If types are the same, no need to confirm
+			if (oldType === newType) return;
+
+			// Show confirmation dialog
+			this.confirmTypeChange(form, typeSelect, oldType, newType);
+		});
+
+		// Store initial type for reference
+		typeSelect.dataset.initialValue = typeSelect.value;
+		form.dataset.currentType = typeSelect.value;
+	}
+
+	/**
+	 * Confirm type change with user
+	 */
+	confirmTypeChange(form, typeSelect, oldType, newType) {
+		// Get current form values
+		const currentValues = {};
+		const formData = new FormData(form);
+		for (let [key, value] of formData.entries()) {
+			if (key !== 'type' && value && value !== '') {
+				currentValues[key] = value;
+			}
+		}
+
+		// Get template for new type to check which fields will be preserved
+		const newTemplate = window.getTemplate(`seo-${newType}`);
+		if (!newTemplate) {
+			console.error('No template found for type:', newType);
+			typeSelect.value = oldType;
+			return;
+		}
+
+		// Extract base field names from current values
+		// Handles both regular fields and repeater fields (fieldName:index:subField)
+		const getBaseFieldName = (fieldName) => {
+			return fieldName.split(':')[0];
+		};
+
+		const currentBaseFields = new Set(
+			Object.keys(currentValues).map(getBaseFieldName)
+		);
+
+		// Get base field names from new template
+		const newFieldElements = newTemplate.querySelectorAll('[data-field]');
+		const newBaseFields = new Set(
+			Array.from(newFieldElements).map(el => el.dataset.field)
+		);
+
+		// If no data-field attributes, fall back to name attributes
+		if (newBaseFields.size === 0) {
+			const nameElements = newTemplate.querySelectorAll('[name]');
+			Array.from(nameElements).forEach(el => {
+				newBaseFields.add(getBaseFieldName(el.getAttribute('name')));
+			});
+		}
+
+		// Determine preserved and lost fields
+		const preservedFields = [...currentBaseFields].filter(field => newBaseFields.has(field));
+		const lostFields = [...currentBaseFields].filter(field => !newBaseFields.has(field));
+
+		// Build confirmation message
+		let message = `Change schema type from ${oldType} to ${newType}?\n\n`;
+
+		if (preservedFields.length > 0) {
+			message += `✓ ${preservedFields.length} field value(s) will be preserved:\n`;
+			message += preservedFields.map(f => `  • ${f}`).join('\n');
+			message += '\n\n';
+		}
+
+		if (lostFields.length > 0) {
+			message += `⚠ ${lostFields.length} field value(s) will be lost:\n`;
+			message += lostFields.map(f => `  • ${f}`).join('\n');
+		}
+
+		// Show confirmation
+		if (confirm(message)) {
+			this.handleTypeChange(form, typeSelect, newType);
+		} else {
+			// User cancelled - revert select
+			typeSelect.value = oldType;
+
+			if (this.a11y && typeof this.a11y.announce === 'function') {
+				this.a11y.announce('Type change cancelled');
+			}
+		}
+	}
+
+	/**
+	 * Handle schema type change
+	 */
+	handleTypeChange(form, typeSelect, newType) {
+		const oldType = form.dataset.currentType || typeSelect.dataset.initialValue;
+
+		// Collect current form data as structured object
+		// Group repeater fields by base name
+		const currentData = this.collectFormData(form);
+
+		// Get template for new type
+		const newFields = window.getTemplate(`seo-${newType}`);
+		if (!newFields) {
+			console.error('No template found for type:', newType);
+			return;
+		}
+
+		// Replace the field container
+		const oldContainer = form.querySelector('.seo-' + oldType);
+		if (oldContainer) {
+			// Insert new fields
+			oldContainer.parentNode.insertBefore(newFields, oldContainer);
+			// Remove old container
+			oldContainer.remove();
+		}
+
+		// Update current type tracking
+		form.dataset.currentType = newType;
+
+		// Use PopulateForm to properly populate all fields including repeaters
+		if (window.jvbPopulateForm) {
+			const populator = new window.jvbPopulateForm();
+			const preservedFields = [];
+
+			// Populate each field that exists in both schemas
+			Object.keys(currentData).forEach(fieldName => {
+				const fieldWrapper = form.querySelector(`[data-field="${fieldName}"]`);
+				if (fieldWrapper) {
+					const fieldType = this.getFieldType(fieldWrapper);
+					const fieldValue = currentData[fieldName];
+
+					// Use PopulateForm's methods for complex fields
+					if (fieldType === 'repeater' && Array.isArray(fieldValue)) {
+						populator.populateRepeaterField(fieldWrapper, fieldName, fieldValue);
+						preservedFields.push(fieldName);
+					} else if (fieldValue !== null && fieldValue !== undefined && fieldValue !== '') {
+						// Simple field - populate directly
+						const field = fieldWrapper.querySelector(`[name="${fieldName}"]`) ||
+							fieldWrapper.querySelector(`[name^="${fieldName}"]`);
+						if (field) {
+							this.populateSimpleField(field, fieldValue);
+							preservedFields.push(fieldName);
+						}
+					}
+				}
+			});
+
+			// Announce changes
+			if (preservedFields.length > 0) {
+				const message = `Schema type changed to ${newType}. Preserved ${preservedFields.length} field value(s).`;
+				console.log(message);
+
+				if (this.a11y && typeof this.a11y.announce === 'function') {
+					this.a11y.announce(message);
+				}
+			} else {
+				const message = `Schema type changed to ${newType}.`;
+				if (this.a11y && typeof this.a11y.announce === 'function') {
+					this.a11y.announce(message);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Collect form data into structured object
+	 * Handles repeater fields by grouping them
+	 */
+	collectFormData(form) {
+		const data = {};
+		const formData = new FormData(form);
+
+		for (let [key, value] of formData.entries()) {
+			if (key === 'type' || key === 'context') continue;
+
+			// Check if this is a repeater field (format: fieldName:index:subField)
+			if (key.includes(':')) {
+				const parts = key.split(':');
+				const baseField = parts[0];
+				const index = parseInt(parts[1]);
+				const subField = parts[2];
+
+				// Initialize repeater array if needed
+				if (!data[baseField]) {
+					data[baseField] = [];
+				}
+
+				// Initialize row object if needed
+				if (!data[baseField][index]) {
+					data[baseField][index] = {};
+				}
+
+				// Store the value
+				data[baseField][index][subField] = value;
+			} else {
+				// Regular field
+				data[key] = value;
+			}
+		}
+
+		return data;
+	}
+
+	/**
+	 * Get field type from wrapper element
+	 */
+	getFieldType(fieldWrapper) {
+		if (fieldWrapper.classList.contains('repeater')) {
+			return 'repeater';
+		}
+		// Add other field type checks as needed
+		return 'text';
+	}
+
+	/**
+	 * Populate a simple field with value
+	 */
+	populateSimpleField(field, value) {
+		if (field.type === 'checkbox') {
+			field.checked = value === '1' || value === 'true' || value === true;
+		} else if (field.tagName === 'SELECT') {
+			setTimeout(() => {
+				field.value = value;
+			}, 10);
+		} else {
+			field.value = value;
+		}
+
+		// Visual feedback
+		field.classList.add('value-preserved');
+		setTimeout(() => field.classList.remove('value-preserved'), 2000);
+	}
+
+	/**
+	 * Add CSS for preserved field indication
+	 */
+	addPreservedFieldStyles() {
+		const style = document.createElement('style');
+		style.textContent = `
+            .value-preserved {
+                background-color: #e7f5e7 !important;
+                transition: background-color 0.3s ease;
+            }
+        `;
+		document.head.appendChild(style);
+	}
+}
+
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbSchema = new SchemaManager();
+		}
+	});
+});
diff --git a/assets/js/dash/ShopManager.js b/assets/js/concise/ShopManager.js
similarity index 93%
rename from assets/js/dash/ShopManager.js
rename to assets/js/concise/ShopManager.js
index 8c38309..7d5b8ab 100644
--- a/assets/js/dash/ShopManager.js
+++ b/assets/js/concise/ShopManager.js
@@ -16,7 +16,7 @@
 
 	// handleSave(data){
 	//
-	// 	data.user = jvbSettings.currentUser;
+	// 	data.user = window.auth.getUser();
 	//
 	// 	window.jvbQueue.addToQueue({
 	// 		endpoint: 'shop',
diff --git a/assets/js/dash/SquareCheckout.js b/assets/js/concise/SquareCheckout.js
similarity index 100%
rename from assets/js/dash/SquareCheckout.js
rename to assets/js/concise/SquareCheckout.js
diff --git a/assets/js/dash/Tabs.js b/assets/js/concise/Tabs.js
similarity index 75%
rename from assets/js/dash/Tabs.js
rename to assets/js/concise/Tabs.js
index 7d86023..4dfce20 100644
--- a/assets/js/dash/Tabs.js
+++ b/assets/js/concise/Tabs.js
@@ -63,7 +63,7 @@
 
             if(hasChildren && hasChildren.querySelector('.tabs')){
                 let container = this.container.querySelector(`.tab-content[data-tab="${tab.dataset['tab']}"]`);
-                let tabs = new window.jvbTabs(container, {}, this);
+				let tabs = new window.jvbTabs(container, {updateURL: false}, this);
                 this.childTabs.set(tab.dataset.tab, tabs);
             }
         });
@@ -156,58 +156,55 @@
      * @param {string} tab - Tab to switch to ('items' or 'lists')
      * @param {boolean} updateHistory - Whether to push the state to the url
      */
-    switchTab(tab, updateHistory = false) {
-
+	switchTab(tab, updateHistory = false) {
 		document.activeElement?.blur();
-		// if (typeof this.callbacks['onSwitch'] === 'function') {
-		// 	this.callbacks.onSwitch(tab)
-		// }
-        // Update tab buttons
-        this.tabs.querySelectorAll('[data-tab]').forEach(tabBtn => {
-            tabBtn.classList.toggle('active', tabBtn.dataset.tab === tab);
-            tabBtn.setAttribute('aria-selected', tabBtn.dataset.tab === tab);
-        });
 
-        // Update tab panels
-        this.container.querySelectorAll('.tab-content').forEach(content => {
-            content.classList.toggle('active', content.dataset.tab === tab);
-            content.setAttribute('aria-hidden', content.dataset.tab !== tab);
+		// Update tab buttons
+		this.tabs.querySelectorAll('[data-tab]').forEach(tabBtn => {
+			tabBtn.classList.toggle('active', tabBtn.dataset.tab === tab);
+			tabBtn.setAttribute('aria-selected', tabBtn.dataset.tab === tab);
+		});
+
+		// Update tab panels
+		this.container.querySelectorAll('.tab-content').forEach(content => {
+			content.classList.toggle('active', content.dataset.tab === tab);
+			content.setAttribute('aria-hidden', content.dataset.tab !== tab);
 			content.hidden = content.dataset.tab !== tab;
-        });
+		});
 
-        // Update state
-        this.activeTab = tab;
-        if (this.callbacks[tab]) {
-            this.callbacks[tab]();
-        }
+		// Update state
+		this.activeTab = tab;
+		if (this.callbacks[tab]) {
+			this.callbacks[tab]();
+		}
 
-        // Update URL hash with full path (only from root container)
-        if (updateHistory) {
-            if (!this.parent) {
-                // This is a root container, build full path including child tabs
-                let fullPath = tab;
+		// Activate first child tab if this tab has children
+		const childContainer = this.childTabs.get(tab);
+		if (childContainer) {
+			const firstTab = childContainer.container.querySelector('button.tab')?.dataset.tab;
+			if (firstTab) {
+				childContainer.switchTab(firstTab, false);
+			}
+		}
 
-                // Add active child tab paths if they exist
-                const childContainer = this.childTabs.get(tab);
-                if (childContainer && childContainer.activeTab) {
-                    fullPath = childContainer.getFullTabPath(childContainer.activeTab);
-                }
+		// Update URL hash with full path (only from root container)
+		if (updateHistory) {
+			if (!this.parent) {
+				window.history.pushState({ tab: tab }, '', `#${tab}`);
+			} else {
+				// This is a child container, notify parent to update URL
+				this.parent.updateUrlFromChild();
+			}
+		}
 
-                window.history.pushState({ tab: fullPath }, '', `#${fullPath}`);
-            } else {
-                // This is a child container, notify parent to update URL
-                this.parent.updateUrlFromChild();
-            }
-        }
+		// Update select dropdown if it exists
+		if (this.selectDropdown && this.selectDropdown.querySelector(`option[value="${tab}"]`)) {
+			this.selectDropdown.value = tab;
+		}
 
-        // Update select dropdown if it exists
-        if (this.selectDropdown && this.selectDropdown.querySelector(`option[value="${tab}"]`)) {
-            this.selectDropdown.value = tab;
-        }
-
-        // Announce to screen readers
-        this.a11y.announce(`Switched to ${tab} tab`);
-    }
+		// Announce to screen readers
+		this.a11y.announce(`Switched to ${tab} tab`);
+	}
 
     /**
      * Update URL when a child tab changes
diff --git a/assets/js/dash/TaxonomyCreator.js b/assets/js/concise/TaxonomyCreator.js
similarity index 99%
rename from assets/js/dash/TaxonomyCreator.js
rename to assets/js/concise/TaxonomyCreator.js
index 37b5960..91ed836 100644
--- a/assets/js/dash/TaxonomyCreator.js
+++ b/assets/js/concise/TaxonomyCreator.js
@@ -271,7 +271,7 @@
 				method: 'POST',
 				headers: {
 					'Content-Type': 'application/json',
-					'X-WP-Nonce': jvbSettings.nonce
+					'X-WP-Nonce': window.auth.getNonce()
 				},
 				body: JSON.stringify({
 					taxonomy: taxonomy,
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index bf3af8f..032c749 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -480,7 +480,13 @@
 
 	initAutocomplete()
 	{
-		this.autocompleteHandler = window.debounce((e) => this.handleAutocomplete(e), 300);
+		this.autocompleteHandler = (e) => {
+			window.debouncer.schedule(
+				'taxonomy-autocomplete',
+				() => this.handleAutocomplete(e),
+				300
+			);
+		};
 		document.addEventListener('input', this.autocompleteHandler);
 		document.addEventListener('blur', this.cleanupAutocomplete.bind(this));
 		// Preload taxonomy data on focus
@@ -1545,5 +1551,10 @@
  * Initialize singleton
  */
 document.addEventListener('DOMContentLoaded', function() {
-	window.jvbSelector = new TaxonomySelector();
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbSelector = new TaxonomySelector();
+		}
+	});
+
 });
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index 56fc593..cf6f6cd 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -500,7 +500,6 @@
 			.map(upload => upload.dataset.uploadId)
 			.filter(id => id);
 
-		console.log('Reordered items:', items);
 
 		// Update hidden input (for form submission)
 		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
@@ -508,7 +507,7 @@
 			hiddenInput.value = items.join(',');
 		}
 
-		// ✅ Update fieldState with new order
+		// Update fieldState with new order
 		const fieldId = this.getFieldIdFromElement(grid);
 		if (fieldId) {
 			const fieldData = this.getFieldData(fieldId);
@@ -524,7 +523,7 @@
 			// If reordering in preview, the order is implicit by DOM position
 			// (we don't store preview order separately)
 
-			this.schedulePersistance(fieldId); // ✅ Persist changes
+			this.schedulePersistance(fieldId);
 		}
 
 		this.a11y.announce('Item reordered');
@@ -1192,7 +1191,7 @@
 			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
 			canMerge: false,
 			headers: {
-				'action_nonce': jvbSettings.dash
+				'action_nonce': window.auth.getNonce('dash')
 			},
 			append: '_upload',
 		};
@@ -1243,7 +1242,7 @@
 			title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''} to server...`,
 			popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
 			canMerge: false,
-			headers: { 'action_nonce': jvbSettings.dash },
+			headers: { 'action_nonce': window.auth.getNonce('dash') },
 			append: '_upload'
 		};
 
@@ -1324,7 +1323,7 @@
 			data: queueData,
 			title: 'Updating meta',
 			canMerge: true,
-			headers: { 'action_nonce': jvbSettings.dash }
+			headers: { 'action_nonce': window.auth.getNonce('dash') }
 		};
 
 		try {
@@ -1671,7 +1670,7 @@
 					storedGroup.changes = { ...groupData.changes };
 				}
 
-				// ✅ Preserve upload order
+				// Preserve upload order
 				if (groupData.uploads) {
 					storedGroup.uploads = [...groupData.uploads];
 				}
@@ -2530,11 +2529,6 @@
 	 * Save field data to store, converting Sets to Arrays
 	 */
 	async saveFieldData(fieldData) {
-		console.log('💾 Saving:', fieldData.id, {
-			uploads: fieldData.uploads?.size,
-			groups: fieldData.groups?.length
-		});
-
 		await this.fieldStore.save({
 			...fieldData,
 			timestamp: Date.now()
@@ -2844,12 +2838,6 @@
 	async checkForStoredUploads() {
 		const allFieldStates = this.fieldStore.getAll();
 
-		console.log('Checking for stored uploads...', {
-			fieldStates: allFieldStates.length,
-			uploadStoreSize: this.uploadStore.data.size
-		});
-		console.log(this.uploadStore.getAll());
-		console.log(this.fieldStore.getAll());
 		const pendingFields = allFieldStates.filter(field => {
 			if (!field.uploads) return false;
 
@@ -2866,7 +2854,6 @@
 					['completed', 'processed', 'local_processing', 'processed-original'].includes(upload.status);
 			});
 		});
-		console.log('Found pending fields:', pendingFields.length);
 		if (pendingFields.length === 0) return;
 
 		this.showRecoveryNotification(pendingFields);
@@ -3154,6 +3141,10 @@
 }
 
 // Initialize when DOM is ready
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbUploads = new UploadManager();
+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
deleted file mode 100644
index 7e34eab..0000000
--- a/assets/js/concise/UploadManagerOld.js
+++ /dev/null
@@ -1,4114 +0,0 @@
-class UploadManager {
-	constructor() {
-		//Load dependencies
-		this.queue = window.jvbQueue;
-		this.a11y = window.jvbA11y;
-		this.error = window.jvbError;
-		this.notifications = window.jvbNotifications;
-
-		//Load Datastore
-		this.initDB();
-
-		//State management
-		this.fields = new Map();
-		this.uploads = new Map();
-		this.uploadBlobs = new Map();
-		this.timeouts = new Map();
-		this.selected = new Map();
-		this.dragState = {
-			isDragging: false,
-			primaryItem: null,
-			draggedItems: [],
-			isMultiDrag: false,
-			fieldId: null,
-			sourceType: null,
-			startTime: null,
-			startPosition: { x: 0, y: 0 },
-			currentPosition: { x: 0, y: 0 },
-			currentTarget: null,
-			validTarget: null,
-			dragPreview: null,
-			touchId: null,
-			touchMoved: false
-		};
-		this.hasGroups = false;
-
-		this.selectionHandlers = new Map();
-
-		//Worker
-		this.worker = {
-			worker: null,
-			timeout: null,
-			tasks: new Map(),
-			restart: {
-				count: 0,
-				max: 3,
-			},
-			settings: {
-				timeout: 10000, //10 seconds per image
-				batchSize: 1,
-				maxConcurrent: 3,
-				restartAfterTimeout: true
-			}
-		};
-
-		//Groups!
-		this.touch = {
-			x: null,
-			y: null
-		}
-		this.hasBulkContext = document.querySelector('details.uploader')!==null;
-		this.isTouching = false;
-		this.groups = new Map();
-		this.groupsMeta = new Map();
-
-		//Notification and Subscribers
-		this.subscribers = new Set();
-
-		this.settings = {
-			allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
-			maxFileSize: 5242880,
-			maxProcessingTime: 120000, // 2 minutes max for processing
-			processingCheckInterval: 5000, // Check every 5 seconds
-			smartCompression: true,
-			fieldTypes: {
-				'single': { maxFiles: 1, allowMultiple: false },
-				'gallery': { maxFiles: 20, allowMultiple: true },
-				'groupable': { maxFiles: 20, allowMultiple: true }
-			}
-		};
-
-		this.acceptedTypes = {
-			image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
-			video: ['video/mp4', 'video/webm', 'video/ogg', 'video/ogv'],
-			document: [
-				'application/pdf',
-				'application/msword',
-				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-				'text/plain',
-				'text/csv'
-			]
-		};
-
-		this.maxSizes = {
-			image: 5 * 1024 * 1024,    // 5MB
-			video: 100 * 1024 * 1024,  // 100MB
-			document: 10 * 1024 * 1024 // 10MB
-		};
-
-		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.initElements();
-		this.initListeners();
-		this.initCompressionWorker();
-		this.queue.subscribe((event, operation) => {
-			if (operation.endpoint !== 'uploads') {
-				return;
-			}
-			switch(event) {
-				case 'cancel-operation':
-					this.clearField(operation.data.get('field_key'));
-					break;
-				case 'operation-status':
-					const fieldId = operation.data?.field_key ||
-						(operation.data instanceof FormData ?
-							operation.data.get('field_key') : null);
-
-					if (fieldId) {
-						this.updateFieldStatus(fieldId, operation.status);
-					}
-					break;
-			}
-		});
-		this.scanFields();
-	}
-
-	initElements() {
-		this.selectors = {
-			field: {
-				field: '.field.upload',
-				dropZone: '.file-upload-container',
-				preview: '.item-grid.preview',
-				previewWrap: '.preview-wrap',
-				selectAll: '[type=checkbox]#select-all-uploads',
-				selectActions: '.selection-actions',
-				selectCount: '.selected .info',
-				hiddenValue: 'input[type="hidden"]',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-			},
-			item: {
-				img: 'img',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-				status: '.status',
-				select: '[name*="select-item"]',
-				actions: '.item-actions',
-				featured: '[name="featured"]',
-				meta: '.upload-meta'
-			},
-			groups: {
-				container: '.item-grid.groups',
-				display: '.group-display',
-				selectAll: '#select-all-group',
-				actions: '.selection-actions',
-				info: '.selection-controls .info',
-				count: '.selection-count',
-				group: '.upload-group',
-				empty: '.empty-group'
-			}
-		};
-		this.ui = {};
-	}
-
-	scanFields() {
-		document.querySelectorAll(this.selectors.field.field).forEach(uploader => {
-			this.registerUploader(uploader);
-		});
-	}
-
-	/**
-	 *
-	 * @param {HTMLElement} uploader
-	 * @param {object} options
-	 * @param {string} options.id Uploader field ID: defaults to uploader.dataset.fieldId
-	 * @param {string} options.type Uploader type: defaults to uploader.dataset.type
-	 * @param {number} options.maxFiles Maximum files to allow: defaults to type defaults
-	 * @param {boolean} options.multiple Whether to allow multiple uploads
-	 * @param {number} options.itemID The post or term ID this is for.
-	 * @param {string} options.mode
-	 * @returns {string}
-	 */
-	registerUploader(uploader, options = {}) {
-		//Determine if this is for a post, term, content uploader, or option
-		let key = uploader.dataset['uploader']??this.determineKey(uploader);
-
-		uploader.dataset['uploader'] = key;
-
-		if (!this.fields.has(key)) {
-			let type = uploader.dataset.type??'single';
-
-			let typeConfig = this.settings.fieldTypes[type]??this.settings.fieldTypes['single'];
-			let config = {
-				key: key,
-				name: uploader.dataset.field,
-				ui: {},
-				type: type,
-				subtype: uploader.dataset.subtype??'image',
-				maxFiles: typeConfig.maxFiles,
-				multiple: typeConfig.allowMultiple,
-				content: uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??false,
-				itemID: uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??false,
-				context: uploader.dataset.context??uploader.closest('dialog')?.dataset.context??false,
-				mode: uploader.dataset.mode??'direct',
-				destination: uploader.dataset.destination ?? 'meta',
-				... options
-			};
-
-			config.ui = window.uiFromSelectors(this.selectors, uploader);
-			config.ui.groups.groups = new Map();
-
-			this.selected.set(key, new Set());
-			this.fields.set(key, config);
-			if(config.destination === 'post_group' && !this.hasGroups) {
-				this.initGroupListeners();
-			}
-			// Initialize selection handler for this field
-			this.initSelectionHandler(key, config);
-		}
-		return key;
-	}
-
-	initSelectionHandler(fieldKey) {
-		const field = this.fields.get(fieldKey);
-		if (!field) return;
-
-		// Don't reinitialize if already exists
-		if (this.selectionHandlers.has(fieldKey)) {
-			return this.selectionHandlers.get(fieldKey);
-		}
-
-		// Get the container - use preview for uploads in preview, or field for all uploads
-		const container = field.ui.field.previewWrap;
-		if (!container) {
-			console.warn('No container found for selection handler:', fieldKey);
-			return;
-		}
-
-		const handler = new window.jvbHandleSelection({
-			container: container,
-			ui: {
-				selectAll: field.ui.field.selectAll,
-				bulkControls: field.ui.field.selectActions,
-				count: field.ui.field.selectCount
-			},
-			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(fieldKey, data.selectedItems);
-					break;
-				case 'select-all':
-					this.handleSelectAll(data.container, data.selected);
-					break;
-			}
-		});
-
-		this.selectionHandlers.set(fieldKey, handler);
-
-		return handler;
-	}
-
-	addGroupSelectionHandler(fieldId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const group = this.groups.get(groupId);
-		if (!group) return;
-
-		let handlerKey = fieldId+'_'+groupId;
-		// Don't reinitialize if already exists
-		if (this.selectionHandlers.has(handlerKey)) {
-			return this.selectionHandlers.get(handlerKey);
-		}
-
-		// Get the container - use preview for uploads in preview, or field for all uploads
-		const container = group.element;
-		if (!container) {
-			console.warn('No container found for selection handler:', fieldKey);
-			return;
-		}
-
-		const handler = new window.jvbHandleSelection({
-			container: container,
-			ui: {
-				selectAll: container.querySelector(this.selectors.groups.selectAll),
-				bulkControls: container.querySelector(this.selectors.groups.actions),
-				count: container.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;
-	}
-
-	removeSelectionHandler(fieldId, groupId = null) {
-		let key = fieldId;
-		if (groupId) {
-			key = key+'_'+groupId;
-		}
-		if (this.selectionHandlers.has(key)) {
-			let handler = this.selectionHandlers.get(key);
-			handler.destroy();
-			this.selectionHandlers.delete(key);
-		}
-	}
-
-	/**
-	 * Builds a key from the uploader, built from the Content Type, ItemID, and FieldName
-	 * @param uploader
-	 * @returns {string}
-	 */
-	determineKey(uploader) {
-		let content = uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??'';
-		let itemID = uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??'';
-		let field = uploader.dataset.field;
-		return `${content}_${itemID}_${field}`;
-	}
-
-	/**
-	 *
-	 * @param {HTMLElement} element
-	 */
-	getFieldIdFromElement(element) {
-		let field = element.closest(this.selectors.field.field);
-		if (!field) {
-			return;
-		}
-		return field.dataset.uploader??this.determineKey(field);
-	}
-
-	getFieldFromElement(element) {
-		let id = this.getFieldIdFromElement(element);
-		return (this.fields.has(id)) ? this.fields.get(id) : false;
-	}
-
-	getUploadFromElement(element) {
-		let id = this.getUploadIdFromElement(element);
-		return (this.uploads.has(id)) ? this.uploads.get(id) : false;
-	}
-
-	getUploadIdFromElement(element) {
-		let upload = element.closest('[data-upload-id]');
-		return upload?.dataset.uploadId || null;
-	}
-
-	getGroupFromElement(element) {
-		let groupId = this.getGroupIdFromElement(element);
-		return (this.groups.has(groupId)) ? this.groups.get(groupId) : false;
-	}
-	getGroupIdFromElement(element) {
-		return element.dataset.groupId??element.closest('[data-group-id]')?.dataset.groupId??element.closest(':has([data-group-id])')?.querySelector('[data-group-id]')?.dataset.groupId??null;
-	}
-
-	getModalType(field) {
-		// Safety check for field.ui
-		if (!field || !field.ui || !field.ui.field || !field.ui.field.field) {
-			return null;
-		}
-
-		const dialog = field.ui.field.field.closest('dialog');
-		if (!dialog) return null;
-
-		if (dialog.classList.contains('edit')) return 'edit';
-		if (dialog.classList.contains('create')) return 'create';
-		if (dialog.classList.contains('bulkEdit')) return 'bulkEdit';
-
-		return dialog.className;
-	}
-
-	getStatusText(status) {
-		return this.statusMapping[status] || status;
-	}
-
-	getStatusIcon(status) {
-		return window.getIcon(this.queue.icons[status]);
-	}
-	getStatusProgress(status) {
-		switch (status) {
-			case 'local_processing':
-				return 28;
-			case 'queued':
-				return 50;
-			case 'uploading':
-				return 66;
-			case 'pending':
-				return 75;
-			case 'processing':
-				return 89;
-			case 'completed':
-				return 100;
-			default:
-				return 0;
-		}
-	}
-
-	/******************************************************************************
-	 LISTENERS
-	******************************************************************************/
-	initListeners() {
-		this.clickHandler 		= this.handleClick.bind(this);
-		this.changeHandler 		= this.handleChange.bind(this);
-
-		if (this.hasBulkContext) {
-			this.pasteHandler 		= this.handlePaste.bind(this);
-			document.addEventListener('paste', this.pasteHandler);
-		}
-
-
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('change', this.changeHandler);
-		window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
-	}
-	clearListeners() {
-		document.removeEventListener('click', this.clickHandler);
-		document.removeEventListener('change', this.changeHandler);
-		if (this.hasBulkContext) {
-			document.removeEventListener('paste', this.pasteHandler);
-		}
-	}
-
-	initGroupListeners() {
-		this.hasGroups = true;
-
-		this.dragStartHandler 	= this.handleDragStart.bind(this);
-		this.dragEndHandler 	= this.handleDragEnd.bind(this);
-		this.dragEnterHandler 	= this.handleDragEnter.bind(this);
-		this.dragOverHandler 	= this.handleDragOver.bind(this);
-		this.dragLeaveHandler 	= this.handleDragLeave.bind(this);
-		this.dropHandler 		= this.handleDrop.bind(this);
-
-		this.touchStartHandler 	= this.handleTouchStart.bind(this);
-		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
-		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
-		this.touchCancelHandler	= this.handleTouchCancel.bind(this);
-
-		document.addEventListener('dragstart', this.dragStartHandler);
-		document.addEventListener('dragend', this.dragEndHandler);
-		document.addEventListener('dragenter', this.dragEnterHandler);
-		document.addEventListener('dragover', this.dragOverHandler);
-		document.addEventListener('dragleave', this.dragLeaveHandler);
-		document.addEventListener('drop', this.dropHandler);
-
-		document.addEventListener('touchstart', this.touchStartHandler, { passive: false });
-		document.addEventListener('touchmove', this.touchMoveHandler, { passive: false });
-		document.addEventListener('touchend', this.touchEndHandler, { passive: false });
-		document.addEventListener('touchcancel', this.touchCancelHandler, { passive: false });
-
-		document.addEventListener('input', (e) => {
-			if (e.target.matches('.fields.group input, .fields.group textarea')) {
-				this.handleGroupMetadataChange(e);
-			}
-		});
-	}
-	handleGroupMetadataChange(e) {
-		if (!e.target.closest('.fields.group')) return;
-
-		const groupElement = e.target.closest('[data-group-id]');
-		if (!groupElement) return;
-
-		const fieldId = groupElement.dataset.fieldId;
-		this.persistFieldState(fieldId);
-	}
-	clearGroupListeners() {
-		document.removeEventListener('dragstart', this.dragStartHandler);
-		document.removeEventListener('dragend', this.dragEndHandler);
-		document.removeEventListener('dragenter', this.dragEnterHandler);
-		document.removeEventListener('dragover', this.dragOverHandler);
-		document.removeEventListener('dragleave', this.dragLeaveHandler);
-		document.removeEventListener('drop', this.dropHandler);
-
-		document.removeEventListener('touchstart', this.touchStartHandler, { passive: false });
-		document.removeEventListener('touchmove', this.touchMoveHandler, { passive: false });
-		document.removeEventListener('touchend', this.touchEndHandler, { passive: false });
-		document.removeEventListener('touchcancel', this.touchCancelHandler, { passive: false });
-	}
-
-	handleClick(e) {
-		if (!e.target.closest(this.selectors.field.field)) {
-			return;
-		}
-		let actionButton = window.targetCheck(e, '[data-action]');
-
-		if (!actionButton) {
-			return;
-		}
-		let action = actionButton.dataset.action;
-
-		let field = this.getFieldFromElement(actionButton);
-		let selected = this.getCurrentSelection(field.key);
-		let group = this.getGroupFromElement(actionButton);
-		let groupId = (group) ? group.id : false;
-		let isItem = actionButton.closest('[data-upload-id]');
-		let items = 'upload';
-		let reference = 'it';
-		if (isItem) {
-			selected = [isItem.dataset.uploadId];
-		} else {
-			if (selected.length > 1) {
-				items = 'uploads';
-				reference = 'them';
-			}
-		}
-
-		let deleteUploads;
-
-		switch (action) {
-			case 'add-to-group':
-				//Create from selection
-				//Check for groupId, if no group id, create new group with selection
-				if (selected.length === 0) {
-					//Nothing to move
-					return;
-				}
-				if (!groupId) {
-					group = this.createGroup(field.key);
-					groupId = group.id;
-				}
-				this.addSelectionToGroup(group.element);
-
-				break;
-			case 'remove-from-group':
-				if (selected.length === 0) {
-					return;
-				}
-				//confirm if they want to keep uploads
-				//remove selection from group
-
-				deleteUploads = !confirm(`Would you like to keep the ${items}, just remove ${reference} from this group?`);
-				selected.forEach(upload => {
-					this.removeFromGroup(field.key, upload, groupId);
-					if (deleteUploads) {
-						this.removeUpload(field.key, upload);
-					}
-				});
-				break;
-			case 'delete-upload':
-				if (selected.length === 0) {
-					return;
-				}
-				//delete selection
-				deleteUploads = false;
-				reference = (reference === 'them') ? 'these' : 'this';
-				if (confirm(`Are you sure you want to delete ${reference} ${items}?`)) {
-					deleteUploads = true;
-				}
-				selected.forEach(upload => {
-					this.removeFromGroup(field.key, upload, groupId);
-					if (deleteUploads) {
-						this.removeUpload(field.key, upload);
-					}
-				});
-				break;
-			case 'delete-group':
-				//delete entire group
-				if (group.uploads.length > 0) {
-
-					deleteUploads = confirm(`Do you want to remove all uploads in the group, too?`);
-					if (deleteUploads) {
-						group.uploads.forEach(upload => {
-							this.removeUpload(field.key, upload);
-						});
-					} else {
-						group.uploads.forEach(upload => {
-							this.addImageToGroup(upload);
-						})
-					}
-				}
-				this.removeGroup(groupId, false);
-				break;
-			case 'upload':
-				//upload groups
-				e.preventDefault();
-				this.submitUploads(field.key);
-				break;
-			case 'restore':
-				let notification = document.querySelector('dialog.restore-uploads');
-				if (!notification) {
-					return;
-				}
-				//restore selected uploads
-				const selectedUploads = this.getSelectedRestorationUploads(notification);
-				if (selectedUploads.length === 0) {
-					// this.notifications.add('No uploads selected for restoration', 'warning');
-					return;
-				}
-				this.restoreSelectedUploads(selectedUploads);
-
-				this.restoreModal.handleClose();
-				this.restoreSelection.destroy();
-				this.restoreSelection = null;
-				// Clean up blob URLs before removing notification
-				this.cleanupRestoreNotificationUrls(notification);
-				notification.remove();
-				break;
-			case 'clear-cache':
-				if (!confirm(`Save these uploads for later?`)) {
-					//clear cached uploads
-					this.cleanupStoredRestoration();
-				}
-
-				this.restoreModal.handleClose();
-				this.restoreSelection.destroy();
-				this.restoreSelection = null;
-				this.restoreModal.destroy();
-				this.restoreModal.modal.remove();
-
-				break;
-		}
-	}
-	handleChange(e) {
-		if (!e.target.closest(this.selectors.field.field) || e.target.classList.contains(this.selectors.field.hiddenValue)) {
-			return;
-		}
-		e.preventDefault();
-
-		if (window.targetCheck(e, '[type="file"]')) {
-			let field = this.getFieldFromElement(e.target);
-			if (!field) {
-				console.warn('File change on unregistered field: ', field.key)
-				return;
-			}
-
-			const files = Array.from(e.target.files);
-			if (files.length === 0) return;
-
-			this.processFiles(field.key, files);
-			e.target.value = '';
-		} else if (e.target.closest('.upload-meta')) {
-			e.preventDefault();
-			let name = e.target.name;
-			let value = e.target.value;
-			let upload = this.getUploadFromElement(e.target);
-			upload.changes[name] = value;
-			this.uploads.set(upload.id, upload);
-			this.persistFieldState(upload.fieldId);
-
-			//It's meta!
-			//TODO:
-			//Step 1) determine whether the images have already been sent to the server. If not, we must wait until they have been
-			//Step 2) Queue the Meta changes. No need to wait, the Queue.js will handle any debouncing/timeouts
-			//Ensure the dependencies have all operations stored to the field that the images were uploaded with (can be multiple)
-			//Send to server for processing
-		} else if (e.target.closest('.group.fields')) {
-			let group = this.getGroupFromElement(e.target);
-			let name = e.target.name;
-			group.changes[name] = e.target.value;
-
-			this.persistFieldState(group.fieldId);
-			this.groups.set(group.id, group);
-		}
-	}
-
-	handlePaste(e) {
-		window.debouncer.schedule(
-			'imagePaste',
-			() => {
-				const items = Array.from(e.clipboardData.items);
-				const imageItems = items.filter(item => item.type.startsWith('image/'));
-
-				if (imageItems.length === 0) return;
-
-				e.preventDefault();
-
-				const fieldId = this.getFieldIdFromElement(e.target);
-				if (!fieldId) return;
-
-				// Convert clipboard items to files
-				const files = [];
-				imageItems.forEach((item, index) => {
-					const file = item.getAsFile();
-					if (file) {
-						// Rename for clarity
-						const newFile = new File([file], `pasted_image_${index + 1}.png`, {
-							type: file.type,
-							lastModified: Date.now()
-						});
-						files.push(newFile);
-					}
-				});
-
-				if (files.length > 0) {
-					this.processFiles(fieldId, files);
-				}
-			},
-			100
-		);
-	}
-
-	isTouchOnFormElement(target) {
-		// Check if target is a form element or inside one
-		const formElements = [
-			'input', 'button', 'label', 'select', 'textarea',
-		];
-
-		return formElements.some(selector => {
-			return target.matches(selector) || target.closest(selector);
-		});
-	}
-	/**** DRAG AND TOUCH *****/
-	startDragOperation(config) {
-		const {
-			primaryElement,
-			sourceType,
-			startPosition,
-			event
-		} = config;
-
-		const uploadId = this.getUploadIdFromElement(primaryElement);
-		const fieldId = this.getFieldIdFromElement(primaryElement);
-
-		// Determine what items to drag
-		const draggedItems = this.getDraggedItems(primaryElement);
-
-		// Initialize drag state
-		this.dragState = {
-			primaryItem: uploadId,
-			draggedItems: draggedItems,
-			isDragging: true,
-			isMultiDrag: draggedItems.length > 1,
-			fieldId: fieldId,
-			sourceType: sourceType,
-			startTime: Date.now(),
-			startPosition: startPosition,
-			currentPosition: startPosition,
-			currentTarget: null,
-			validTarget: null,
-			dragPreview: null,
-			touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
-			touchMoved: false
-		};
-
-		// Create drag preview
-		this.createDragPreview(primaryElement);
-
-		// Apply dragging state
-		this.applyDraggingState(true);
-
-		const announceText = this.dragState.isMultiDrag
-			? `Started dragging ${draggedItems.length} items`
-			: 'Started dragging item';
-
-		this.a11y.announce(announceText);
-		this.provideDragFeedback('start');
-
-		return true;
-	}
-
-	updateDragOperation(position, elementUnderPointer) {
-		if (!this.dragState.isDragging) return;
-
-		const { sourceType, startPosition } = this.dragState;
-
-		// Update position
-		this.dragState.currentPosition = position;
-
-		// Check for significant movement (touch)
-		if (sourceType === 'touch' && !this.dragState.touchMoved) {
-			const deltaX = Math.abs(position.x - startPosition.x);
-			const deltaY = Math.abs(position.y - startPosition.y);
-
-			if (deltaX > 10 || deltaY > 10) {
-				this.dragState.touchMoved = true;
-			}
-		}
-
-		// Update preview and target
-		this.updateDragPreview(position);
-		this.updateDropTarget(elementUnderPointer);
-	}
-
-	endDragOperation(elementUnderPointer = null) {
-		if (!this.dragState.isDragging) return;
-
-		const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
-			this.dragState.validTarget;
-
-		// Process drop if valid - but only here, not in handleDrop
-		if (wasSuccessful && this.dragState.validTarget) {
-			this.processItemDrop({
-				itemIds: this.dragState.draggedItems,
-				targetElement: this.dragState.validTarget,
-				fieldId: this.dragState.fieldId,
-				dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
-				sourceType: this.dragState.sourceType
-			});
-		}
-
-		// Cleanup
-		this.cleanupDragOperation();
-
-		const announceText = wasSuccessful
-			? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
-			: 'Drag cancelled';
-
-		this.a11y.announce(announceText);
-	}
-
-	/**
-	 * Shared method to process any drop operation (drag or touch)
-	 * @param {Object} dropData - Standardized drop data
-	 * @returns {boolean} Success status
-	 */
-	processItemDrop(dropData) {
-		const { itemIds, targetElement, fieldId, dropType, sourceType } = dropData;
-
-		if (!itemIds?.length || !targetElement || !fieldId) {
-			return false;
-		}
-
-		let isPreviewDrop = targetElement.classList.contains('preview') &&
-			targetElement.classList.contains('item-grid');
-		let actualTarget = targetElement;
-
-		// Handle empty group drops
-		if (targetElement.classList.contains('empty-group')) {
-			let group = this.createGroup(fieldId);
-			if (!group) {
-				console.error('Failed to create group');
-				return false;
-			}
-			actualTarget = group.grid;
-			isPreviewDrop = false;
-		}
-
-		itemIds.forEach(uploadId => {
-			this.addImageToGroup(uploadId, isPreviewDrop ? null : actualTarget, false);
-		});
-
-		const field = this.fields.get(fieldId);
-		if (field) {
-			this.clearAllSelections(field);
-		}
-
-		this.persistFieldState(fieldId);
-
-		const announceText = dropType === 'multiple'
-			? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
-			: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
-
-		this.a11y.announce(announceText);
-		this.provideFeedback(sourceType, 'success', {
-			count: itemIds.length,
-			isMultiple: dropType === 'multiple'
-		});
-
-		return true;
-	}
-
-
-
-	cleanupDragOperation() {
-		if (this.dragState.dragPreview) {
-			this.dragState.dragPreview.remove();
-		}
-
-		this.applyDraggingState(false);
-		this.clearDropTargetStates();
-
-		// Reset state
-		this.dragState.isDragging = false;
-		this.dragState.dragPreview = null;
-		this.dragState.draggedItems = [];
-	}
-
-	/**
-	 * Determine what items to drag (single or multiple selection)
-	 */
-	getDraggedItems(element) {
-		const selectedUploads = this.getSelectedUploads(element);
-		const primaryUploadId = element.dataset.uploadId;
-
-		// If we have multiple selections and primary is selected, drag all
-		if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
-			return selectedUploads;
-		}
-
-		// Otherwise, just drag the primary item
-		return [primaryUploadId];
-	}
-
-	/**
-	 * Apply/remove dragging visual state to items
-	 */
-	applyDraggingState(isDragging) {
-		this.dragState.draggedItems.forEach(uploadId => {
-			const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
-			if (element) {
-				element.classList.toggle('dragging', isDragging);
-			}
-		});
-	}
-
-	/**
-	 * Create drag preview element
-	 */
-	/**
-	 * Create drag preview element from template
-	 */
-	createDragPreview() {
-		const { draggedItems, sourceType } = this.dragState;
-
-		// Get the template
-		const template = window.getTemplate('dragPreview');
-		if (!template) {
-			console.error('Drag preview template not found');
-			return;
-		}
-
-		this.dragState.dragPreview = template;
-		const itemsContainer = template.querySelector('.drag-items');
-		const countBadge = template.querySelector('.drag-count');
-
-		// Set data attributes for CSS targeting
-		template.dataset.source = sourceType;
-
-		// Handle single vs multi-item
-		const itemCount = draggedItems.length;
-
-		if (itemCount > 1) {
-			// Multi-item: show count and stack up to 3 items
-			template.dataset.count = itemCount;
-			countBadge.dataset.count = itemCount;
-			countBadge.hidden = false;
-
-			const displayCount = Math.min(itemCount, 3);
-			for (let i = 0; i < displayCount; i++) {
-				const uploadId = draggedItems[i];
-				const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
-
-				if (uploadElement) {
-					const clonedItem = uploadElement.cloneNode(true);
-					clonedItem.dataset.uploadId = `${uploadId}-preview`;
-					// Remove interactive elements from clone
-					clonedItem.querySelectorAll('input, button, details').forEach(el => el.remove());
-					itemsContainer.appendChild(clonedItem);
-				}
-			}
-		} else {
-			// Single item: just clone it
-			const uploadElement = document.querySelector(`[data-upload-id="${draggedItems[0]}"]`);
-			if (uploadElement) {
-				const clonedItem = uploadElement.cloneNode(true);
-				clonedItem.dataset.uploadId = `${draggedItems[0]}-preview`;
-				// Remove interactive elements from clone
-				clonedItem.querySelectorAll('input, button, details').forEach(el => el.remove());
-				itemsContainer.appendChild(clonedItem);
-			}
-		}
-
-		// Add to DOM
-		document.body.appendChild(this.dragState.dragPreview);
-
-		// Position immediately at start position
-		this.updateDragPreview(this.dragState.startPosition);
-	}
-
-	/**
-	 * Update drag preview position
-	 */
-	updateDragPreview(position) {
-		if (!this.dragState.dragPreview) return;
-
-		const preview = this.dragState.dragPreview;
-
-		// Determine offset based on source type
-		let offset;
-		if (this.dragState.sourceType === 'touch') {
-			// For touch, offset up and to the left so finger doesn't cover preview
-			offset = this.dragState.isMultiDrag
-				? { x: -60, y: -80 }
-				: { x: -50, y: -60 };
-		} else {
-			// For mouse, smaller offset
-			offset = this.dragState.isMultiDrag
-				? { x: 15, y: 15 }
-				: { x: 10, y: 10 };
-		}
-
-		// Position the preview at the current pointer position with offset
-		preview.style.left = `${position.x + offset.x}px`;
-		preview.style.top = `${position.y + offset.y}px`;
-	}
-
-	/**
-	 * Update drop target highlighting
-	 */
-	updateDropTarget(elementUnderPointer) {
-		// Clear previous target
-		if (this.dragState.currentTarget) {
-			this.clearDropTargetState(this.dragState.currentTarget);
-		}
-
-		// Find valid drop target
-		const validTarget = this.findValidDropTarget(elementUnderPointer);
-
-		// Update state
-		this.dragState.currentTarget = elementUnderPointer;
-		this.dragState.validTarget = validTarget;
-
-		// Apply visual feedback
-		if (validTarget) {
-			this.applyDropTargetState(validTarget);
-
-			// Haptic feedback for touch
-			if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
-				const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
-				navigator.vibrate(pattern);
-			}
-		}
-	}
-
-	/**
-	 * Find valid drop target from element
-	 */
-	findValidDropTarget(element) {
-		const target = element?.closest('.item-grid.group, .empty-group, .item-grid.preview');
-		return target && this.getFieldIdFromElement(target) === this.dragState.fieldId ? target : null;
-	}
-
-	/**
-	 * Apply drop target visual state
-	 */
-	applyDropTargetState(target) {
-		target.classList.add('dragover');
-
-		if (this.dragState.isMultiDrag) {
-			target.classList.add('multi-drop');
-			target.setAttribute('data-item-count', this.dragState.draggedItems.length);
-		}
-	}
-
-	/**
-	 * Clear drop target state from element
-	 */
-	clearDropTargetState(target) {
-		target.classList.remove('dragover', 'multi-drop');
-		target.removeAttribute('data-item-count');
-	}
-
-	/**
-	 * Clear all drop target states
-	 */
-	clearDropTargetStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-
-
-	/**
-	 * Provide feedback for drag operations
-	 */
-	provideDragFeedback(type) {
-		const hapticPatterns = {
-			start: [50],
-			success: this.dragState.isMultiDrag ? [30, 20, 30] : [50],
-			error: [100, 50, 100],
-			warning: [50]
-		};
-
-		// Haptic feedback (vibration on supported devices)
-		if (navigator.vibrate && hapticPatterns[type]) {
-			navigator.vibrate(hapticPatterns[type]);
-		}
-
-		// Visual feedback
-		const feedback = document.createElement('div');
-		feedback.className = `drag-feedback ${type}`;
-		feedback.style.cssText = `
-		position: fixed;
-		top: 50%;
-		left: 50%;
-		transform: translate(-50%, -50%);
-		padding: 1rem 2rem;
-		background: var(--${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'warning'});
-		color: white;
-		border-radius: var(--radius);
-		z-index: 10001;
-		animation: feedbackPulse 0.3s ease;
-		pointer-events: none;
-	`;
-
-		const icons = {
-			start: '↕️',
-			success: '✓',
-			error: '✗',
-			warning: '⚠'
-		};
-
-		feedback.textContent = icons[type] || '';
-		document.body.appendChild(feedback);
-
-		setTimeout(() => {
-			feedback.style.animation = 'fadeOut 0.3s ease';
-			setTimeout(() => feedback.remove(), 300);
-		}, 500);
-	}
-
-	/**
-	 * Provide consistent feedback for different input methods
-	 */
-	provideFeedback(sourceType, feedbackType, data = {}) {
-		const hapticPatterns = {
-			success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
-			error: [100, 50, 100]
-		};
-
-		if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
-			navigator.vibrate(hapticPatterns[feedbackType]);
-		}
-	}
-
-	clearDragoverStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-	/*********
-	 *  DRAG HANDLERS
-	 ********/
-	handleDragEnter(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		// Only handle external files
-		if (e.dataTransfer.types.includes('Files')) {
-			e.preventDefault();
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer) {
-				uploadContainer.classList.add('dragover');
-			}
-		}
-	}
-	handleDragLeave(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
-			uploadContainer.classList.remove('dragover');
-		}
-	}
-	handleDragStart(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'drag',
-			startPosition: { x: e.clientX, y: e.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
-			e.dataTransfer.effectAllowed = 'move';
-		} else {
-			e.preventDefault();
-		}
-	}
-
-	handleDragOver(e) {
-		if (!this.dragState.isDragging) return;
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		e.preventDefault();
-		e.dataTransfer.dropEffect = 'move';
-
-		const elementUnderPointer = document.elementFromPoint(e.clientX, e.clientY);
-		this.updateDragOperation(
-			{ x: e.clientX, y: e.clientY },
-			elementUnderPointer
-		);
-	}
-
-	handleDrop(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		e.preventDefault();
-		this.clearDragoverStates();
-
-		// Handle external files (new uploads)
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer) {
-			const files = Array.from(e.dataTransfer.files);
-			if (files.length > 0) {
-				const fieldId = this.getFieldIdFromElement(uploadContainer);
-				if (fieldId) {
-					this.processFiles(fieldId, files);
-					this.a11y.announce(`${files.length} file(s) dropped for upload`);
-				}
-			}
-		}
-	}
-
-	handleDragEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		// Find the element under the final drop position
-		const elementUnderDrop = document.elementFromPoint(
-			this.dragState.currentPosition?.x || e.clientX,
-			this.dragState.currentPosition?.y || e.clientY
-		);
-
-		this.endDragOperation(elementUnderDrop);
-	}
-	/*********
-	 * TOUCH HANDLERS
-	 ********/
-	handleTouchStart(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-		if (this.isTouchOnFormElement(e.target)) {
-			return;
-		}
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const touch = e.touches[0];
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'touch',
-			startPosition: { x: touch.clientX, y: touch.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.preventDefault(); // Prevent scrolling
-		}
-	}
-
-	handleTouchMove(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.touches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.updateDragOperation(
-			{ x: touch.clientX, y: touch.clientY },
-			elementUnderTouch
-		);
-	}
-
-	handleTouchEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.changedTouches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.endDragOperation(elementUnderTouch);
-	}
-
-	handleTouchCancel(e) {
-		if (!this.dragState.isDragging) {
-			return;
-		}
-		if (this.dragState.isDragging) {
-			this.cleanupDragOperation();
-			this.a11y.announce('Drag cancelled');
-		}
-	}
-	/*******************************************************************************
-	 QUEUE INTEGRATION
-	 *******************************************************************************/
-	async submitUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Check if there are uploads to submit
-		const pendingUploads = Array.from(field.uploads || [])
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload &&
-				(upload.status === 'processed' ||
-					upload.status === 'processed-original'));
-
-		if (pendingUploads.length === 0) {
-			// this.notifications.add('No uploads ready to submit', 'warning');
-			return;
-		}
-
-		// Queue the uploads
-		try {
-			await this.queueUpload(fieldId);
-			// this.notifications.add(`Submitting ${pendingUploads.length} upload(s)`, 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'submitUploads',
-				fieldId
-			});
-			// this.notifications.add('Failed to submit uploads', 'error');
-		}
-	}
-	async retryUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		try {
-			// Reset status
-			this.updateUploadStatus(uploadId, 'received');
-
-			// If we have the processed file, skip to queuing
-			if (upload.processedFile) {
-				this.updateUploadStatus(uploadId, 'processed');
-				await this.queueUpload(upload.fieldId);
-			} else if (upload.originalFile) {
-				// Reprocess the file
-				const reprocessed = await this.processFile(upload.originalFile, field);
-				if (reprocessed) {
-					await this.queueUpload(upload.fieldId);
-				}
-			} else {
-				throw new Error('No file data available for retry');
-			}
-
-			// this.notifications.add('Retrying upload...', 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'retryUpload',
-				uploadId
-			});
-			// this.notifications.add('Failed to retry upload', 'error');
-		}
-	}
-
-	async queueUpload(fieldId) {
-		//Further cache it, or is it already cached at this point?
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const uploads = Array.from(field.uploads);
-		if (uploads.length === 0) {
-			return;
-		}
-
-		const data = this.prepareUploadData(field, uploads);
-		this.a11y.announce('Queuing for upload');
-		let img = (uploads.length === 1) ? 'image' : 'images';
-		const operation = {
-			endpoint: 'uploads',
-			method: 'POST',
-			data: data,
-			title: `Uploading ${uploads.length} ${img} to server...`,
-			popup: `Uploading ${uploads.length} ${img}...`,
-			canMerge: false,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			},
-			append: '_upload'
-		}
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (!upload) {
-					return;
-				}
-				upload.operationId = operationId;
-				this.updateUploadStatus(uploadId, 'queued');
-			});
-			field.operationId = operationId;
-
-			return operationId;
-		} catch (error) {
-			throw error;
-		} finally {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	prepareUploadData(field, uploads) {
-
-		const formData = new FormData();
-		formData.append('content', field.content);
-		formData.append('mode', field.mode);
-		formData.append('field_name', field.name);
-		formData.append('field_key', field.key);
-		formData.append('field_type', field.type);
-		formData.append('subtype', field.subtype);
-		formData.append('item_id', field.itemID);		//post, term, or user id
-		formData.append('context', field.context);	//post, term, or user
-		formData.append('destination', field.destination || 'meta'); //meta, post, post_group
-		let uploadMap = [];
-
-		const fieldGroups = this.getFieldGroups(field.key);
-		if (field.destination === 'post_group' && fieldGroups.length > 0) {
-			// User has created groups
-			let groups = [];
-			let titles = [];
-			let featuredImages = [];
-
-			fieldGroups.forEach(group => {
-				let groupUploadIndices = [];
-				let featuredIndex = null;
-
-				group.uploads.forEach(uploadId => {
-					let upload = this.uploads.get(uploadId);
-					if (upload) {
-						const fileToUpload = upload.processedFile || upload.originalFile;
-						if (fileToUpload) {
-							formData.append('files[]', fileToUpload);
-							const fileIndex = uploadMap.length;
-							uploadMap.push(upload.id);
-							groupUploadIndices.push(upload.id);
-
-							// Check if this is the featured image
-							const radioInput = upload.element?.querySelector('[name="featured"]');
-							if (radioInput?.checked) {
-								featuredIndex = upload.id;
-							}
-						}
-					}
-				});
-
-				groups.push(groupUploadIndices);
-				titles.push(group.title || '');
-				featuredImages.push(featuredIndex);
-			});
-
-			formData.append('groups', JSON.stringify(groups));
-			formData.append('group_titles', JSON.stringify(titles));
-			formData.append('featured_images', JSON.stringify(featuredImages));
-		} else {
-			// No groups - just append all files
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (upload) {
-					const fileToUpload = upload.processedFile || upload.originalFile;
-					if (fileToUpload) {
-						formData.append('files[]', fileToUpload);
-						uploadMap.push(upload.id);
-					}
-				}
-			});
-		}
-		formData.append('upload_ids', JSON.stringify(uploadMap));
-
-		// console.log('Final FormData:');
-		// for (let pair of formData.entries()) {
-		// 	console.log(pair[0], pair[1]);
-		// }
-
-		return formData;
-	}
-
-	getFieldGroups(fieldId) {
-		const groups = [];
-
-		this.groups.forEach((groupData, groupId) => {
-			if (groupData.fieldId === fieldId) {
-				const field = this.fields.get(fieldId);
-				const groupElement = field?.ui?.groups?.groups?.get(groupId);
-
-				groups.push({
-					id: groupId,
-					uploads: Array.from(groupData.uploads || new Set()),
-					meta: this.groupsMeta.get(groupId) || {},
-					element: groupElement || null
-				});
-			}
-		});
-
-		return groups;
-	}
-
-	/**
-	 * Build groups data from field state
-	 */
-	buildGroupsData(field, uploads) {
-		const groups = [];
-		const titles = [];
-		const uploadMap = [];
-
-		if (field.groups && field.groups.length > 0) {
-			// User has explicitly created groups
-			field.groups.forEach(group => {
-				const groupUploads = [];
-				group.uploads.forEach(uploadId => {
-					groupUploads.push(uploadId);
-					uploadMap.push(uploadId);
-				});
-				groups.push(groupUploads);
-				titles.push(group.title || '');
-			});
-		} else {
-			// No explicit groups - treat all as one group
-			const allUploads = [];
-			uploads.forEach(uploadId => {
-				allUploads.push(uploadId);
-				uploadMap.push(uploadId);
-			});
-			groups.push(allUploads);
-			titles.push('');
-		}
-
-		return { groups, titles, uploadMap };
-	}
-
-	async queueImageMeta(e) {
-		const upload = this.getUploadFromElement(element);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		// Collect meta data from the form
-		const metaContainer = element.closest('.upload-meta');
-		if (!metaContainer) return;
-
-		const metaData = {
-			title: metaContainer.querySelector('[name="title"]')?.value || '',
-			alt_text: metaContainer.querySelector('[name="alt_text"]')?.value || '',
-			caption: metaContainer.querySelector('[name="caption"]')?.value || '',
-			description: metaContainer.querySelector('[name="description"]')?.value || ''
-		};
-
-		// Update upload meta
-		upload.meta = { ...upload.meta, ...metaData };
-		this.uploads.set(upload.id, upload);
-
-		// Mark that we have meta changes
-		this.hasMetaChanges = true;
-
-		// Determine if upload has been sent to server
-		const isOnServer = upload.status === 'completed' && upload.attachmentId;
-
-		if (isOnServer) {
-			// Queue immediate update
-			await this.sendMetaUpdate(upload);
-		} else if (upload.operationId) {
-			// Wait for upload to complete, then send meta
-			this.queueDependentMetaUpdate(upload);
-		} else {
-			// Upload hasn't been queued yet, meta will be sent with initial upload
-			this.persistFieldState(field.key);
-		}
-	}
-
-	/**
-	 * Send meta update to server
-	 */
-	async sendMetaUpdate(upload) {
-		const formData = new FormData();
-		formData.append('attachment_id', upload.attachmentId);
-		formData.append('title', upload.meta.title);
-		formData.append('alt_text', upload.meta.alt_text);
-		formData.append('caption', upload.meta.caption);
-		formData.append('description', upload.meta.description);
-		//TODO:
-		// Send an array of attachment IDs with the changes, similar to the post editing logic
-		/**
-		 *  let data = {
-		 *  	items: {
-		 *      	uploadID: {
-		 *          	title: '',
-		 *          	alt: '',
-		 *          	caption: '',
-		 *         		depends_on: ''  <-- only necessary if uploadID is the generated upload_id
-		 *      	}
-		 *  	},
-		 *  	user: userID
-		 *  }
-		 *
-		 *  WHERE uploadID = attachment_id (if already uploaded) or our generated upload_id if the file hasn't been processed yet
-		 *
-		 */
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			data: formData,
-			title: `Updating metadata for ${upload.meta.originalName}`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
-
-		try {
-			await this.queue.addToQueue(operation);
-			// this.notifications.add('Metadata updated', 'success');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'sendMetaUpdate',
-				uploadId: upload.id
-			});
-		}
-	}
-
-	/**
-	 * Queue meta update that depends on upload completion
-	 */
-	queueDependentMetaUpdate(upload) {
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			dependencies: [upload.operationId],
-			data: () => {
-				// This function will be called when dependencies are resolved
-				const formData = new FormData();
-				formData.append('operation_id', upload.operationId);
-				formData.append('upload_id', upload.id);
-				formData.append('title', upload.meta.title);
-				formData.append('alt_text', upload.meta.alt_text);
-				formData.append('caption', upload.meta.caption);
-				formData.append('description', upload.meta.description);
-				return formData;
-			},
-			title: `Updating metadata after upload`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
-
-		this.queue.addToQueue(operation);
-	}
-	/*******************************************************************************
-	 IMAGE PROCESSING
-	*******************************************************************************/
-	async processFiles(fieldId, files) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Hide upload container, show group display
-		if (field.ui.field.dropZone) {
-			field.ui.field.dropZone.hidden = true;
-		}
-		if (field.ui.groups.display) {
-			field.ui.groups.display.hidden = false;
-		}
-
-		const totalFiles = files.length;
-		let processedCount = 0;
-
-		// Show initial progress
-		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-
-		// Initialize field uploads set if needed
-		if (!field.uploads) {
-			field.uploads = new Set();
-		}
-
-		// Process files
-		const processPromises = Array.from(files).map(async (file, index) => {
-			try {
-				// Create upload ID
-				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-				// Create upload data
-				const uploadData = {
-					id: uploadId,
-					fieldId: fieldId,
-					originalFile: file,
-					processedFile: null,
-					preview: null,
-					status: 'local_processing',
-					element: null,
-					location: null,
-					meta: {
-						originalName: file.name,
-						size: file.size,
-						type: file.type
-					}
-				};
-
-				// Create preview URL
-				uploadData.preview = URL.createObjectURL(file);
-
-				// Process the file (resize if image)
-				if (file.type.startsWith('image/')) {
-					uploadData.processedFile = await this.processImage(file, field.subtype);
-				} else {
-					uploadData.processedFile = file;
-				}
-
-				// Store blob data separately in IndexedDB
-				if (this.db) {
-					try {
-						await this.storeBlobData(uploadId, uploadData.processedFile || file);
-					} catch (error) {
-						console.warn('Failed to store blob data:', error);
-					}
-				}
-
-				// Create DOM element
-				const subtype = this.getSubtypeFromMime(file.type);
-				uploadData.element = this.createImageElement({
-					...uploadData,
-					subtype: subtype
-				}, field.destination === 'post_group');
-
-				// Show progress on the item
-				this.showUploadProgress(uploadId, true);
-				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
-
-				// Add to preview grid
-				if (field.ui.field.preview) {
-					field.ui.field.preview.appendChild(uploadData.element);
-					uploadData.location = field.ui.field.preview;
-				}
-
-				// Store upload
-				this.uploads.set(uploadId, uploadData);
-				field.uploads.add(uploadId);
-
-				// Update progress
-				processedCount++;
-				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
-				this.updateUploadItemProgress(uploadId, 100, 'processed');
-				uploadData.status = 'processed';
-
-				// Fade out item progress after a moment
-				setTimeout(() => {
-					this.showUploadProgress(uploadId, false);
-				}, 1000);
-
-				return uploadId;
-
-			} catch (error) {
-				console.error('Error processing file:', file.name, error);
-				processedCount++;
-				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
-				return null;
-			}
-		});
-
-		// Wait for all files to process
-		await Promise.all(processPromises);
-
-		this.updateFieldState(fieldId);
-		// Cache the state (now without DOM references)
-		await this.persistFieldState(fieldId);
-
-		// Queue for upload if in direct mode
-		if (field.mode === 'direct' && field.destination !== 'post_group') {
-			await this.queueUpload(fieldId);
-		}
-
-		// Lock uploads if max reached
-		this.maybeLockUploads(fieldId);
-	}
-
-	updateFieldState(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.ui.field.field) return;
-
-		const container = field.ui.field.field;
-		const uploadCount = field.uploads?.size || 0;
-		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
-
-		// Set data attributes for CSS targeting
-		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
-		container.dataset.uploadCount = uploadCount.toString();
-		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
-
-		// Update ARIA labels for accessibility
-		if (field.ui.field.preview) {
-			field.ui.field.preview.setAttribute('aria-label',
-				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
-			);
-		}
-	}
-
-	/**
-	 * Store file blob data in IndexedDB
-	 */
-	async storeBlobData(uploadId, file) {
-		if (!this.db) return;
-
-		const blobData = {
-			uploadId: uploadId,
-			data: file,
-			name: file.name,
-			type: file.type,
-			lastModified: file.lastModified,
-			timestamp: Date.now()
-		};
-
-		try {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').put(blobData);
-		} catch (error) {
-			console.error('Failed to store blob data:', error);
-			throw error;
-		}
-	}
-
-	/**
-	 * Show/hide progress indicator on individual upload items
-	 */
-	showUploadProgress(uploadId, show = true) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (progressEl) {
-			if (show) {
-				progressEl.style.removeProperty('animation');
-				progressEl.hidden = false;
-			} else {
-				progressEl.style.animation = 'fadeOut var(--transition-base)';
-				setTimeout(() => {
-					progressEl.hidden = true;
-				}, 300);
-			}
-		}
-	}
-
-	/**
-	 * Update individual upload progress bar
-	 */
-	updateUploadItemProgress(uploadId, percent, status = null) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (!progressEl) return;
-
-		const fill = progressEl.querySelector('.fill');
-		const details = progressEl.querySelector('.details');
-		const icon = progressEl.querySelector('.icon');
-
-		if (fill) {
-			fill.style.width = `${percent}%`;
-		}
-
-		if (status && details) {
-			details.textContent = this.getStatusText(status);
-		}
-
-		if (status && icon) {
-			icon.innerHTML = this.getStatusIcon(status).outerHTML;
-		}
-	}
-	checkFieldLimits(fieldId, additionalFiles) {
-		const field = this.fields.get(fieldId);
-		if (!field) return false;
-
-		const currentCount = field.uploads?.size || 0;
-		const totalCount = currentCount + additionalFiles;
-
-		if (totalCount > field.maxFiles) {
-			// this.notifications.add(
-			// 	`Cannot add ${additionalFiles} files. Max ${field.maxFiles} allowed, currently have ${currentCount}.`,
-			// 	'warning'
-			// );
-			return false;
-		}
-
-		return true;
-	}
-	generateUploadId() {
-		return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-	}
-	validateFile(file, field) {
-		// Type validation
-		if (!this.settings.allowedTypes.includes(file.type)) {
-			this.notify(`Invalid file type: ${file.type}`, 'error');
-			return false;
-		}
-
-		// Size validation
-		if (file.size > this.settings.maxFileSize) {
-			this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
-			return false;
-		}
-
-		return true;
-	}
-
-	formatBytes(bytes, decimals = 2) {
-		if (bytes === 0) return '0 Bytes';
-
-		const k = 1024;
-		const dm = decimals < 0 ? 0 : decimals;
-		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-
-		const i = Math.floor(Math.log(bytes) / Math.log(k));
-
-		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
-	}
-
-	shouldProcessClientSide(file, subtype) {
-		// Only process images client-side
-		if (subtype === 'image' && file.type.startsWith('image/')) {
-			return true;
-		}
-
-		// Videos and documents go straight to server
-		return false;
-	}
-
-	async processBatch(fieldId, files) {
-		const results = [];
-		const processingQueue = [];
-		const maxConcurrent = this.worker.settings.maxConcurrent;
-
-		let total = files.length;
-		let processedCount = 0;
-
-		// Show initial progress
-		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-		let field = this.fields.get(fieldId);
-		// Initialize field uploads set if needed
-		if (!field.uploads) {
-			field.uploads = new Set();
-		}
-
-
-		for (let i = 0; i < files.length; i++) {
-			this.showUploadProgress(uploadId, true);
-			this.updateUploadProgress(fieldId, i, total);
-			// Wait if we've reached max concurrent processing
-			if (processingQueue.length >= maxConcurrent) {
-				await Promise.race(processingQueue);
-			}
-
-			const processPromise = this.processFile(files[i], field)
-				.then(upload => {
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-
-					if (upload) results.push(upload);
-					return upload;
-				})
-				.catch(error => {
-					console.error(`Failed to process ${files[i].name}:`, error);
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-					return null;
-				});
-
-			processingQueue.push(processPromise);
-		}
-
-		// Wait for remaining files
-		await Promise.all(processingQueue);
-		return results;
-	}
-
-	async processFile(file, field, uploadId = null) {
-		if (!field || !file) {
-			console.error('Missing required parameters:', { file, field });
-			return null;
-		}
-
-		if (!this.shouldProcessClientSide(file, field.subtype)) {
-			return upload;
-		}
-
-		const id = uploadId || `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-		try {
-			// Create upload object
-			const upload = {
-				id,
-				fieldId: field.key,
-				originalFile: file,
-				processedFile: null,
-				preview: null,
-				status: 'local_processing',
-				element: null,
-				location: null,
-				groupId: null,
-				changes: {},
-				meta: {
-					originalName: file.name,
-					size: file.size,
-					type: file.type
-				}
-			};
-
-			// Create preview URL
-			upload.preview = URL.createObjectURL(file);
-
-			// Process the file
-			let processedFile = null;
-			let processingFailed = false;
-
-			if (file.type.startsWith('image/')) {
-				try {
-					processedFile = await this.processImage(file, id);
-				} catch (error) {
-					console.warn(`Image processing failed for ${file.name}, using original:`, error);
-					processingFailed = true;
-					processedFile = file;
-				}
-			} else {
-				processedFile = file; // Videos/documents use original
-			}
-
-			upload.processedFile = processedFile;
-			upload.processingFailed = processingFailed;
-
-			// Store in uploads map
-			this.uploads.set(id, upload);
-
-			// Add to field's uploads
-			if (!field.uploads) {
-				field.uploads = new Set();
-			}
-			field.uploads.add(id);
-
-			// Update status
-			this.updateUploadStatus(id, 'processed');
-
-			// Persist state
-			await this.persistFieldState(field.key);
-
-			// Announce to screen readers
-			const message = processingFailed
-				? `${file.name} added (original format)`
-				: `${file.name} processed and ready`;
-			this.a11y.announce(message);
-
-			return upload;
-
-		} catch (error) {
-			// Clean up failed upload
-			this.cleanupFailedUpload(id, field.key);
-
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'processFile',
-				uploadId: id,
-				fileName: file.name
-			});
-
-			return null;
-		}
-	}
-
-	async processImage(file, uploadId) {
-		const timeout = this.worker.settings.timeout;
-
-		return new Promise((resolve, reject) => {
-			let timeoutId;
-			let taskCompleted = false;
-
-			// Set timeout
-			timeoutId = setTimeout(() => {
-				if (!taskCompleted) {
-					taskCompleted = true;
-
-					// Remove from active tasks
-					this.worker.tasks.delete(uploadId);
-
-					// Maybe restart worker if configured
-					if (this.worker.settings.restartAfterTimeout) {
-						this.restartCompressionWorker();
-					}
-
-					reject(new Error(`Processing timeout for ${file.name}`));
-				}
-			}, timeout);
-
-			// Track this task
-			this.worker.tasks.set(uploadId, { file, timeoutId });
-
-			// Process image
-			this.handleProcess(file, uploadId)
-				.then(result => {
-					if (!taskCompleted) {
-						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) {
-		// Skip non-images
-		if (!file.type.startsWith('image/')) {
-			return file;
-		}
-
-		const maxDimension = this.getMaxDimension();
-		const quality = 0.85;
-
-		// Try worker first if available
-		if (this.shouldUseWorker(file)) {
-			try {
-				// Ensure worker is initialized
-				if (!this.worker.worker) {
-					this.initCompressionWorker();
-				}
-
-				if (this.worker.worker) {
-					return await this.processWithWorker(file, uploadId, maxDimension, quality);
-				}
-			} catch (error) {
-				console.warn('Worker processing failed, falling back to main thread:', error);
-			}
-		}
-
-		// Fallback to main thread
-		return await this.processOnMainThread(file, maxDimension, quality);
-	}
-
-	/**
-	 * Process image on main thread with better error handling
-	 */
-	async processOnMainThread(file, maxDimension, quality) {
-		return new Promise((resolve, reject) => {
-			const img = new Image();
-			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;
-				}
-				// Explicitly clean up canvas
-				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;
-
-					// Enhanced image smoothing
-					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 = URL.createObjectURL(file);
-				img.src = objectUrl;
-			} catch (error) {
-				cleanup();
-				reject(new Error(`Failed to create object URL: ${error.message}`));
-			}
-		});
-	}
-
-	/**
-	 * Get optimal output format
-	 */
-	getOptimalFormat(file) {
-		// Keep original format for certain types
-		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
-			return file.type;
-		}
-
-		// Use WebP if supported, otherwise JPEG
-		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
-	}
-
-	/**
-	 * Get optimal quality setting
-	 */
-	getOptimalQuality(file, requestedQuality) {
-		// Higher quality for smaller files
-		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
-		if (file.size < 2 * 1024 * 1024) return requestedQuality;
-
-		// Lower quality for very large files
-		return Math.min(requestedQuality, 0.8);
-	}
-
-	/**
-	 * Generate processed file name
-	 */
-	getProcessedFileName(originalFile, outputFormat) {
-		const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
-
-		const extensions = {
-			'image/webp': '.webp',
-			'image/jpeg': '.jpg',
-			'image/png': '.png',
-			'image/gif': '.gif'
-		};
-
-		return baseName + (extensions[outputFormat] || '.jpg');
-	}
-
-	/**
-	 * Get maximum dimension based on device capabilities
-	 */
-	getMaxDimension() {
-		const screenWidth = window.screen.width;
-		const devicePixelRatio = window.devicePixelRatio || 1;
-
-		// Scale based on device capabilities
-		if (screenWidth * devicePixelRatio > 2560) return 2400;
-		if (screenWidth * devicePixelRatio > 1920) return 1920;
-		return 1200;
-	}
-
-	/**
-	 * Determine if we should use Web Worker
-	 */
-	shouldUseWorker(file) {
-		// Use worker for large files or when available
-		return this.worker.worker &&
-			file.size > 1024 * 1024 && // > 1MB
-			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;
-			}
-
-			// Create unique message ID for this task
-			const messageId = `${uploadId}_${Date.now()}`;
-
-			// Handler for this specific message
-			const messageHandler = (e) => {
-				if (e.data.messageId !== messageId) return;
-
-				// Remove handler
-				this.worker.worker.removeEventListener('message', messageHandler);
-				this.worker.worker.removeEventListener('error', errorHandler);
-
-				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}`));
-			};
-
-			// Add handlers
-			this.worker.worker.addEventListener('message', messageHandler);
-			this.worker.worker.addEventListener('error', errorHandler);
-
-			// Send message to worker
-			this.worker.worker.postMessage({
-				messageId,
-				file,
-				maxDimension,
-				quality,
-				outputFormat: this.getOptimalFormat(file)
-			});
-		});
-	}
-
-	/**
-	 * Restart compression worker
-	 */
-	restartCompressionWorker() {
-		// Terminate existing worker
-		if (this.worker.worker) {
-			this.worker.worker.terminate();
-			this.worker.worker = null;
-		}
-
-		// Clear active tasks
-		this.worker.tasks.clear();
-
-		// Check restart limit
-		if (this.worker.restart.count >= this.worker.restart.max) {
-			console.error('Max worker restarts reached, disabling worker');
-			return;
-		}
-
-		this.worker.restart.count++;
-
-		// Reinitialize
-		this.initCompressionWorker();
-	}
-
-	/**
-	 * Initialize Web Worker for image compression
-	 */
-	initCompressionWorker() {
-		if (this.worker.worker || typeof Worker === 'undefined') return;
-
-		try {
-			const workerScript = `
-            self.onmessage = async function(e) {
-                const { messageId, file, maxDimension, quality, outputFormat } = e.data;
-
-                try {
-                    // Create ImageBitmap from file
-                    const bitmap = await createImageBitmap(file);
-
-                    // Calculate dimensions
-                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
-                    const width = Math.round(bitmap.width * scale);
-                    const height = Math.round(bitmap.height * scale);
-
-                    // Create OffscreenCanvas
-                    const canvas = new OffscreenCanvas(width, height);
-                    const ctx = canvas.getContext('2d');
-
-                    // Draw and resize
-                    ctx.imageSmoothingEnabled = true;
-                    ctx.imageSmoothingQuality = 'high';
-                    ctx.drawImage(bitmap, 0, 0, width, height);
-
-                    // Clean up bitmap
-                    bitmap.close();
-
-                    // Convert to blob
-                    const blob = await canvas.convertToBlob({
-                        type: outputFormat,
-                        quality: quality
-                    });
-
-                    self.postMessage({
-                        messageId,
-                        success: true,
-                        blob: blob,
-                        format: outputFormat
-                    });
-
-                } catch (error) {
-                    self.postMessage({
-                        messageId,
-                        success: false,
-                        error: error.message
-                    });
-                }
-            };
-        `;
-
-			const blob = new Blob([workerScript], { type: 'application/javascript' });
-			this.worker.worker = new Worker(URL.createObjectURL(blob));
-
-		} catch (error) {
-			console.warn('Failed to initialize compression worker:', error);
-			this.worker.worker = null;
-		}
-	}
-
-	/**
-	 * Calculate optimal dimensions with aspect ratio preservation
-	 */
-	calculateOptimalDimensions(img, maxDimension) {
-		let { width, height } = img;
-
-		// Don't upscale
-		if (width <= maxDimension && height <= maxDimension) {
-			return { width, height };
-		}
-
-		// Calculate scale factor
-		const scale = Math.min(maxDimension / width, maxDimension / height);
-
-		return {
-			width: Math.round(width * scale),
-			height: Math.round(height * scale)
-		};
-	}
-
-
-	/**
-	 * Check WebP support
-	 */
-	supportsWebP() {
-		const canvas = document.createElement('canvas');
-		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
-	}
-
-	/**
-	 * Clean up failed upload
-	 */
-	cleanupFailedUpload(uploadId, fieldId) {
-		const field = this.fields.get(fieldId);
-		if (field?.uploads) {
-			field.uploads.delete(uploadId);
-		}
-
-		const upload = this.uploads.get(uploadId);
-		if (upload) {
-			// Clean up preview URL
-			if (upload.preview?.startsWith('blob:')) {
-				URL.revokeObjectURL(upload.preview);
-			}
-
-			// Remove element
-			upload.element?.remove();
-
-			// Remove from uploads
-			this.uploads.delete(uploadId);
-		}
-
-		// Remove from active tasks
-		this.worker.tasks.delete(uploadId);
-	}
-	/*******************************************************************************
-	 UI FUNCTIONALITY
-	*******************************************************************************/
-	/**
-	 * Update upload status correctly
-	 */
-	updateUploadStatus(uploadId, status) {
-		let upload = this.uploads.get(uploadId);
-		if(!upload) {
-			return;
-		}
-		upload.status = status;
-
-		this.updateImageUI(upload.id);
-		this.persistFieldState(upload.fieldId);
-	}
-	updateImageUI(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload?.element) return;
-
-
-		const progressEl = upload.element.querySelector('.progress');
-		const itemEl = upload.element;
-
-		// Update status class on item for CSS styling
-		if (itemEl) {
-			itemEl.className = itemEl.className.replace(/status-[\w-]+/g, '');
-			itemEl.classList.add(`status-${upload.status}`);
-		}
-
-		if (progressEl) {
-			let icon = this.getStatusIcon(upload.status);
-			let message = this.getStatusText(upload.status);
-			let progress = this.getStatusProgress(upload.status);
-
-			const fill = progressEl.querySelector('.fill');
-			const itemIcon = progressEl.querySelector('span.icon');
-			const itemMessage = progressEl.querySelector('span.details');
-
-			if (fill) {
-				fill.style.width = `${progress}%`;
-			}
-			if (itemMessage) itemMessage.textContent = message;
-			if (itemIcon) {
-				window.removeChildren(itemIcon);
-				itemIcon.append(icon);
-			}
-
-			if (upload.status === 'completed') {
-				setTimeout(() => {
-					if (progressEl) {
-						window.fade(progressEl, false);
-					}
-				}, 1000);
-			}
-		}
-	}
-	/**
-	 * Hide the uploader drop zone if we have reached our limit
-	 */
-	maybeLockUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		if (field.ui.field.dropZone) {
-			const hasUploads = field.uploads && field.uploads.size > 0;
-			const atMaxFiles = field.uploads && field.uploads.size >= field.maxFiles;
-
-			// Hide if we have uploads OR if we're at max files
-			field.ui.field.dropZone.hidden = hasUploads || atMaxFiles;
-		}
-	}
-	createImageElement(upload, draggable = false) {
-		let image = window.getTemplate('uploadItem');
-		if (!image) {
-			console.error('Image template not found');
-			return;
-		}
-		image.dataset.uploadId = upload.id;
-		if (upload.originalFile) {
-			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
-		}
-
-
-		image.querySelector('[name="featured"]').value = upload.id;
-		let [
-			featured,
-			img,
-			video,
-			preview,
-			details
-		] = [
-			image.querySelector('[name="featured"]'),
-			image.querySelector('img'),
-			image.querySelector('video'),
-			image.querySelector('label > span'),
-			image.querySelector('details')
-		];
-		[
-			featured.value,
-			img.src,
-			img.alt
-		] = [
-			upload.id,
-			upload.preview,
-			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
-		];
-
-		switch (image.dataset.subtype) {
-			case 'image':
-				[
-					img.src,
-					img.alt
-				] = [
-					upload.preview,
-					upload.originalFile?.name ?? upload.meta?.originalName?? ''
-				];
-				video.remove();
-				preview.remove();
-				break;
-			case 'video':
-				video.src = upload.preview;
-				img.remove();
-				preview.remove();
-				break;
-			case 'document':
-				const fileName = upload.originalFile?.name ?? upload.meta?.originalName ?? '';
-				const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
-				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;
-		}
-		if (details) {
-			let template = window.getTemplate('uploadMeta');
-			if (template){
-				details.append(template);
-			}
-		}
-		image.draggable = draggable;
-
-		// Update input IDs safely
-		image.querySelectorAll('input').forEach(input => {
-			let id = input.id;
-			if (id) {
-				let newId = id + upload.id;
-				let label = input.parentNode.querySelector(`label[for="${id}"]`);
-				input.id = newId;
-				if (label) {
-					label.htmlFor = newId;
-				}
-			}
-		});
-
-		return image;
-	}
-
-
-	getSubtypeFromMime(mimeType) {
-		if (mimeType.startsWith('image/')) return 'image';
-		if (mimeType.startsWith('video/')) return 'video';
-		return 'document';
-	}
-
-	updateUploadProgress(fieldId, current, total, message) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		let progressBar = field.ui.field.progress.progress;
-
-		// Create progress bar if it doesn't exist
-		if (!progressBar) {
-			progressBar = window.getTemplate('imageProgress');
-
-			if (!progressBar) {
-				console.warn('Progress bar template not found');
-				return;
-			}
-
-			// Insert after drop zone or at top of container
-			const container = field.ui.field.field;
-			const insertAfter = field.ui.field.dropZone;
-
-			if (insertAfter) {
-				insertAfter.insertAdjacentElement('afterend', progressBar);
-			} else if (container) {
-				container.prepend(progressBar);
-			}
-
-			// Update the field UI reference to match actual structure
-			if (!field.ui.field.progress) {
-				field.ui.field.progress = {};
-			}
-			field.ui.field.progress = {
-				progress: progressBar,
-				bar: progressBar.querySelector('.bar'),
-				fill: progressBar.querySelector('.fill'),
-				details: progressBar.querySelector('.details'),
-				text: progressBar.querySelector('.details .text'),
-				count: progressBar.querySelector('.details .count')
-			};
-		}
-
-
-		progressBar.hidden = false;
-		progressBar.style.display = 'flex';
-		progressBar.style.animation = 'none';
-		progressBar.style.opacity = '1';
-
-		// Update progress bar
-		const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
-		const progressFill = field.ui.field.progress.fill;
-		const progressText = field.ui.field.progress.text;
-		const progressCount = field.ui.field.progress.count;
-
-		if (progressFill) {
-			progressFill.style.width = `${progressPercent}%`;
-		}
-
-		if (progressText) {
-			progressText.textContent = message;
-		}
-
-		if (progressCount) {
-			progressCount.textContent = `${current}/${total}`;
-		}
-
-		// Hide when complete
-		if (current >= total) {
-			setTimeout(() => {
-				progressBar.style.animation = 'fadeOut var(--transition-base)';
-				setTimeout(() => {
-					progressBar.hidden = true;
-					progressBar.style.display = 'none';
-				}, 300);
-			}, 1000);
-		}
-	}
-
-	hideUploadProgress(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const progressBar = field.ui.field.progress.progress;
-		if (progressBar) {
-			window.fade(progressBar, false);
-		}
-	}
-	/*******************************************************************************
-	 INDEXEDDB CACHE FUNCTIONALITY
-	*******************************************************************************/
-	async initDB() {
-		if (!('indexedDB' in window)) return;
-
-		const request = indexedDB.open(`jvb_uploads_db`, 1);
-
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-			if (!db.objectStoreNames.contains('fieldStates')) {
-				const store = db.createObjectStore('fieldStates', { keyPath: 'fieldId' });
-				store.createIndex('timestamp', 'timestamp', { unique: false });
-				store.createIndex('content', 'content', { unique: false });
-				store.createIndex('itemId', 'itemId', { unique: false });
-			}
-
-			// Blob storage remains separate for performance
-			if (!db.objectStoreNames.contains('uploadBlobs')) {
-				db.createObjectStore('uploadBlobs', { keyPath: 'uploadId' });
-			}
-		};
-
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFields();
-			this.checkPendingUploads();
-		};
-
-		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
-		};
-	}
-
-	async loadFields() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readonly');
-			const fieldStates = tx.objectStore('fieldStates');
-			const blobStore = tx.objectStore('uploadBlobs');
-			const request = fieldStates.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(field => {
-					let uploads = field.uploads;
-					let uploadIds = uploads.map(upload => upload.id);
-					field.uploads = new Set(uploadIds);
-					this.fields.set(field.key, field);
-					uploads.forEach(upload => {
-						this.uploads.set(upload.id, upload);
-					});
-				});
-				this.notify('uploads-loaded', { items: Array.from(this.uploads.values()) });
-				resolve();
-			};
-
-			const blobRequest = blobStore.getAll();
-
-			blobRequest.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					this.uploadBlobs.set(item.id, item);
-				});
-				this.notify('blobs-loaded', { items: Array.from(this.uploadBlobs.values()) });
-				resolve();
-			};
-		});
-	}
-
-	getUpload(uploadId) {
-		return this.uploads.get(uploadId);
-	}
-
-	updateFieldStatus(fieldId, status) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		field.uploads.forEach(upload => {
-			this.updateUploadStatus(upload, status);
-		});
-
-		// Update UI based on status
-		const container = field.ui.field.field;
-		if (container) {
-			container.dataset.uploadStatus = status;
-
-			// Show/hide relevant UI elements
-			const submitBtn = container.querySelector('.submit-uploads');
-			if (submitBtn) {
-				submitBtn.disabled = status === 'uploading' || status === 'processing';
-			}
-		}
-	}
-
-	/**
-	 * Handle successful upload completion
-	 */
-	handleUploadComplete(operation) {
-		const response = operation.response;
-		if (!response?.uploads) return;
-
-		response.uploads.forEach(serverUpload => {
-			const upload = this.uploads.get(serverUpload.upload_id);
-			if (upload) {
-				upload.attachmentId = serverUpload.attachment_id;
-				this.updateUploadStatus(serverUpload.upload_id, 'completed');
-				this.uploads.set(upload.id, upload);
-
-				// **ADD: Cleanup after successful upload**
-				this.clearUpload(upload.id);
-			}
-		});
-
-		const fieldKey = operation.data.get('field_key');
-		if (fieldKey) {
-			// **ADD: Clear field cache after all uploads complete**
-			const field = this.fields.get(fieldKey);
-			const allComplete = Array.from(field.uploads).every(id => {
-				const upload = this.uploads.get(id);
-				return upload?.status === 'completed';
-			});
-
-			if (allComplete) {
-				this.clearField(fieldKey);
-			}
-		}
-	}
-
-	/**
-	 * Store upload with DataStore integration
-	 */
-	async setUpload(fieldId, file, uploadId = null) {
-		if (!uploadId) {
-			uploadId = this.generateUploadId();
-		}
-		const upload = {
-			id: uploadId,
-			fieldId: fieldId,
-			groupId: null,
-			originalFile: file,
-			processedFile: null,
-			status: 'received',
-			progress: { percent: 0, message: 'Received...' },
-			preview: URL.createObjectURL(file),
-			createdAt: Date.now(),
-			meta: {
-				title: '',
-				alt_text: '',
-				caption: '',
-				originalName: file.name,
-				originalType: file.type,
-				originalSize: file.size
-			},
-			changes: {}
-		};
-
-		// Add to field
-		const field = this.fields.get(fieldId);
-		if (!field) {
-			console.error(`Field ${fieldId} not found`);
-			return null;
-		}
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadId);
-
-		upload.element = this.createImageElement(upload, field.type==='groupable');
-		upload.ui = window.uiFromSelectors(this.selectors.item, upload.element);
-
-		// Store in memory
-		this.uploads.set(uploadId, upload);
-		this.updateImageUI(uploadId);
-
-		// Persist to DataStore
-		await this.persistFieldState(fieldId);
-
-		return upload;
-	}
-
-	/**
-	 * Get uploads for a field, optionally cleaned for storage
-	 * @param {string} fieldId
-	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
-	 * @returns {Array}
-	 */
-	getFieldUploads(fieldId, clean = false) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.uploads) return [];
-
-		return Array.from(field.uploads)
-			.map(uploadId => {
-				const upload = this.uploads.get(uploadId);
-				if (!upload) return null;
-
-				if (clean) {
-					// Return cleaned version without DOM references
-					return {
-						id: upload.id,
-						fieldId: upload.fieldId,
-						status: upload.status,
-						preview: upload.preview,
-						attachmentId: upload.attachmentId,
-						operationId: upload.operationId,
-						groupId: upload.groupId || null,
-						meta: {
-							originalName: upload.meta?.originalName || upload.originalFile?.name,
-							size: upload.meta?.size || upload.originalFile?.size,
-							type: upload.meta?.type || upload.originalFile?.type,
-							title: upload.meta?.title,
-							alt: upload.meta?.alt,
-							caption: upload.meta?.caption
-						}
-					};
-				}
-
-				// Return full upload object
-				return upload;
-			})
-			.filter(Boolean);
-	}
-
-	/**
-	 * Persist upload to DataStore
-	 */
-	async persistFieldState(fieldId) {
-		if (!this.db) return;
-
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Create clean field config
-		const { ui, ...cleanConfig } = field;
-
-		const fieldState = {
-			fieldId: fieldId,
-			timestamp: Date.now(),
-
-			config: {
-				...cleanConfig,
-				fieldName: field.name,
-				dataField: field.ui?.field?.field?.dataset?.field
-			},
-
-			// Recovery context with normalized URL
-			context: {
-				url: this.normalizeUrl(window.location.href),
-				fullUrl: window.location.href, // Keep for reference
-				modalType: this.getModalType(field),
-				formId: field.formId,
-				// **FIX**: Store additional identifiers
-				fieldSelector: `.field.upload[data-field="${field.name}"]`
-			},
-
-			// Uploads (cleaned of DOM references and blob URLs)
-			uploads: this.getFieldUploads(fieldId, true).map(upload => {
-				// **FIX**: Don't store blob URLs as they become invalid
-				const { preview, element, location, ...cleanUpload } = upload;
-				return cleanUpload;
-			}),
-
-			// Groups structure
-			groups: Array.from(this.groups.entries())
-				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
-				.map(([id, data]) => ({
-					id: data.id,
-					uploads: Array.from(data.uploads),
-					meta: data.meta || {},
-					changes: data.changes || {}
-				}))
-		};
-
-		try {
-			const tx = this.db.transaction(['fieldStates'], 'readwrite');
-			await tx.objectStore('fieldStates').put(fieldState);
-		} catch (error) {
-			console.error('Failed to persist field state:', error);
-		}
-	}
-
-	normalizeUrl(url) {
-		try {
-			const urlObj = new URL(url);
-			// Return just the origin + pathname (no query string or hash)
-			return urlObj.origin + urlObj.pathname;
-		} catch (e) {
-			return url;
-		}
-	}
-	/*******************************************************************************
-	 RESTORE FUNCTIONALITY
-	*******************************************************************************/
-	async checkPendingUploads() {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const fieldStore = tx.objectStore('fieldStates');
-
-		const allFieldStates = await new Promise(resolve => {
-			const request = fieldStore.getAll();
-			request.onsuccess = () => resolve(request.result);
-		});
-
-
-		allFieldStates.forEach(field => {
-			console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
-			field.uploads.forEach((upload, idx) => {
-				console.log(`  Upload ${idx}:`, {
-					id: upload.id,
-					status: upload.status,
-					operationId: upload.operationId,
-					hasOperationId: !!upload.operationId
-				});
-			});
-		});
-
-		// Filter for pending uploads (not yet sent to server)
-		const pendingFields = allFieldStates.filter(field =>
-			field.uploads.some(upload =>
-				// If no operationId, it hasn't been sent to server yet
-				!upload.operationId &&
-				// And it's been processed locally
-				(upload.status === 'completed' ||
-					upload.status === 'processed' ||
-					upload.status === 'local_processing' ||
-					upload.status === 'processed-original')
-			)
-		);
-
-		console.log('Pending Fields: ', pendingFields);
-
-		if (pendingFields.length === 0) return;
-
-		// Show recovery notification
-		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 (const upload of field.uploads) {
-
-				let uploadItem = window.getTemplate('uploadItem');
-				if (!uploadItem) continue;
-			//
-			// 	const imgEl = uploadItem.querySelector('img');
-			// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
-			//
-				const blobData = await this.getBlobData(upload.id);
-
-
-				if (blobData) {
-					try {
-						// Create new blob URL from stored data
-						const blob = new Blob([blobData.data], { type: blobData.type });
-						const previewUrl = URL.createObjectURL(blob);
-
-						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.config.key;
-
-						let subtype = this.getSubtypeFromMime(blobData.type);
-						uploadItem.dataset.subtype = subtype;
-						switch (subtype) {
-							case 'image':
-								[
-									img.src,
-									img.alt
-								] = [
-									previewUrl,
-									upload.originalFile?.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 cleanupStoredRestoration() {
-		if (!this.db) return;
-
-		const notification = document.querySelector('dialog.restore-uploads');
-		if (!notification) return;
-
-		// Get all upload IDs from the notification
-		const items = notification.querySelectorAll('[data-upload-id]');
-		const uploadIds = Array.from(items).map(item => item.dataset.uploadId);
-
-		// Clean up blob URLs in the notification
-		this.cleanupRestoreNotificationUrls(notification);
-
-		// **Delete blob data from IndexedDB**
-		if (uploadIds.length > 0) {
-			const tx = this.db.transaction(['uploadBlobs', 'fieldStates'], 'readwrite');
-
-			// Delete all blob data
-			uploadIds.forEach(uploadId => {
-				tx.objectStore('uploadBlobs').delete(uploadId);
-			});
-
-			// Also delete field states
-			const fieldIds = Array.from(items).map(item => item.dataset.fieldId);
-			const uniqueFieldIds = [...new Set(fieldIds)];
-
-			uniqueFieldIds.forEach(fieldId => {
-				if (fieldId) {
-					tx.objectStore('fieldStates').delete(fieldId);
-				}
-			});
-
-			await tx.complete;
-		}
-	}
-
-	cleanupRestoreNotificationUrls(notification) {
-		if (!notification) return;
-
-		// Find all elements with preview URLs
-		const items = notification.querySelectorAll('[data-preview-url]');
-		items.forEach(item => {
-			const url = item.dataset.previewUrl;
-			if (url && url.startsWith('blob:')) {
-				URL.revokeObjectURL(url);
-				delete item.dataset.previewUrl;
-			}
-		});
-	}
-
-	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) {
-		// Group by field
-		const byField = new Map();
-		selectedUploads.forEach(item => {
-			if (!byField.has(item.fieldId)) {
-				byField.set(item.fieldId, []);
-			}
-			byField.get(item.fieldId).push(item.uploadId);
-		});
-
-		// Get full field states from IndexedDB
-		if (!this.db) {
-			// this.notifications.add('Cannot restore: Database not available', 'error');
-			return;
-		}
-
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const store = tx.objectStore('fieldStates');
-
-		for (const [fieldId, uploadIds] of byField.entries()) {
-			const request = store.get(fieldId);
-			const fieldState = await new Promise(resolve => {
-				request.onsuccess = () => resolve(request.result);
-				request.onerror = () => resolve(null);
-			});
-
-			if (fieldState) {
-				// Filter to only selected uploads
-				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
-				await this.restoreField(fieldState);
-			}
-		}
-
-		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
-	}
-
-	async restoreField(fieldState) {
-		const { config, context, uploads, groups } = 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.fields.has(fieldKey)) {
-			fieldKey = this.registerUploader(fieldElement, config);
-		}
-
-		const field = this.fields.get(fieldKey);
-		if (!field) {
-			console.error('Failed to register field for restoration');
-			return;
-		}
-
-		if (!field.ui.groups) {
-			field.ui.groups = {};
-		}
-		if (!field.ui.groups.groups) {
-			field.ui.groups.groups = new Map();
-		}
-
-		// Make sure we have the container and empty group references
-		if (!field.ui.groups.container) {
-			field.ui.groups.container = fieldElement.querySelector('.item-grid.groups');
-		}
-		if (!field.ui.groups.empty) {
-			field.ui.groups.empty = fieldElement.querySelector('.empty-group');
-		}
-		let display = fieldElement.querySelector('.group-display');
-		if (display) {
-			display.hidden = false;
-		}
-
-		// Restore uploads
-		for (const uploadData of uploads) {
-			await this.restoreUpload(field, uploadData);
-		}
-
-		// Restore groups
-		if (groups && groups.length > 0) {
-			await this.restoreGroups(field, groups, uploads);
-		}
-
-		// Update UI
-		this.updateFieldState(fieldKey);
-		this.maybeLockUploads(fieldKey);
-
-		await this.persistFieldState(fieldKey);
-
-		// Queue for upload if needed (should not happen for post_group)
-		if (config.mode === 'direct' && config.destination !== 'post_group') {
-			await this.queueUpload(fieldKey);
-		}
-	}
-
-	async restoreUpload(field, uploadData) {
-		// Try to get blob data from IndexedDB
-		const blobData = await this.getBlobData(uploadData.id);
-
-		if (blobData) {
-			const file = blobData.data instanceof File
-				? blobData.data
-				: new File(
-					[blobData.data],
-					blobData.name,
-					{ type: blobData.type, lastModified: blobData.lastModified }
-				);
-
-			uploadData.originalFile = file;
-			uploadData.processedFile = file;
-			uploadData.preview = URL.createObjectURL(file);
-		} else {
-			console.warn('Blob data not found for upload:', uploadData.id);
-			return; // Skip this upload if we can't restore the file
-		}
-
-		// Add to field
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadData.id);
-
-		// Recreate DOM element
-		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
-		uploadData.element = this.createImageElement({
-			...uploadData,
-			subtype: subtype
-		}, field.destination === 'post_group');
-
-		// Restore to correct location
-		let location;
-		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
-			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
-		} else {
-			location = field.ui.field.preview;
-		}
-
-		if (location) {
-			location.appendChild(uploadData.element);
-			uploadData.location = location;
-		}
-
-		// Store in memory
-		this.uploads.set(uploadData.id, uploadData);
-	}
-
-	async restoreFieldStates(fieldStates) {
-		// Group by URL
-		const byUrl = new Map();
-		fieldStates.forEach(field => {
-			if (!byUrl.has(field.context.url)) {
-				byUrl.set(field.context.url, []);
-			}
-			byUrl.get(field.context.url).push(field);
-		});
-
-		// If all on current page, restore directly
-		if (byUrl.size === 1 && byUrl.has(window.location.href)) {
-			for (const fieldState of fieldStates) {
-				await this.restoreField(fieldState);
-			}
-			// this.notifications.add(`Restored ${fieldStates.length} field(s)`, 'success');
-		} else {
-			// Store intent to restore and navigate
-			sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(fieldStates));
-
-			// Navigate to first URL
-			const firstUrl = byUrl.keys().next().value;
-			if (window.location.href !== firstUrl) {
-				window.location.href = firstUrl;
-			}
-		}
-	}
-
-	async restoreGroups(field, groups, uploads) {
-		// Ensure the groups.groups Map exists
-		if (!field.ui.groups.groups) {
-			field.ui.groups.groups = new Map();
-		}
-
-		for (const groupData of groups) {
-			// Create group element
-			const groupElement = this.createGroupElement(groupData.id, field.key);
-
-			// Store in field UI Map
-			field.ui.groups.groups.set(groupData.id, groupElement);
-
-			// Insert into DOM
-			if (field.ui.groups.container && field.ui.groups.empty) {
-				field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-			} else if (field.ui.groups.container) {
-				field.ui.groups.container.appendChild(groupElement);
-			}
-
-			this.groups.set(groupData.id, {
-				id: groupData.id,
-				fieldId: field.key,
-				element: groupElement,
-				uploads: new Set(groupData.uploads), // FIXED: was groupData.uploadIds
-				meta: groupData.meta || {},
-				changes: groupData.changes || {}
-			});
-
-			// Move uploads to group
-			groupData.uploads.forEach(uploadId => {
-				const upload = uploads.find(u => u.id === uploadId);
-				if (upload && upload.element) {
-					const groupGrid = groupElement.querySelector('.item-grid');
-					if (groupGrid) {
-						groupGrid.appendChild(upload.element);
-						upload.location = groupGrid;
-						upload.groupId = groupData.id;
-					}
-				}
-			});
-		}
-	}
-
-	async getBlobData(uploadId) {
-		if (!this.db) return null;
-
-		const tx = this.db.transaction(['uploadBlobs'], 'readonly');
-		const request = tx.objectStore('uploadBlobs').get(uploadId);
-
-		return new Promise(resolve => {
-			request.onsuccess = () => resolve(request.result);
-			request.onerror = () => resolve(null);
-		});
-	}
-
-	async openModalForRestore(context) {
-		const { modalType, formId } = context;
-
-		// Find and click the appropriate button to open the modal
-		let trigger = null;
-
-		switch(modalType) {
-			case 'create':
-				trigger = document.querySelector('[data-action="create"]');
-				break;
-			case 'edit':
-				// Need to find the specific edit button
-				trigger = document.querySelector(`[data-action="edit"][data-id="${context.itemId}"]`);
-				break;
-			case 'bulkEdit':
-				trigger = document.querySelector('[data-action="bulk-edit"]');
-				break;
-		}
-
-		if (trigger) {
-			trigger.click();
-
-			// Wait for modal to open
-			await new Promise(resolve => setTimeout(resolve, 300));
-		}
-	}
-	/*******************************************************************************
-	 GROUP FUNCTIONALITY
-	 Includes selection, dragging, and grouping logic
-	*******************************************************************************/
-	/**
-	 *
-	 * @param {string} uploadId as defined by setUpload
-	 * @param {HTMLElement|null} target The target location
-	 * @param {boolean} persist whethet to cache this change
-	 */
-	addImageToGroup(uploadId, target = null, persist = true) {
-		let upload = this.getUpload(uploadId);
-		if(!upload) {
-			return;
-		}
-		let field = this.fields.get(upload.fieldId);
-		if (!field) {
-			return;
-		}
-
-		//Already in the Preview Grid, or already in the group we're moving to
-		if ((!target && upload.location === field.ui.field.preview) || target === upload.location) {
-			return;
-		}
-
-		// Remove from previous location
-		if (upload.location) {
-			let groupId = upload.location.dataset.groupId;
-			if (groupId) {
-				let group = this.groups.get(groupId);
-				if (group && group.uploads) {
-					group.uploads.delete(uploadId);
-
-					if (group.uploads.size === 0) {
-						this.removeGroup(groupId);
-					}
-				}
-			}
-		}
-
-		const checkbox = upload.element.querySelector('[name*="select-item"]');
-		if (checkbox) {
-			checkbox.checked = false;
-		}
-
-		upload.element.querySelector('[name="featured"]').hidden = !target;
-
-		//If no target, it's going to the preview grid
-		if (!target) {
-			target = field.ui.field.preview;
-		} else if (!target.classList.contains('item-grid') || !target.classList.contains('preview')) {
-			// It's a group target
-			let groupId = target.dataset.groupId;
-			let group = this.groups.get(groupId);
-			if (!group) {
-				group = this.createGroup(upload.fieldId);
-				target = group.grid;
-			}
-			if (group) {
-				group.uploads.add(uploadId);
-			}
-		}
-
-		upload.location = target;
-		target.append(upload.element);
-
-		if (persist) {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	addSelectionToGroup(target) {
-		let field = this.getFieldFromElement(target);
-		if (!field) {
-			return;
-		}
-		let currentSelection = this.getCurrentSelection(field.key);
-		if (currentSelection.length === 0 ) {
-			return;
-		}
-
-		let group = this.getGroupFromElement(target);
-		if (!group && target !== field.ui.field.preview) {
-			group = this.createGroup(field.key);
-		}
-
-		currentSelection.forEach(uploadId => {
-			this.addImageToGroup(uploadId, group.grid??null, false);
-		});
-
-		this.persistFieldState(group.fieldId);
-	}
-
-	getCurrentSelection(fieldId) {
-		let selected = [];
-		for (var [key, handler] of this.selectionHandlers) {
-			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
-				selected = selected.concat([... handler.selectedItems]);
-			}
-		}
-		return selected;
-	}
-
-	/**
-	 * Remove an empty group from the field
-	 * @param {string} groupId - The group to remove
-	 * @param {boolean} confirm - ask for confirmation
-	 */
-	removeGroup(groupId, confirm = false) {
-		let group = this.groups.get(groupId);
-		if (!group) {
-			return;
-		}
-
-		if (confirm && group.uploads && group.uploads.size > 0) {
-			if(!window.confirm('This will delete this group. Any uploads in this group will return to the main grid. Are you sure?')){
-				return;
-			}
-		}
-
-		// Move any remaining uploads back to preview
-		if (group.uploads && group.uploads.size > 0) {
-			Array.from(group.uploads).forEach(uploadId => {
-				this.addImageToGroup(uploadId, null, false);
-			});
-		}
-
-		// Remove from groups Map
-		this.groups.delete(groupId);
-
-		// Remove DOM element
-		let groupElement = group.element;
-		if (groupElement) {
-			groupElement.remove();
-			this.a11y.announce('Group removed');
-		}
-
-		this.persistFieldState(group.fieldId);
-	}
-
-	/**
-	 * Create a new group
-	 */
-	createGroup(fieldKey) {
-		const field = this.fields.get(fieldKey);
-		if (!field) {
-			console.error('Field not found:', fieldKey);
-			return null;
-		}
-
-		const groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-		const groupElement = this.createGroupElement(groupId, fieldKey);
-		if (!groupElement) {
-			console.error('Failed to create group element');
-			return null;
-		}
-
-		// Store in field UI Map
-		if (!field.ui.groups) {
-			field.ui.groups = {
-				groups: new Map(),
-				container: null,
-				empty: null,
-				display: null
-			};
-		}
-
-		field.ui.groups.groups.set(groupId, groupElement);
-
-		// Insert into DOM
-		if (field.ui.groups.container && field.ui.groups.empty) {
-			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-		} else if (field.ui.groups.container) {
-			field.ui.groups.container.appendChild(groupElement);
-		}
-
-		// Create group object
-		const group = {
-			id: groupId,
-			fieldId: fieldKey,
-			element: groupElement,
-			grid: groupElement.querySelector('.item-grid.group'),
-			uploads: new Set(),
-			meta: {},
-			changes: {}
-		};
-
-		// Store group
-		this.groups.set(groupId, group);
-
-		// Initialize selection handler for this group
-		this.addGroupSelectionHandler(fieldKey, groupId);
-
-		// Persist state
-		this.persistFieldState(fieldKey);
-
-		return group;
-	}
-
-
-	/**
-	 * Remove upload from group
-	 */
-	removeFromGroup(fieldId, uploadId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const group = field.groups.find(g => g.id === groupId);
-		if (!group) return;
-
-		group.uploads = group.uploads.filter(id => id !== uploadId);
-
-		this.renderGroupUI(fieldId);
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Update group title
-	 */
-	updateGroupTitle(fieldId, groupId, title) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const group = field.groups.find(g => g.id === groupId);
-		if (!group) return;
-
-		group.title = title;
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Delete group
-	 */
-	deleteGroup(fieldId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		field.groups = field.groups.filter(g => g.id !== groupId);
-
-		this.renderGroupUI(fieldId);
-		this.removeSelectionHandler(fieldId, groupId);
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Render group UI
-	 */
-	renderGroupUI(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const container = field.ui.group.container;
-		if (!container) {
-			console.warn('Groups container not found for field:', fieldId);
-			return;
-		}
-
-		// Clear existing
-		window.removeChildren(container);
-
-		// Render each group
-		field.groups.forEach(group => {
-			const groupEl = this.createGroupElement(fieldId, group);
-			container.appendChild(groupEl);
-		});
-	}
-
-	createGroupElement(groupId, fieldId) {
-		let groupElement = window.getTemplate('imageGroup');
-		if (!groupElement) return;
-
-		groupElement.dataset.groupId = groupId;
-		groupElement.dataset.fieldId = fieldId;
-
-		let fields = window.getTemplate('groupMetadata');
-		const fieldsContainer = groupElement.querySelector('.fields');
-		if (fieldsContainer && fields) {
-			fieldsContainer.append(fields);
-
-			// Set unique IDs and names for form fields
-			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
-			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
-
-			if (titleInput) {
-				titleInput.id = `${groupId}_title`;
-				titleInput.name = `${groupId}[post_title]`;
-			}
-			if (excerptInput) {
-				excerptInput.id = `${groupId}_excerpt`;
-				excerptInput.name = `${groupId}[post_excerpt]`;
-			}
-			let field = this.fields.get(fieldId);
-			if (field.content !== '') {
-				let summary = groupElement.querySelector('summary');
-				summary.textContent = field.content + ' Fields';
-			}
-		} else {
-			groupElement.querySelector('details').remove();
-		}
-
-		const gridContainer = groupElement.querySelector('.item-grid.group');
-		if (gridContainer) {
-			gridContainer.dataset.groupId = groupId;
-		}
-
-		return groupElement;
-	}
-
-	handleSelectAll(element, checked = null) {
-		this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
-	}
-
-	clearAllSelections(field) {
-		const handler = this.selectionHandlers.get(field.key);
-		if (handler) {
-			handler.clearSelection();
-		}
-	}
-
-	getSelectedUploads(element) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return [];
-
-		const handler = this.selectionHandlers.get(field.key);
-		return handler ? handler.getSelected() : [];
-	}
-
-	removeSelection(button) {
-		let fieldId = this.getFieldIdFromElement(button);
-
-		const selectedUploads = this.getSelectedUploads(button);
-		if (selectedUploads.length === 0) {
-			this.notify('No uploads selected', 'warning');
-			return;
-		}
-
-		selectedUploads.forEach(upload => {
-			this.removeUpload(fieldId, upload);
-		});
-	}
-
-	removeUpload(fieldId, uploadId) {
-		const field = this.fields.get(fieldId);
-		const upload = this.uploads.get(uploadId);
-
-		if (!field || !upload) return;
-
-		// Remove from field
-		field.uploads?.delete(uploadId);
-
-		// Remove from group if grouped
-		if (upload.groupId) {
-			const group = this.groups.get(upload.groupId);
-			if (group && group.uploads) {
-				group.uploads.delete(uploadId);
-
-				if (group.uploads.size === 0) {
-					this.removeGroup(upload.groupId);
-				}
-			}
-		}
-
-		// Clean up element
-		upload.element?.remove();
-
-		// Clean up memory
-		this.clearUpload(uploadId);
-
-		// Update field state after removal
-		this.updateFieldState(fieldId);
-
-		// Update UI
-		this.maybeLockUploads(fieldId);
-		const handler = this.selectionHandlers.get(field.key);
-		if (handler) {
-			handler.deselect(uploadId);
-		}
-
-		this.a11y.announce('Upload removed');
-	}
-
-	/**************************************************************************
-	 META
-	 Handled separately, in case it is edited in the middle of processing images
-	**************************************************************************/
-
-	/**************************************************************************
-	 SUBSCRIBERS
-	**************************************************************************/
-	/**
-	 * Event system
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	handleBeforeUnload(e) {
-		// Check for any uploads in processing or pending state
-		const unsavedUploads = Array.from(this.uploads.values()).filter(upload =>
-			upload.status === 'processing' ||
-			upload.status === 'pending' ||
-			upload.status === 'uploading'
-		);
-
-		if (unsavedUploads.length > 0) {
-			const message = 'You have uploads in progress. Are you sure you want to leave?';
-			e.preventDefault();
-			e.returnValue = message;
-			return message;
-		}
-	}
-	/**************************************************************************
-	 CLEANUP
-	**************************************************************************/
-	cleanup() {
-		this.clearListeners();
-		if (this.hasGroups) {
-			this.clearGroupListeners();
-		}
-		this.compressionWorker = null;
-		this.subscribers.clear();
-	}
-
-	/**
-	 * Clear individual upload from cache after successful server upload
-	 */
-	async clearUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		// Clean up preview URL
-		if (upload.preview && upload.preview.startsWith('blob:')) {
-			URL.revokeObjectURL(upload.preview);
-			upload.preview = null;
-		}
-
-		// Clean up element preview URL
-		if (upload.element) {
-			const previewUrl = upload.element.dataset.previewUrl;
-			if (previewUrl && previewUrl.startsWith('blob:')) {
-				URL.revokeObjectURL(previewUrl);
-				delete upload.element.dataset.previewUrl;
-			}
-		}
-
-		this.persistFieldState(upload.fieldId);
-		// Remove from memory
-		this.uploads.delete(uploadId);
-		this.uploadBlobs.delete(uploadId);
-
-		// Remove from IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').delete(uploadId);
-		}
-	}
-
-	/**
-	 * Clear all uploads for a field and cleanup resources
-	 */
-	clearField(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const uploads = Array.from(field.uploads || []);
-
-		// Cleanup each upload's resources
-		uploads.forEach(uploadId => {
-			this.clearUpload(uploadId);
-			this.uploads.delete(uploadId);
-		});
-
-		// Clear field state
-		this.fields.delete(fieldId);
-
-		// Cleanup IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-			tx.objectStore('fieldStates').delete(fieldId);
-			uploads.forEach(uploadId => {
-				tx.objectStore('uploadBlobs').delete(uploadId);
-			});
-		}
-	}
-}
-
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbUploads = new UploadManager();
-});
diff --git a/assets/js/concise/UploadManagerOlder.js b/assets/js/concise/UploadManagerOlder.js
deleted file mode 100644
index c51d8cd..0000000
--- a/assets/js/concise/UploadManagerOlder.js
+++ /dev/null
@@ -1,4010 +0,0 @@
-class UploadManager {
-	constructor() {
-		//Load dependencies
-		this.queue = window.jvbQueue;
-		this.a11y = window.jvbA11y;
-		this.error = window.jvbError;
-		this.notifications = window.jvbNotifications;
-
-		//Load Datastore
-		this.initDB();
-
-		//State management
-		this.fields = new Map();
-		this.uploads = new Map();
-		this.uploadBlobs = new Map();
-		this.timeouts = new Map();
-		this.selected = new Map();
-		this.dragState = null;
-		this.hasGroups = false;
-
-		this.selectionHandlers = new Map();
-
-		//Worker
-		this.worker = {
-			worker: null,
-			timeout: null,
-			tasks: new Map(),
-			restart: {
-				count: 0,
-				max: 3,
-			},
-			settings: {
-				timeout: 10000, //10 seconds per image
-				batchSize: 1,
-				maxConcurrent: 3,
-				restartAfterTimeout: true
-			}
-		};
-
-		//Groups!
-		this.touch = {
-			x: null,
-			y: null
-		}
-		this.hasBulkContext = document.querySelector('details.uploader')!==null;
-		this.isTouching = false;
-		this.groups = new Map();
-
-		//Notification and Subscribers
-		this.subscribers = new Set();
-
-		this.settings = {
-			allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
-			maxFileSize: 5242880,
-			maxProcessingTime: 120000, // 2 minutes max for processing
-			processingCheckInterval: 5000, // Check every 5 seconds
-			smartCompression: true,
-			fieldTypes: {
-				'single': { maxFiles: 1, allowMultiple: false },
-				'gallery': { maxFiles: 20, allowMultiple: true },
-				'groupable': { maxFiles: 20, allowMultiple: true }
-			}
-		};
-
-		this.acceptedTypes = {
-			image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
-			video: ['video/mp4', 'video/webm', 'video/ogg', 'video/ogv'],
-			document: [
-				'application/pdf',
-				'application/msword',
-				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-				'text/plain',
-				'text/csv'
-			]
-		};
-
-		this.maxSizes = {
-			image: 5 * 1024 * 1024,    // 5MB
-			video: 100 * 1024 * 1024,  // 100MB
-			document: 10 * 1024 * 1024 // 10MB
-		};
-
-		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.initElements();
-		this.initListeners();
-		this.initCompressionWorker();
-		this.queue.subscribe((event, operation) => {
-			console.log('Operation Endpoint: ', operation.endpoint);
-			if (operation.endpoint !== 'uploads') {
-				return;
-			}
-			switch(event) {
-				case 'cancel-operation':
-					this.clearField(operation.data.get('field_key'));
-					break;
-				case 'operation-status':
-					console.log('Operation Data: ',operation.data);
-					const fieldId = operation.data?.field_key ||
-						(operation.data instanceof FormData ?
-							operation.data.get('field_key') : null);
-
-					if (fieldId) {
-						console.log('Updating field status:', fieldId, operation.status);
-						this.updateFieldStatus(fieldId, operation.status);
-					}
-					break;
-			}
-		});
-		this.scanFields();
-	}
-
-	initElements() {
-		this.selectors = {
-			field: {
-				field: '.field.upload',
-				dropZone: '.file-upload-container',
-				preview: '.item-grid.preview',
-				hiddenValue: 'input[type="hidden"]',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-			},
-			item: {
-				img: 'img',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-				status: '.status',
-				select: '[name*="select-item"]',
-				actions: '.item-actions',
-				featured: '[name="featured"]',
-				meta: '.upload-meta'
-			},
-			groups: {
-				container: '.item-grid.groups',
-				display: '.group-display',
-				selectAll: '#select-all-uploads',
-				actions: '.selection-actions',
-				info: '.selection-controls .info',
-				count: '.selection-count',
-				group: '.upload-group',
-				empty: '.empty-group'
-			}
-		};
-		this.ui = {};
-	}
-
-	scanFields() {
-		document.querySelectorAll(this.selectors.field.field).forEach(uploader => {
-			this.registerUploader(uploader);
-		});
-	}
-
-	/**
-	 *
-	 * @param {HTMLElement} uploader
-	 * @param {object} options
-	 * @param {string} options.id Uploader field ID: defaults to uploader.dataset.fieldId
-	 * @param {string} options.type Uploader type: defaults to uploader.dataset.type
-	 * @param {number} options.maxFiles Maximum files to allow: defaults to type defaults
-	 * @param {boolean} options.multiple Whether to allow multiple uploads
-	 * @param {number} options.itemID The post or term ID this is for.
-	 * @param {string} options.mode
-	 * @returns {string}
-	 */
-	registerUploader(uploader, options = {}) {
-		//Determine if this is for a post, term, content uploader, or option
-		let key = uploader.dataset['uploader']??this.determineKey(uploader);
-
-		uploader.dataset['uploader'] = key;
-
-		if (!this.fields.has(key)) {
-			let type = uploader.dataset.type??'single';
-
-			let typeConfig = this.settings.fieldTypes[type]??this.settings.fieldTypes['single'];
-			let config = {
-				key: key,
-				name: uploader.dataset.field,
-				ui: {},
-				type: type,
-				subtype: uploader.dataset.subtype??'image',
-				maxFiles: typeConfig.maxFiles,
-				multiple: typeConfig.allowMultiple,
-				content: uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??false,
-				itemID: uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??false,
-				context: uploader.dataset.context??uploader.closest('dialog')?.dataset.context??false,
-				mode: uploader.dataset.mode??'direct',
-				destination: uploader.dataset.destination ?? 'meta',
-				... options
-			};
-
-			config.ui = window.uiFromSelectors(this.selectors, uploader);
-			config.ui.groups.groups = new Map();
-
-			this.selected.set(key, new Set());
-			this.fields.set(key, config);
-			if(config.destination === 'post_group' && !this.hasGroups) {
-				this.initGroupListeners();
-			}
-			// Initialize selection handler for this field
-			this.initSelectionHandler(key, config);
-		}
-		return key;
-	}
-
-	initSelectionHandler(fieldKey) {
-		const field = this.fields.get(fieldKey);
-		if (!field) return;
-
-		// Don't reinitialize if already exists
-		if (this.selectionHandlers.has(fieldKey)) {
-			return this.selectionHandlers.get(fieldKey);
-		}
-
-		// Get the container - use preview for uploads in preview, or field for all uploads
-		const container = field.ui.field.preview || field.ui.field.field;
-		if (!container) {
-			console.warn('No container found for selection handler:', fieldKey);
-			return;
-		}
-
-		const handler = new window.jvbHandleSelection({
-			container: container,
-			ui: {
-				selectAll: field.ui.groups.selectAll,
-				bulkControls: field.ui.groups.actions,
-				count: field.ui.groups.count
-			},
-			itemSelector: '[data-upload-id]',
-			checkboxSelector: '[name*="select-item"]',
-			onSelectionChange: (selectedItems) => {
-				// Sync with UploadManager's selected set
-				this.selected.set(fieldKey, selectedItems);
-
-				// Call any additional UI updates if needed
-				// this.onSelectionChanged(fieldKey, selectedItems);
-			}
-		});
-
-		this.selectionHandlers.set(fieldKey, handler);
-		return handler;
-	}
-
-	/**
-	 * Builds a key from the uploader, built from the Content Type, ItemID, and FieldName
-	 * @param uploader
-	 * @returns {string}
-	 */
-	determineKey(uploader) {
-		let content = uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??'';
-		let itemID = uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??'';
-		let field = uploader.dataset.field;
-		return `${content}_${itemID}_${field}`;
-	}
-
-	/**
-	 *
-	 * @param {HTMLElement} element
-	 */
-	getFieldIdFromElement(element) {
-		let field = element.closest('.field.upload');
-		if (!field) {
-			return;
-		}
-		return field.dataset.uploader??this.determineKey(field);
-	}
-
-	getFieldFromElement(element) {
-		let id = this.getFieldIdFromElement(element);
-		return (this.fields.has(id)) ? this.fields.get(id) : false;
-	}
-
-	getUploadFromElement(element) {
-		let id = this.getUploadIdFromElement(element);
-		return (this.uploads.has(id)) ? this.uploads.get(id) : false;
-	}
-
-	getUploadIdFromElement(element) {
-		let upload = element.closest('[data-upload-id]');
-		return upload?.dataset.uploadId || null;
-	}
-
-	getGroupFromElement(element) {
-		let groupId = this.getGroupIdFromElement(element);
-		return (this.groups.has(groupId)) ? this.groups.get(groupId) : false;
-	}
-	getGroupIdFromElement(element) {
-		return element.dataset.groupId??element.closest('[data-group-id]')?.dataset.groupId??element.closest(':has([data-group-id])')?.querySelector('[data-group-id]')?.dataset.groupId??null;
-	}
-
-	getModalType(field) {
-		// Safety check for field.ui
-		if (!field || !field.ui || !field.ui.field || !field.ui.field.field) {
-			return null;
-		}
-
-		const dialog = field.ui.field.field.closest('dialog');
-		if (!dialog) return null;
-
-		if (dialog.classList.contains('edit')) return 'edit';
-		if (dialog.classList.contains('create')) return 'create';
-		if (dialog.classList.contains('bulkEdit')) return 'bulkEdit';
-
-		return dialog.className;
-	}
-
-	getStatusText(status) {
-		return this.statusMapping[status] || status;
-	}
-
-	getStatusIcon(status) {
-		return window.getIcon(this.queue.icons[status]);
-	}
-	getStatusProgress(status) {
-		console.log('Getting status progress for: ', status);
-		switch (status) {
-			case 'local_processing':
-				return 28;
-			case 'queued':
-				return 50;
-			case 'uploading':
-				return 66;
-			case 'pending':
-				return 75;
-			case 'processing':
-				return 89;
-			case 'completed':
-				return 100;
-			default:
-				return 0;
-		}
-	}
-
-	/******************************************************************************
-	 LISTENERS
-	 ******************************************************************************/
-	initListeners() {
-		this.clickHandler 		= this.handleClick.bind(this);
-		this.changeHandler 		= this.handleChange.bind(this);
-
-		if (this.hasBulkContext) {
-			this.pasteHandler 		= this.handlePaste.bind(this);
-			document.addEventListener('paste', this.pasteHandler);
-		}
-
-
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('change', this.changeHandler);
-		window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
-	}
-	clearListeners() {
-		document.removeEventListener('click', this.clickHandler);
-		document.removeEventListener('change', this.changeHandler);
-		if (this.hasBulkContext) {
-			document.removeEventListener('paste', this.pasteHandler);
-		}
-	}
-
-	initGroupListeners() {
-		this.hasGroups = true;
-
-		this.dragStartHandler 	= this.handleDragStart.bind(this);
-		this.dragEndHandler 	= this.handleDragEnd.bind(this);
-		this.dragEnterHandler 	= this.handleDragEnter.bind(this);
-		this.dragOverHandler 	= this.handleDragOver.bind(this);
-		this.dragLeaveHandler 	= this.handleDragLeave.bind(this);
-		this.dropHandler 		= this.handleDrop.bind(this);
-
-		this.touchStartHandler 	= this.handleTouchStart.bind(this);
-		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
-		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
-		this.touchCancelHandler	= this.handleTouchCancel.bind(this);
-
-		document.addEventListener('dragstart', this.dragStartHandler);
-		document.addEventListener('dragend', this.dragEndHandler);
-		document.addEventListener('dragenter', this.dragEnterHandler);
-		document.addEventListener('dragover', this.dragOverHandler);
-		document.addEventListener('dragleave', this.dragLeaveHandler);
-		document.addEventListener('drop', this.dropHandler);
-
-		document.addEventListener('touchstart', this.touchStartHandler);
-		document.addEventListener('touchmove', this.touchMoveHandler);
-		document.addEventListener('touchend', this.touchEndHandler);
-		document.addEventListener('touchcancel', this.touchCancelHandler);
-
-		document.addEventListener('input', (e) => {
-			if (e.target.matches('.fields.group input, .fields.group textarea')) {
-				this.handleGroupMetadataChange(e);
-			}
-		});
-	}
-	handleGroupMetadataChange(e) {
-		if (!e.target.closest('.fields.group')) return;
-
-		const groupElement = e.target.closest('[data-group-id]');
-		if (!groupElement) return;
-
-		const fieldId = groupElement.dataset.fieldId;
-		this.persistFieldState(fieldId);
-	}
-	clearGroupListeners() {
-		document.removeEventListener('dragstart', this.dragStartHandler);
-		document.removeEventListener('dragend', this.dragEndHandler);
-		document.removeEventListener('dragenter', this.dragEnterHandler);
-		document.removeEventListener('dragover', this.dragOverHandler);
-		document.removeEventListener('dragleave', this.dragLeaveHandler);
-		document.removeEventListener('drop', this.dropHandler);
-
-		document.removeEventListener('touchstart', this.touchStartHandler);
-		document.removeEventListener('touchmove', this.touchMoveHandler);
-		document.removeEventListener('touchend', this.touchEndHandler);
-		document.removeEventListener('touchcancel', this.touchCancelHandler);
-	}
-
-	handleClick(e) {
-		if (!e.target.closest(this.selectors.field.field)) {
-			return;
-		}
-
-		if (window.targetCheck(e, '.restart-uploads')) {
-			e.preventDefault();
-			const fieldId = this.getFieldIdFromElement(e.target);
-			this.restartUploads(fieldId);
-		} else if (window.targetCheck(e, '.dismiss-cache-restore')) {
-			e.preventDefault();
-			const notification = e.target.closest('.upload-recovery-notification');
-			if (notification) notification.remove();
-		} else if (window.targetCheck(e, '.create-from-selection')) {
-			e.preventDefault();
-			let group = this.createGroup(this.getFieldFromElement(e.target));
-			this.addSelectionToGroup(group);
-		} else if (window.targetCheck(e, '.remove-selection')) {
-			e.preventDefault();
-			this.removeSelection(e.target);
-		} else if (window.targetCheck(e, '.add-to-group, .add-selection-to-group')) {
-			e.preventDefault();
-			this.addSelectionToGroup(e.target);
-		} else if (window.targetCheck(e, '.remove-group')) {
-			e.preventDefault();
-			const groupElement = e.target.closest('.upload-group');
-			if (groupElement) {
-				let field = this.getFieldFromElement(groupElement);
-				this.removeGroup(groupElement, true);
-			}
-		} else if (window.targetCheck(e, '.remove')) {
-			e.preventDefault();
-			const uploadId = this.getUploadIdFromElement(e.target);
-			const fieldId = this.getFieldIdFromElement(e.target);
-			if (uploadId && fieldId) {
-				this.removeUpload(fieldId, uploadId);
-			}
-		} else if (window.targetCheck(e, '.submit-uploads')) {
-			e.preventDefault();
-			const fieldId = this.getFieldIdFromElement(e.target);
-			this.submitUploads(fieldId);
-		} else if (window.targetCheck(e, '.retry-upload')) {
-			e.preventDefault();
-			const uploadId = this.getUploadIdFromElement(e.target);
-			this.retryUpload(uploadId);
-		}
-	}
-	handleChange(e) {
-		if (!e.target.closest(this.selectors.field.field) || e.target.classList.contains(this.selectors.field.hiddenValue)) {
-			return;
-		}
-		e.preventDefault();
-
-		if (window.targetCheck(e, '[type="file"]')) {
-			console.log(this.fields);
-			let field = this.getFieldFromElement(e.target);
-			console.log(field);
-			if (!field) {
-				console.warn('File change on unregistered field: ', field.key)
-				return;
-			}
-
-			const files = Array.from(e.target.files);
-			if (files.length === 0) return;
-
-			this.processFiles(field.key, files);
-			e.target.value = '';
-		} else if (e.target.closest('.upload-meta')) {
-			e.preventDefault();
-			let name = e.target.name;
-			let value = e.target.value;
-			let upload = this.getUploadFromElement(e.target);
-			upload.changes[name] = value;
-			this.uploads.set(upload.id, upload);
-			this.persistFieldState(upload.fieldId);
-
-			//It's meta!
-			//TODO:
-			//Step 1) determine whether the images have already been sent to the server. If not, we must wait until they have been
-			//Step 2) Queue the Meta changes. No need to wait, the Queue.js will handle any debouncing/timeouts
-			//Ensure the dependencies have all operations stored to the field that the images were uploaded with (can be multiple)
-			//Send to server for processing
-		} else if (e.target.closest('.group.fields')) {
-			let group = this.getGroupFromElement(e.target);
-			let name = e.target.name;
-			group.changes[name] = e.target.value;
-
-			this.persistFieldState(group.fieldId);
-			this.groups.set(group.id, group);
-		}
-	}
-
-	handlePaste(e) {
-		window.debouncer.schedule(
-			'imagePaste',
-			() => {
-				const items = Array.from(e.clipboardData.items);
-				const imageItems = items.filter(item => item.type.startsWith('image/'));
-
-				if (imageItems.length === 0) return;
-
-				e.preventDefault();
-
-				const fieldId = this.getFieldIdFromElement(e.target);
-				if (!fieldId) return;
-
-				// Convert clipboard items to files
-				const files = [];
-				imageItems.forEach((item, index) => {
-					const file = item.getAsFile();
-					if (file) {
-						// Rename for clarity
-						const newFile = new File([file], `pasted_image_${index + 1}.png`, {
-							type: file.type,
-							lastModified: Date.now()
-						});
-						files.push(newFile);
-					}
-				});
-
-				if (files.length > 0) {
-					this.processFiles(fieldId, files);
-				}
-			},
-			100
-		);
-	}
-
-	isTouchOnFormElement(target) {
-		// Check if target is a form element or inside one
-		const formElements = [
-			'input', 'button', 'label', 'select', 'textarea',
-		];
-
-		return formElements.some(selector => {
-			return target.matches(selector) || target.closest(selector);
-		});
-	}
-	/**** DRAG AND TOUCH *****/
-	startDragOperation(config) {
-		const {
-			primaryElement,
-			sourceType,
-			startPosition,
-			event
-		} = config;
-
-		const uploadId = this.getUploadIdFromElement(primaryElement);
-		const fieldId = this.getFieldIdFromElement(primaryElement);
-
-		// Determine what items to drag
-		const draggedItems = this.getDraggedItems(primaryElement);
-
-		// Initialize drag state
-		this.dragState = {
-			primaryItem: uploadId,
-			draggedItems: draggedItems,
-			isDragging: true,
-			isMultiDrag: draggedItems.length > 1,
-			fieldId: fieldId,
-			sourceType: sourceType,
-			startTime: Date.now(),
-			startPosition: startPosition,
-			currentPosition: startPosition,
-			currentTarget: null,
-			validTarget: null,
-			dragPreview: null,
-			touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
-			touchMoved: false
-		};
-
-		// Create drag preview
-		this.createDragPreview(primaryElement);
-
-		// Apply dragging state
-		this.applyDraggingState(true);
-
-		const announceText = this.dragState.isMultiDrag
-			? `Started dragging ${draggedItems.length} items`
-			: 'Started dragging item';
-
-		this.a11y.announce(announceText);
-		this.provideDragFeedback('start');
-
-		return true;
-	}
-
-	updateDragOperation(position, elementUnderPointer) {
-		if (!this.dragState.isDragging) return;
-
-		const { sourceType, startPosition } = this.dragState;
-
-		// Update position
-		this.dragState.currentPosition = position;
-
-		// Check for significant movement (touch)
-		if (sourceType === 'touch' && !this.dragState.touchMoved) {
-			const deltaX = Math.abs(position.x - startPosition.x);
-			const deltaY = Math.abs(position.y - startPosition.y);
-
-			if (deltaX > 10 || deltaY > 10) {
-				this.dragState.touchMoved = true;
-			}
-		}
-
-		// Update preview and target
-		this.updateDragPreview(position);
-		this.updateDropTarget(elementUnderPointer);
-	}
-
-	endDragOperation(elementUnderPointer = null) {
-		if (!this.dragState.isDragging) return;
-
-		const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
-			this.dragState.validTarget;
-
-		// Process drop if valid - but only here, not in handleDrop
-		if (wasSuccessful && this.dragState.validTarget) {
-			this.processItemDrop({
-				itemIds: this.dragState.draggedItems,
-				targetElement: this.dragState.validTarget,
-				fieldId: this.dragState.fieldId,
-				dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
-				sourceType: this.dragState.sourceType
-			});
-		}
-
-		// Cleanup
-		this.cleanupDragOperation();
-
-		const announceText = wasSuccessful
-			? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
-			: 'Drag cancelled';
-
-		this.a11y.announce(announceText);
-	}
-
-	/**
-	 * Shared method to process any drop operation (drag or touch)
-	 * @param {Object} dropData - Standardized drop data
-	 * @returns {boolean} Success status
-	 */
-	processItemDrop(dropData) {
-		const {
-			itemIds,
-			targetElement,
-			fieldId,
-			dropType,
-			sourceType
-		} = dropData;
-
-		if (!itemIds?.length || !targetElement || !fieldId) {
-			return false;
-		}
-
-		// Determine if it's a preview drop
-		let isPreviewDrop = targetElement.classList.contains('item-grid') && targetElement.classList.contains('preview');
-
-		// Handle empty group drops by creating the group element
-		let actualTarget = targetElement;
-		if (targetElement.classList.contains('empty-group')) {
-			let group = this.createGroup(fieldId);
-			if (!group) {
-				console.error('Failed to create group');
-				return false;
-			}
-			actualTarget = group.querySelector('.item-grid');
-			if (!actualTarget) {
-				console.error('Group element missing .item-grid');
-				return false;
-			}
-			isPreviewDrop = false;
-		}
-
-		// Use existing addImageToGroup method for each item
-		itemIds.forEach(uploadId => {
-			this.addImageToGroup(uploadId, actualTarget, isPreviewDrop);
-		});
-
-
-		const field = this.fields.get(fieldId);
-		if (field) {
-			this.clearAllSelections(field);
-		}
-
-		this.persistFieldState(fieldId);
-
-		// Announce completion
-		const announceText = dropType === 'multiple'
-			? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
-			: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
-
-		this.a11y.announce(announceText);
-		this.provideFeedback(sourceType, 'success', {
-			count: itemIds.length,
-			isMultiple: dropType === 'multiple'
-		});
-
-		return true;
-	}
-
-
-
-	cleanupDragOperation() {
-		if (this.dragState.dragPreview) {
-			this.dragState.dragPreview.remove();
-		}
-
-		this.applyDraggingState(false);
-		this.clearDropTargetStates();
-
-		// Reset state
-		this.dragState.isDragging = false;
-		this.dragState.dragPreview = null;
-		this.dragState.draggedItems = [];
-	}
-
-	/**
-	 * Determine what items to drag (single or multiple selection)
-	 */
-	getDraggedItems(element) {
-		const selectedUploads = this.getSelectedUploads(element);
-		const primaryUploadId = element.dataset.uploadId;
-
-		// If we have multiple selections and primary is selected, drag all
-		if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
-			return selectedUploads;
-		}
-
-		// Otherwise, just drag the primary item
-		return [primaryUploadId];
-	}
-
-	/**
-	 * Apply/remove dragging visual state to items
-	 */
-	applyDraggingState(isDragging) {
-		this.dragState.draggedItems.forEach(uploadId => {
-			const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
-			if (element) {
-				element.classList.toggle('dragging', isDragging);
-			}
-		});
-	}
-
-	/**
-	 * Create drag preview element
-	 */
-	createDragPreview(originalElement) {
-		const { isMultiDrag, draggedItems } = this.dragState;
-
-		if (isMultiDrag) {
-			this.dragState.dragPreview = this.createMultiDragPreview(originalElement, draggedItems);
-		} else {
-			this.dragState.dragPreview = this.createSingleDragPreview(originalElement);
-		}
-
-		// Set initial transform to position it at start
-		const offset = this.dragState.sourceType === 'touch'
-			? (this.dragState.isMultiDrag ? { x: -60, y: -80 } : { x: -50, y: -60 })
-			: (this.dragState.isMultiDrag ? { x: 15, y: 15 } : { x: 10, y: 10 });
-
-		this.dragState.dragPreview.style.transform =
-			`translate(${this.dragState.startPosition.x + offset.x}px, ${this.dragState.startPosition.y + offset.y}px) scale(1.05)`;
-
-		document.body.appendChild(this.dragState.dragPreview);
-	}
-
-	/**
-	 * Create single item drag preview
-	 */
-	createSingleDragPreview(originalElement) {
-		const preview = originalElement.cloneNode(true);
-		preview.dataset.uploadId = preview.dataset.uploadId+'-dragging';
-		this.styleDragPreview(preview, false);
-		return preview;
-	}
-
-	styleDragPreview(preview, isMulti = false) {
-		preview.style.cssText = `
-        position: fixed;
-        z-index: 10000;
-        pointer-events: none;
-        opacity: 0.9;
-        transform: scale(1.05);
-        transition: transform 0.2s ease;
-        ${isMulti ? `
-            width: 120px;
-            height: 120px;
-            background: white;
-            border-radius: 8px;
-            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
-            padding: 4px;
-        ` : `
-            border-radius: 4px;
-            box-shadow: 0 4px 16px rgba(0,0,0,0.2);
-        `}
-    `;
-
-		// Add dragging class for additional styling
-		preview.classList.add('drag-preview', 'is-dragging');
-		if (isMulti) {
-			preview.classList.add('multi-item');
-		}
-	}
-
-	/**
-	 * Create multiple items drag preview
-	 */
-	createMultiDragPreview(originalElement, draggedItems) {
-		const container = document.createElement('div');
-		container.className = 'drag-preview multi-item';
-
-		container.style.cssText = `
-		position: relative;
-		width: 120px;
-		height: 120px;
-	`;
-
-		// Create stacked effect with up to 3 items
-		const displayCount = Math.min(draggedItems.length, 3);
-
-		for (let i = 0; i < displayCount; i++) {
-			const uploadId = draggedItems[i];
-			const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
-
-			if (uploadElement) {
-				const stackedItem = uploadElement.cloneNode(true);
-				stackedItem.dataset.uploadId = uploadId + '_dragging';
-
-				// **FIX**: Improved stacking with better visual separation
-				stackedItem.style.cssText = `
-				position: absolute;
-				top: ${i * 8}px;
-				left: ${i * 8}px;
-				width: calc(100% - ${i * 8}px);
-				height: calc(100% - ${i * 8}px);
-				opacity: ${1 - (i * 0.15)};
-				transform: rotate(${(i - 1) * 3}deg);
-				z-index: ${10 - i};
-				border-radius: 4px;
-				overflow: hidden;
-				box-shadow: 0 2px 8px rgba(0,0,0,0.${5 - i});
-			`;
-				container.appendChild(stackedItem);
-			}
-		}
-
-		// Add count badge
-		if (draggedItems.length > 1) {
-			const badge = this.createCountBadge(draggedItems.length);
-			container.appendChild(badge);
-		}
-
-		this.styleDragPreview(container, true);
-		return container;
-	}
-	/**
-	 * Update drag preview position
-	 */
-	updateDragPreview(position) {
-		if (!this.dragState.dragPreview) return;
-
-		// Calculate offset based on preview type and source
-		let offset;
-		if (this.dragState.sourceType === 'touch') {
-			offset = this.dragState.isMultiDrag ? { x: -60, y: -80 } : { x: -50, y: -60 };
-		} else {
-			offset = this.dragState.isMultiDrag ? { x: 15, y: 15 } : { x: 10, y: 10 };
-		}
-
-		const deltaX = position.x - this.dragState.startPosition.x;
-		const deltaY = position.y - this.dragState.startPosition.y;
-
-		this.dragState.dragPreview.style.transform = `translate(${deltaX + offset.x}px, ${deltaY + offset.y}px) scale(1.05)`;
-	}
-
-	/**
-	 * Update drop target highlighting
-	 */
-	updateDropTarget(elementUnderPointer) {
-		// Clear previous target
-		if (this.dragState.currentTarget) {
-			this.clearDropTargetState(this.dragState.currentTarget);
-		}
-
-		// Find valid drop target
-		const validTarget = this.findValidDropTarget(elementUnderPointer);
-
-		// Update state
-		this.dragState.currentTarget = elementUnderPointer;
-		this.dragState.validTarget = validTarget;
-
-		// Apply visual feedback
-		if (validTarget) {
-			this.applyDropTargetState(validTarget);
-
-			// Haptic feedback for touch
-			if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
-				const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
-				navigator.vibrate(pattern);
-			}
-		}
-	}
-
-	/**
-	 * Find valid drop target from element
-	 */
-	findValidDropTarget(element) {
-		const target = element?.closest('.item-grid.group, .empty-group, .item-grid.preview');
-		return target && this.getFieldIdFromElement(target) === this.dragState.fieldId ? target : null;
-	}
-
-	/**
-	 * Apply drop target visual state
-	 */
-	applyDropTargetState(target) {
-		target.classList.add('dragover');
-
-		if (this.dragState.isMultiDrag) {
-			target.classList.add('multi-drop');
-			target.setAttribute('data-item-count', this.dragState.draggedItems.length);
-		}
-	}
-
-	/**
-	 * Clear drop target state from element
-	 */
-	clearDropTargetState(target) {
-		target.classList.remove('dragover', 'multi-drop');
-		target.removeAttribute('data-item-count');
-	}
-
-	/**
-	 * Clear all drop target states
-	 */
-	clearDropTargetStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-
-	/**
-	 * Create count badge for multi-item preview
-	 */
-	createCountBadge(count) {
-		const badge = document.createElement('div');
-		badge.className = 'selection-count-badge';
-		badge.textContent = count.toString();
-		return badge;
-	}
-
-	/**
-	 * Provide feedback for drag operations
-	 */
-	provideDragFeedback(type) {
-		const hapticPatterns = {
-			start: [50],
-			success: this.dragState.isMultiDrag ? [30, 20, 30] : [50],
-			error: [100, 50, 100],
-			warning: [50]
-		};
-
-		// Haptic feedback (vibration on supported devices)
-		if (navigator.vibrate && hapticPatterns[type]) {
-			navigator.vibrate(hapticPatterns[type]);
-		}
-
-		// Visual feedback
-		const feedback = document.createElement('div');
-		feedback.className = `drag-feedback ${type}`;
-		feedback.style.cssText = `
-		position: fixed;
-		top: 50%;
-		left: 50%;
-		transform: translate(-50%, -50%);
-		padding: 1rem 2rem;
-		background: var(--${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'warning'});
-		color: white;
-		border-radius: var(--radius);
-		z-index: 10001;
-		animation: feedbackPulse 0.3s ease;
-		pointer-events: none;
-	`;
-
-		const icons = {
-			start: '↕️',
-			success: '✓',
-			error: '✗',
-			warning: '⚠'
-		};
-
-		feedback.textContent = icons[type] || '';
-		document.body.appendChild(feedback);
-
-		setTimeout(() => {
-			feedback.style.animation = 'fadeOut 0.3s ease';
-			setTimeout(() => feedback.remove(), 300);
-		}, 500);
-	}
-
-	/**
-	 * Provide consistent feedback for different input methods
-	 */
-	provideFeedback(sourceType, feedbackType, data = {}) {
-		const hapticPatterns = {
-			success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
-			error: [100, 50, 100]
-		};
-
-		if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
-			navigator.vibrate(hapticPatterns[feedbackType]);
-		}
-	}
-
-	clearDragoverStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-	/*********
-	 *  DRAG HANDLERS
-	 ********/
-	handleDragEnter(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		// Only handle external files
-		if (e.dataTransfer.types.includes('Files')) {
-			e.preventDefault();
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer) {
-				uploadContainer.classList.add('dragover');
-			}
-		}
-	}
-	handleDragLeave(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
-			uploadContainer.classList.remove('dragover');
-		}
-	}
-	handleDragStart(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'drag',
-			startPosition: { x: e.clientX, y: e.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
-			e.dataTransfer.effectAllowed = 'move';
-		} else {
-			e.preventDefault();
-		}
-	}
-
-	handleDragOver(e) {
-		if (!this.dragState || !this.dragState.isDragging) return;
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		e.preventDefault();
-
-		e.dataTransfer.dropEffect = 'move';
-
-		const elementUnderPointer = document.elementFromPoint(e.clientX, e.clientY);
-		this.updateDragOperation(
-			{ x: e.clientX, y: e.clientY },
-			elementUnderPointer
-		);
-	}
-
-	handleDrop(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		e.preventDefault();
-		this.clearDragoverStates();
-
-		// Handle external files (new uploads)
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer) {
-			const files = Array.from(e.dataTransfer.files);
-			if (files.length > 0) {
-				const fieldId = this.getFieldIdFromElement(uploadContainer);
-				if (fieldId) {
-					this.processFiles(fieldId, files);
-					this.a11y.announce(`${files.length} file(s) dropped for upload`);
-				}
-			}
-		}
-	}
-
-	handleDragEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		// Find the element under the final drop position
-		const elementUnderDrop = document.elementFromPoint(
-			this.dragState.currentPosition?.x || e.clientX,
-			this.dragState.currentPosition?.y || e.clientY
-		);
-
-		this.endDragOperation(elementUnderDrop);
-	}
-	/*********
-	 * TOUCH HANDLERS
-	 ********/
-	handleTouchStart(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-		if (this.isTouchOnFormElement(e.target)) {
-			return;
-		}
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const touch = e.touches[0];
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'touch',
-			startPosition: { x: touch.clientX, y: touch.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.preventDefault(); // Prevent scrolling
-		}
-	}
-
-	handleTouchMove(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.touches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.updateDragOperation({ x: touch.clientX, y: touch.clientY }, elementUnderTouch);
-	}
-
-	handleTouchEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.changedTouches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.endDragOperation(elementUnderTouch);
-	}
-
-	handleTouchCancel(e) {
-		if (this.dragState.isDragging) {
-			this.cleanupDragOperation();
-			this.a11y.announce('Drag cancelled');
-		}
-	}
-	/*******************************************************************************
-	 QUEUE INTEGRATION
-	 *******************************************************************************/
-	async submitUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Check if there are uploads to submit
-		const pendingUploads = Array.from(field.uploads || [])
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload &&
-				(upload.status === 'processed' ||
-					upload.status === 'processed-original'));
-
-		if (pendingUploads.length === 0) {
-			// this.notifications.add('No uploads ready to submit', 'warning');
-			return;
-		}
-
-		// Queue the uploads
-		try {
-			await this.queueUpload(fieldId);
-			// this.notifications.add(`Submitting ${pendingUploads.length} upload(s)`, 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'submitUploads',
-				fieldId
-			});
-			// this.notifications.add('Failed to submit uploads', 'error');
-		}
-	}
-	async retryUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		try {
-			// Reset status
-			this.updateUploadStatus(uploadId, 'received');
-
-			// If we have the processed file, skip to queuing
-			if (upload.processedFile) {
-				this.updateUploadStatus(uploadId, 'processed');
-				await this.queueUpload(upload.fieldId);
-			} else if (upload.originalFile) {
-				// Reprocess the file
-				const reprocessed = await this.processFile(upload.fieldId, upload.originalFile);
-				if (reprocessed) {
-					await this.queueUpload(upload.fieldId);
-				}
-			} else {
-				throw new Error('No file data available for retry');
-			}
-
-			// this.notifications.add('Retrying upload...', 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'retryUpload',
-				uploadId
-			});
-			// this.notifications.add('Failed to retry upload', 'error');
-		}
-	}
-	async restartUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const failedUploads = Array.from(field.uploads)
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload && upload.status === 'failed');
-
-		if (failedUploads.length === 0) {
-			// this.notifications.add('No failed uploads to restart', 'info');
-			return;
-		}
-
-		for (const upload of failedUploads) {
-			await this.retryUpload(upload.id);
-		}
-
-		// this.notifications.add(`Restarting ${failedUploads.length} upload(s)`, 'info');
-	}
-	async queueUpload(fieldId) {
-		//Further cache it, or is it already cached at this point?
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const uploads = Array.from(field.uploads);
-		if (uploads.length === 0) {
-			return;
-		}
-
-		const data = this.prepareUploadData(field, uploads);
-		this.a11y.announce('Queuing for upload');
-		let img = (uploads.length === 1) ? 'image' : 'images';
-		const operation = {
-			endpoint: 'uploads',
-			method: 'POST',
-			data: data,
-			title: `Uploading ${uploads.length} ${img} to server...`,
-			popup: `Uploading ${uploads.length} ${img}...`,
-			canMerge: false,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			},
-			append: '_upload'
-		}
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (!upload) {
-					return;
-				}
-				upload.operationId = operationId;
-				this.updateUploadStatus(uploadId, 'queued');
-			});
-			field.operationId = operationId;
-
-			return operationId;
-		} catch (error) {
-			throw error;
-		} finally {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	prepareUploadData(field, uploads) {
-		console.log('Preparing Upload:', field);
-		const formData = new FormData();
-		formData.append('content', field.content);
-		formData.append('mode', field.mode);
-		formData.append('field_name', field.name);
-		formData.append('field_key', field.key);
-		formData.append('field_type', field.type);
-		formData.append('subtype', field.subtype);
-		formData.append('item_id', field.itemID);		//post, term, or user id
-		formData.append('context', field.context);	//post, term, or user
-		formData.append('destination', field.destination || 'meta'); //meta, post, post_group
-		let uploadMap = [];
-
-		const fieldGroups = this.getFieldGroups(field.key);
-		if (field.destination === 'post_group' && fieldGroups.length > 0) {
-			// User has created groups
-			let groups = [];
-			let titles = [];
-			let featuredImages = [];
-
-			fieldGroups.forEach(group => {
-				let groupUploadIndices = [];
-				let featuredIndex = null;
-
-				group.uploads.forEach(uploadId => {
-					let upload = this.uploads.get(uploadId);
-					if (upload) {
-						const fileToUpload = upload.processedFile || upload.originalFile;
-						if (fileToUpload) {
-							formData.append('files[]', fileToUpload);
-							const fileIndex = uploadMap.length;
-							uploadMap.push(upload.id);
-							groupUploadIndices.push(upload.id);
-
-							// Check if this is the featured image
-							const radioInput = upload.element?.querySelector('[name="featured"]');
-							if (radioInput?.checked) {
-								featuredIndex = upload.id;
-							}
-						}
-					}
-				});
-
-				groups.push(groupUploadIndices);
-				titles.push(group.title || '');
-				featuredImages.push(featuredIndex);
-			});
-
-			formData.append('groups', JSON.stringify(groups));
-			formData.append('group_titles', JSON.stringify(titles));
-			formData.append('featured_images', JSON.stringify(featuredImages));
-		} else {
-			// No groups - just append all files
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (upload) {
-					const fileToUpload = upload.processedFile || upload.originalFile;
-					if (fileToUpload) {
-						formData.append('files[]', fileToUpload);
-						uploadMap.push(upload.id);
-					}
-				}
-			});
-		}
-		formData.append('upload_ids', JSON.stringify(uploadMap));
-
-		console.log('Final FormData:');
-		for (let pair of formData.entries()) {
-			console.log(pair[0], pair[1]);
-		}
-
-		return formData;
-	}
-
-	getFieldGroups(fieldId) {
-		const groups = [];
-
-		this.groups.forEach((groupData, groupId) => {
-			if (groupData.fieldId === fieldId) {
-				groups.push({
-					id: groupId,
-					uploads: Array.from(groupData.uploads || new Set()),
-					meta: this.groupsMeta.get(groupId) || {},
-					element: this.fields.get(fieldId)?.ui.groups.groups.get(groupId)
-				});
-			}
-		});
-
-		return groups;
-	}
-
-	/**
-	 * Build groups data from field state
-	 */
-	buildGroupsData(field, uploads) {
-		const groups = [];
-		const titles = [];
-		const uploadMap = [];
-
-		if (field.groups && field.groups.length > 0) {
-			// User has explicitly created groups
-			field.groups.forEach(group => {
-				const groupUploads = [];
-				group.uploads.forEach(uploadId => {
-					groupUploads.push(uploadId);
-					uploadMap.push(uploadId);
-				});
-				groups.push(groupUploads);
-				titles.push(group.title || '');
-			});
-		} else {
-			// No explicit groups - treat all as one group
-			const allUploads = [];
-			uploads.forEach(uploadId => {
-				allUploads.push(uploadId);
-				uploadMap.push(uploadId);
-			});
-			groups.push(allUploads);
-			titles.push('');
-		}
-
-		return { groups, titles, uploadMap };
-	}
-
-	async queueImageMeta(e) {
-		const upload = this.getUploadFromElement(element);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		// Collect meta data from the form
-		const metaContainer = element.closest('.upload-meta');
-		if (!metaContainer) return;
-
-		const metaData = {
-			title: metaContainer.querySelector('[name="title"]')?.value || '',
-			alt_text: metaContainer.querySelector('[name="alt_text"]')?.value || '',
-			caption: metaContainer.querySelector('[name="caption"]')?.value || '',
-			description: metaContainer.querySelector('[name="description"]')?.value || ''
-		};
-
-		// Update upload meta
-		upload.meta = { ...upload.meta, ...metaData };
-		this.uploads.set(upload.id, upload);
-
-		// Mark that we have meta changes
-		this.hasMetaChanges = true;
-
-		// Determine if upload has been sent to server
-		const isOnServer = upload.status === 'completed' && upload.attachmentId;
-
-		if (isOnServer) {
-			// Queue immediate update
-			await this.sendMetaUpdate(upload);
-		} else if (upload.operationId) {
-			// Wait for upload to complete, then send meta
-			this.queueDependentMetaUpdate(upload);
-		} else {
-			// Upload hasn't been queued yet, meta will be sent with initial upload
-			this.persistFieldState(field.key);
-		}
-	}
-
-	/**
-	 * Send meta update to server
-	 */
-	async sendMetaUpdate(upload) {
-		const formData = new FormData();
-		formData.append('attachment_id', upload.attachmentId);
-		formData.append('title', upload.meta.title);
-		formData.append('alt_text', upload.meta.alt_text);
-		formData.append('caption', upload.meta.caption);
-		formData.append('description', upload.meta.description);
-		//TODO:
-		// Send an array of attachment IDs with the changes, similar to the post editing logic
-		/**
-		 *  let data = {
-		 *  	items: {
-		 *      	uploadID: {
-		 *          	title: '',
-		 *          	alt: '',
-		 *          	caption: '',
-		 *         		depends_on: ''  <-- only necessary if uploadID is the generated upload_id
-		 *      	}
-		 *  	},
-		 *  	user: userID
-		 *  }
-		 *
-		 *  WHERE uploadID = attachment_id (if already uploaded) or our generated upload_id if the file hasn't been processed yet
-		 *
-		 */
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			data: formData,
-			title: `Updating metadata for ${upload.meta.originalName}`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
-
-		try {
-			await this.queue.addToQueue(operation);
-			// this.notifications.add('Metadata updated', 'success');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'sendMetaUpdate',
-				uploadId: upload.id
-			});
-		}
-	}
-
-	/**
-	 * Queue meta update that depends on upload completion
-	 */
-	queueDependentMetaUpdate(upload) {
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			dependencies: [upload.operationId],
-			data: () => {
-				// This function will be called when dependencies are resolved
-				const formData = new FormData();
-				formData.append('operation_id', upload.operationId);
-				formData.append('upload_id', upload.id);
-				formData.append('title', upload.meta.title);
-				formData.append('alt_text', upload.meta.alt_text);
-				formData.append('caption', upload.meta.caption);
-				formData.append('description', upload.meta.description);
-				return formData;
-			},
-			title: `Updating metadata after upload`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
-
-		this.queue.addToQueue(operation);
-	}
-	/*******************************************************************************
-	 IMAGE PROCESSING
-	 *******************************************************************************/
-	async processFiles(fieldId, files) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Hide upload container, show group display
-		if (field.ui.field.dropZone) {
-			field.ui.field.dropZone.hidden = true;
-		}
-		if (field.ui.groups.display) {
-			field.ui.groups.display.hidden = false;
-		}
-
-		const totalFiles = files.length;
-		let processedCount = 0;
-
-		// Show initial progress
-		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-
-		// Initialize field uploads set if needed
-		if (!field.uploads) {
-			field.uploads = new Set();
-		}
-
-		// Process files
-		const processPromises = Array.from(files).map(async (file, index) => {
-			try {
-				// Create upload ID
-				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-				// Create upload data
-				const uploadData = {
-					id: uploadId,
-					fieldId: fieldId,
-					originalFile: file,
-					processedFile: null,
-					preview: null,
-					status: 'local_processing',
-					element: null,
-					location: null,
-					meta: {
-						originalName: file.name,
-						size: file.size,
-						type: file.type
-					}
-				};
-
-				// Create preview URL
-				uploadData.preview = URL.createObjectURL(file);
-
-				// Process the file (resize if image)
-				if (file.type.startsWith('image/')) {
-					uploadData.processedFile = await this.processImage(file, field.subtype);
-				} else {
-					uploadData.processedFile = file;
-				}
-
-				// Store blob data separately in IndexedDB
-				if (this.db) {
-					try {
-						await this.storeBlobData(uploadId, uploadData.processedFile || file);
-					} catch (error) {
-						console.warn('Failed to store blob data:', error);
-					}
-				}
-
-				// Create DOM element
-				const subtype = this.getSubtypeFromMime(file.type);
-				uploadData.element = this.createImageElement({
-					...uploadData,
-					subtype: subtype
-				}, field.destination === 'post_group');
-
-				// Show progress on the item
-				this.showUploadProgress(uploadId, true);
-				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
-
-				// Add to preview grid
-				if (field.ui.field.preview) {
-					field.ui.field.preview.appendChild(uploadData.element);
-					uploadData.location = field.ui.field.preview;
-				}
-
-				// Store upload
-				this.uploads.set(uploadId, uploadData);
-				field.uploads.add(uploadId);
-
-				// Update progress
-				processedCount++;
-				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
-				this.updateUploadItemProgress(uploadId, 100, 'processed');
-				uploadData.status = 'processed';
-
-				// Fade out item progress after a moment
-				setTimeout(() => {
-					this.showUploadProgress(uploadId, false);
-				}, 1000);
-
-				return uploadId;
-
-			} catch (error) {
-				console.error('Error processing file:', file.name, error);
-				processedCount++;
-				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
-				return null;
-			}
-		});
-
-		// Wait for all files to process
-		await Promise.all(processPromises);
-
-		this.updateFieldState(fieldId);
-		// Cache the state (now without DOM references)
-		await this.persistFieldState(fieldId);
-
-		// Queue for upload if in direct mode
-		if (field.mode === 'direct' && field.destination !== 'post_group') {
-			await this.queueUpload(fieldId);
-		}
-
-		// Lock uploads if max reached
-		this.maybeLockUploads(fieldId);
-	}
-
-	updateFieldState(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.ui.field.field) return;
-
-		const container = field.ui.field.field;
-		const uploadCount = field.uploads?.size || 0;
-		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
-
-		// Set data attributes for CSS targeting
-		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
-		container.dataset.uploadCount = uploadCount.toString();
-		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
-
-		// Update ARIA labels for accessibility
-		if (field.ui.field.preview) {
-			field.ui.field.preview.setAttribute('aria-label',
-				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
-			);
-		}
-	}
-
-	/**
-	 * Store file blob data in IndexedDB
-	 */
-	async storeBlobData(uploadId, file) {
-		if (!this.db) return;
-
-		const blobData = {
-			uploadId: uploadId,
-			data: file,
-			name: file.name,
-			type: file.type,
-			lastModified: file.lastModified,
-			timestamp: Date.now()
-		};
-
-		try {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').put(blobData);
-		} catch (error) {
-			console.error('Failed to store blob data:', error);
-			throw error;
-		}
-	}
-
-	/**
-	 * Show/hide progress indicator on individual upload items
-	 */
-	showUploadProgress(uploadId, show = true) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (progressEl) {
-			if (show) {
-				progressEl.style.removeProperty('animation');
-				progressEl.hidden = false;
-			} else {
-				progressEl.style.animation = 'fadeOut var(--transition-base)';
-				setTimeout(() => {
-					progressEl.hidden = true;
-				}, 300);
-			}
-		}
-	}
-
-	/**
-	 * Update individual upload progress bar
-	 */
-	updateUploadItemProgress(uploadId, percent, status = null) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (!progressEl) return;
-
-		const fill = progressEl.querySelector('.fill');
-		const details = progressEl.querySelector('.details');
-		const icon = progressEl.querySelector('.icon');
-
-		if (fill) {
-			fill.style.width = `${percent}%`;
-		}
-
-		if (status && details) {
-			details.textContent = this.getStatusText(status);
-		}
-
-		if (status && icon) {
-			icon.innerHTML = this.getStatusIcon(status).outerHTML;
-		}
-	}
-	checkFieldLimits(fieldId, additionalFiles) {
-		const field = this.fields.get(fieldId);
-		if (!field) return false;
-
-		const currentCount = field.uploads?.size || 0;
-		const totalCount = currentCount + additionalFiles;
-
-		if (totalCount > field.maxFiles) {
-			// this.notifications.add(
-			// 	`Cannot add ${additionalFiles} files. Max ${field.maxFiles} allowed, currently have ${currentCount}.`,
-			// 	'warning'
-			// );
-			return false;
-		}
-
-		return true;
-	}
-	generateUploadId() {
-		return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-	}
-	validateFile(file, field) {
-		// Type validation
-		if (!this.settings.allowedTypes.includes(file.type)) {
-			this.notify(`Invalid file type: ${file.type}`, 'error');
-			return false;
-		}
-
-		// Size validation
-		if (file.size > this.settings.maxFileSize) {
-			this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
-			return false;
-		}
-
-		return true;
-	}
-
-	formatBytes(bytes, decimals = 2) {
-		if (bytes === 0) return '0 Bytes';
-
-		const k = 1024;
-		const dm = decimals < 0 ? 0 : decimals;
-		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-
-		const i = Math.floor(Math.log(bytes) / Math.log(k));
-
-		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
-	}
-
-	shouldProcessClientSide(file, subtype) {
-		// Only process images client-side
-		if (subtype === 'image' && file.type.startsWith('image/')) {
-			return true;
-		}
-
-		// Videos and documents go straight to server
-		return false;
-	}
-
-	async processBatch(fieldId, files) {
-		const results = [];
-		const processingQueue = [];
-		const maxConcurrent = this.worker.settings.maxConcurrent;
-
-		let total = files.length;
-		let processedCount = 0;
-
-		// Show initial progress
-		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-		let field = this.fields.get(fieldId);
-		// Initialize field uploads set if needed
-		if (!field.uploads) {
-			field.uploads = new Set();
-		}
-
-
-		for (let i = 0; i < files.length; i++) {
-			this.showUploadProgress(uploadId, true);
-			this.updateUploadProgress(fieldId, i, total);
-			// Wait if we've reached max concurrent processing
-			if (processingQueue.length >= maxConcurrent) {
-				await Promise.race(processingQueue);
-			}
-
-			const processPromise = this.processFile(fieldId, files[i])
-				.then(upload => {
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-
-					if (upload) results.push(upload);
-					return upload;
-				})
-				.catch(error => {
-					console.error(`Failed to process ${files[i].name}:`, error);
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-					return null;
-				});
-
-			processingQueue.push(processPromise);
-		}
-
-		// Wait for remaining files
-		await Promise.all(processingQueue);
-		return results;
-	}
-
-	async processFile(fieldId, file) {
-		const field = this.fields.get(fieldId);
-
-		const upload = await this.setUpload(fieldId, file);
-
-		if (!upload) {
-			return null;
-		}
-		if (!this.shouldProcessClientSide(file, field.subtype)) {
-			return upload;
-		}
-		const uploadId = upload.id;
-		try {
-			// Update UI immediately
-			this.addImageToGroup(uploadId);
-			this.updateUploadStatus(uploadId, 'local_processing');
-
-			// Attempt to process the image
-			let processedFile = null;
-			let processingFailed = false;
-
-			try {
-				processedFile = await this.processImage(file, uploadId);
-			} catch (error) {
-				console.warn(`Processing failed for ${file.name}, using original:`, error);
-				processingFailed = true;
-				processedFile = file; // Use original
-			}
-
-			// Update upload with processed file
-			upload.processedFile = processedFile;
-			upload.processingFailed = processingFailed;
-
-			// Update status
-			this.updateUploadStatus(uploadId, 'processed');
-
-			// Save to uploads map
-			this.uploads.set(uploadId, upload);
-
-			// Persist state
-			if (field && field.key) {
-				await this.persistFieldState(field.key);
-			}
-
-			const message = processingFailed
-				? `${file.name} added (original format)`
-				: `${file.name} processed and ready`;
-			this.a11y.announce(message);
-
-			return upload;
-
-		} catch (error) {
-			// Clean up failed upload
-			this.cleanupFailedUpload(uploadId, field.key);
-
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'processFile',
-				uploadId,
-				fileName: file.name
-			});
-
-			return null;
-		}
-	}
-
-	async processImage(file, uploadId) {
-		const timeout = this.worker.settings.timeout;
-
-		return new Promise((resolve, reject) => {
-			let timeoutId;
-			let taskCompleted = false;
-
-			// Set timeout
-			timeoutId = setTimeout(() => {
-				if (!taskCompleted) {
-					taskCompleted = true;
-
-					// Remove from active tasks
-					this.worker.tasks.delete(uploadId);
-
-					// Maybe restart worker if configured
-					if (this.worker.settings.restartAfterTimeout) {
-						this.restartCompressionWorker();
-					}
-
-					reject(new Error(`Processing timeout for ${file.name}`));
-				}
-			}, timeout);
-
-			// Track this task
-			this.worker.tasks.set(uploadId, { file, timeoutId });
-
-			// Process image
-			this.handleProcess(file, uploadId)
-				.then(result => {
-					if (!taskCompleted) {
-						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) {
-		// Skip non-images
-		if (!file.type.startsWith('image/')) {
-			return file;
-		}
-
-		const maxDimension = this.getMaxDimension();
-		const quality = 0.85;
-
-		// Try worker first if available
-		if (this.shouldUseWorker(file)) {
-			try {
-				// Ensure worker is initialized
-				if (!this.worker.worker) {
-					this.initCompressionWorker();
-				}
-
-				if (this.worker.worker) {
-					return await this.processWithWorker(file, uploadId, maxDimension, quality);
-				}
-			} catch (error) {
-				console.warn('Worker processing failed, falling back to main thread:', error);
-			}
-		}
-
-		// Fallback to main thread
-		return await this.processOnMainThread(file, maxDimension, quality);
-	}
-
-	/**
-	 * Process image on main thread with better error handling
-	 */
-	async processOnMainThread(file, maxDimension, quality) {
-		return new Promise((resolve, reject) => {
-			const img = new Image();
-			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;
-				}
-				// Explicitly clean up canvas
-				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;
-
-					// Enhanced image smoothing
-					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 = URL.createObjectURL(file);
-				img.src = objectUrl;
-			} catch (error) {
-				cleanup();
-				reject(new Error(`Failed to create object URL: ${error.message}`));
-			}
-		});
-	}
-
-	/**
-	 * Get optimal output format
-	 */
-	getOptimalFormat(file) {
-		// Keep original format for certain types
-		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
-			return file.type;
-		}
-
-		// Use WebP if supported, otherwise JPEG
-		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
-	}
-
-	/**
-	 * Get optimal quality setting
-	 */
-	getOptimalQuality(file, requestedQuality) {
-		// Higher quality for smaller files
-		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
-		if (file.size < 2 * 1024 * 1024) return requestedQuality;
-
-		// Lower quality for very large files
-		return Math.min(requestedQuality, 0.8);
-	}
-
-	/**
-	 * Generate processed file name
-	 */
-	getProcessedFileName(originalFile, outputFormat) {
-		const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
-
-		const extensions = {
-			'image/webp': '.webp',
-			'image/jpeg': '.jpg',
-			'image/png': '.png',
-			'image/gif': '.gif'
-		};
-
-		return baseName + (extensions[outputFormat] || '.jpg');
-	}
-
-	/**
-	 * Get maximum dimension based on device capabilities
-	 */
-	getMaxDimension() {
-		const screenWidth = window.screen.width;
-		const devicePixelRatio = window.devicePixelRatio || 1;
-
-		// Scale based on device capabilities
-		if (screenWidth * devicePixelRatio > 2560) return 2400;
-		if (screenWidth * devicePixelRatio > 1920) return 1920;
-		return 1200;
-	}
-
-	/**
-	 * Determine if we should use Web Worker
-	 */
-	shouldUseWorker(file) {
-		// Use worker for large files or when available
-		return this.worker.worker &&
-			file.size > 1024 * 1024 && // > 1MB
-			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;
-			}
-
-			// Create unique message ID for this task
-			const messageId = `${uploadId}_${Date.now()}`;
-
-			// Handler for this specific message
-			const messageHandler = (e) => {
-				if (e.data.messageId !== messageId) return;
-
-				// Remove handler
-				this.worker.worker.removeEventListener('message', messageHandler);
-				this.worker.worker.removeEventListener('error', errorHandler);
-
-				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}`));
-			};
-
-			// Add handlers
-			this.worker.worker.addEventListener('message', messageHandler);
-			this.worker.worker.addEventListener('error', errorHandler);
-
-			// Send message to worker
-			this.worker.worker.postMessage({
-				messageId,
-				file,
-				maxDimension,
-				quality,
-				outputFormat: this.getOptimalFormat(file)
-			});
-		});
-	}
-
-	/**
-	 * Restart compression worker
-	 */
-	restartCompressionWorker() {
-		console.log('Restarting compression worker...');
-
-		// Terminate existing worker
-		if (this.worker.worker) {
-			this.worker.worker.terminate();
-			this.worker.worker = null;
-		}
-
-		// Clear active tasks
-		this.worker.tasks.clear();
-
-		// Check restart limit
-		if (this.worker.restart.count >= this.worker.restart.max) {
-			console.error('Max worker restarts reached, disabling worker');
-			return;
-		}
-
-		this.worker.restart.count++;
-
-		// Reinitialize
-		this.initCompressionWorker();
-	}
-
-	/**
-	 * Initialize Web Worker for image compression
-	 */
-	initCompressionWorker() {
-		if (this.worker.worker || typeof Worker === 'undefined') return;
-
-		try {
-			const workerScript = `
-            self.onmessage = async function(e) {
-                const { messageId, file, maxDimension, quality, outputFormat } = e.data;
-
-                try {
-                    // Create ImageBitmap from file
-                    const bitmap = await createImageBitmap(file);
-
-                    // Calculate dimensions
-                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
-                    const width = Math.round(bitmap.width * scale);
-                    const height = Math.round(bitmap.height * scale);
-
-                    // Create OffscreenCanvas
-                    const canvas = new OffscreenCanvas(width, height);
-                    const ctx = canvas.getContext('2d');
-
-                    // Draw and resize
-                    ctx.imageSmoothingEnabled = true;
-                    ctx.imageSmoothingQuality = 'high';
-                    ctx.drawImage(bitmap, 0, 0, width, height);
-
-                    // Clean up bitmap
-                    bitmap.close();
-
-                    // Convert to blob
-                    const blob = await canvas.convertToBlob({
-                        type: outputFormat,
-                        quality: quality
-                    });
-
-                    self.postMessage({
-                        messageId,
-                        success: true,
-                        blob: blob,
-                        format: outputFormat
-                    });
-
-                } catch (error) {
-                    self.postMessage({
-                        messageId,
-                        success: false,
-                        error: error.message
-                    });
-                }
-            };
-        `;
-
-			const blob = new Blob([workerScript], { type: 'application/javascript' });
-			this.worker.worker = new Worker(URL.createObjectURL(blob));
-
-		} catch (error) {
-			console.warn('Failed to initialize compression worker:', error);
-			this.worker.worker = null;
-		}
-	}
-
-	/**
-	 * Calculate optimal dimensions with aspect ratio preservation
-	 */
-	calculateOptimalDimensions(img, maxDimension) {
-		let { width, height } = img;
-
-		// Don't upscale
-		if (width <= maxDimension && height <= maxDimension) {
-			return { width, height };
-		}
-
-		// Calculate scale factor
-		const scale = Math.min(maxDimension / width, maxDimension / height);
-
-		return {
-			width: Math.round(width * scale),
-			height: Math.round(height * scale)
-		};
-	}
-
-
-	/**
-	 * Check WebP support
-	 */
-	supportsWebP() {
-		const canvas = document.createElement('canvas');
-		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
-	}
-
-	/**
-	 * Clean up failed upload
-	 */
-	cleanupFailedUpload(uploadId, fieldId) {
-		const field = this.fields.get(fieldId);
-		if (field?.uploads) {
-			field.uploads.delete(uploadId);
-		}
-
-		const upload = this.uploads.get(uploadId);
-		if (upload) {
-			// Clean up preview URL
-			if (upload.preview?.startsWith('blob:')) {
-				URL.revokeObjectURL(upload.preview);
-			}
-
-			// Remove element
-			upload.element?.remove();
-
-			// Remove from uploads
-			this.uploads.delete(uploadId);
-		}
-
-		// Remove from active tasks
-		this.worker.tasks.delete(uploadId);
-	}
-	/*******************************************************************************
-	 UI FUNCTIONALITY
-	 *******************************************************************************/
-	/**
-	 * Update upload status correctly
-	 */
-	updateUploadStatus(uploadId, status) {
-		console.log('Updating upload status for: ', uploadId);
-		let upload = this.uploads.get(uploadId);
-		if(!upload) {
-			return;
-		}
-		upload.status = status;
-
-		this.updateImageUI(upload.id);
-		this.persistFieldState(upload.fieldId);
-	}
-	updateImageUI(uploadId) {
-		console.log('Updating image UI: ', uploadId);
-		const upload = this.uploads.get(uploadId);
-		console.log(upload);
-		if (!upload?.element) return;
-
-
-		const progressEl = upload.element.querySelector('.progress');
-		const itemEl = upload.element;
-
-		console.log('Updating Upload UI:', upload);
-		// Update status class on item for CSS styling
-		if (itemEl) {
-			itemEl.className = itemEl.className.replace(/status-[\w-]+/g, '');
-			itemEl.classList.add(`status-${upload.status}`);
-		}
-
-		if (progressEl) {
-			let icon = this.getStatusIcon(upload.status);
-			let message = this.getStatusText(upload.status);
-			let progress = this.getStatusProgress(upload.status);
-
-			const fill = progressEl.querySelector('.fill');
-			const itemIcon = progressEl.querySelector('span.icon');
-			const itemMessage = progressEl.querySelector('span.details');
-
-			if (fill) {
-				fill.style.width = `${progress}%`;
-			}
-			if (itemMessage) itemMessage.textContent = message;
-			if (itemIcon) {
-				window.removeChildren(itemIcon);
-				itemIcon.append(icon);
-			}
-
-			if (upload.status === 'completed') {
-				setTimeout(() => {
-					if (progressEl) {
-						window.fade(progressEl, false);
-					}
-				}, 1000);
-			}
-		}
-	}
-	/**
-	 * Hide the uploader drop zone if we have reached our limit
-	 */
-	maybeLockUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		if (field.ui.field.dropZone) {
-			const hasUploads = field.uploads && field.uploads.size > 0;
-			const atMaxFiles = field.uploads && field.uploads.size >= field.maxFiles;
-
-			// Hide if we have uploads OR if we're at max files
-			field.ui.field.dropZone.hidden = hasUploads || atMaxFiles;
-		}
-	}
-	createImageElement(upload, draggable = false) {
-		let image = window.getTemplate('uploadItem');
-		if (!image) {
-			console.error('Image template not found');
-			return;
-		}
-		image.dataset.uploadId = upload.id;
-		console.log(upload);
-		if (upload.originalFile) {
-			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
-		}
-
-
-		image.querySelector('[name="featured"]').value = upload.id;
-		let [
-			featured,
-			img,
-			video,
-			preview,
-			details
-		] = [
-			image.querySelector('[name="featured"]'),
-			image.querySelector('img'),
-			image.querySelector('video'),
-			image.querySelector('label > span'),
-			image.querySelector('details')
-		];
-		[
-			featured.value,
-			img.src,
-			img.alt
-		] = [
-			upload.id,
-			upload.preview,
-			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
-		];
-
-		switch (image.dataset.subtype) {
-			case 'image':
-				[
-					img.src,
-					img.alt
-				] = [
-					upload.preview,
-					upload.originalFile?.name ?? upload.meta?.originalName?? ''
-				];
-				video.remove();
-				preview.remove();
-				break;
-			case 'video':
-				video.src = upload.preview;
-				img.remove();
-				preview.remove();
-				break;
-			case 'document':
-				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;
-		}
-		if (details) {
-			let template = window.getTemplate('uploadMeta');
-			if (template){
-				details.append(template);
-			}
-		}
-		image.draggable = draggable;
-
-		// Update input IDs safely
-		image.querySelectorAll('input').forEach(input => {
-			let id = input.id;
-			if (id) {
-				let newId = id + upload.id;
-				let label = input.parentNode.querySelector(`label[for="${id}"]`);
-				input.id = newId;
-				if (label) {
-					label.htmlFor = newId;
-				}
-			}
-		});
-
-		return image;
-	}
-
-
-	getSubtypeFromMime(mimeType) {
-		if (mimeType.startsWith('image/')) return 'image';
-		if (mimeType.startsWith('video/')) return 'video';
-		return 'document';
-	}
-
-	updateUploadProgress(fieldId, current, total, message) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		let progressBar = field.ui.field.progress.progress;
-
-		// Create progress bar if it doesn't exist
-		if (!progressBar) {
-			progressBar = window.getTemplate('imageProgress');
-
-			if (!progressBar) {
-				console.warn('Progress bar template not found');
-				return;
-			}
-
-			// Insert after drop zone or at top of container
-			const container = field.ui.field.field;
-			const insertAfter = field.ui.field.dropZone;
-
-			if (insertAfter) {
-				insertAfter.insertAdjacentElement('afterend', progressBar);
-			} else if (container) {
-				container.prepend(progressBar);
-			}
-
-			// Update the field UI reference to match actual structure
-			if (!field.ui.field.progress) {
-				field.ui.field.progress = {};
-			}
-			field.ui.field.progress = {
-				progress: progressBar,
-				bar: progressBar.querySelector('.bar'),
-				fill: progressBar.querySelector('.fill'),
-				details: progressBar.querySelector('.details'),
-				text: progressBar.querySelector('.details .text'),
-				count: progressBar.querySelector('.details .count')
-			};
-		}
-
-
-		progressBar.hidden = false;
-		progressBar.style.display = 'flex';
-		progressBar.style.animation = 'none';
-		progressBar.style.opacity = '1';
-
-		// Update progress bar
-		const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
-		const progressFill = field.ui.field.progress.fill;
-		const progressText = field.ui.field.progress.text;
-		const progressCount = field.ui.field.progress.count;
-
-		if (progressFill) {
-			progressFill.style.width = `${progressPercent}%`;
-		}
-
-		if (progressText) {
-			progressText.textContent = message;
-		}
-
-		if (progressCount) {
-			progressCount.textContent = `${current}/${total}`;
-		}
-
-		// Hide when complete
-		if (current >= total) {
-			setTimeout(() => {
-				progressBar.style.animation = 'fadeOut var(--transition-base)';
-				setTimeout(() => {
-					progressBar.hidden = true;
-					progressBar.style.display = 'none';
-				}, 300);
-			}, 1000);
-		}
-	}
-
-	hideUploadProgress(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const progressBar = field.ui.field.progress.progress;
-		if (progressBar) {
-			window.fade(progressBar, false);
-		}
-	}
-	/*******************************************************************************
-	 INDEXEDDB CACHE FUNCTIONALITY
-	 *******************************************************************************/
-	async initDB() {
-		if (!('indexedDB' in window)) return;
-
-		const request = indexedDB.open(`jvb_uploads_db`, 1);
-
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-			if (!db.objectStoreNames.contains('fieldStates')) {
-				const store = db.createObjectStore('fieldStates', { keyPath: 'fieldId' });
-				store.createIndex('timestamp', 'timestamp', { unique: false });
-				store.createIndex('content', 'content', { unique: false });
-				store.createIndex('itemId', 'itemId', { unique: false });
-			}
-
-			// Blob storage remains separate for performance
-			if (!db.objectStoreNames.contains('uploadBlobs')) {
-				db.createObjectStore('uploadBlobs', { keyPath: 'uploadId' });
-			}
-		};
-
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFields();
-			this.checkPendingUploads();
-		};
-
-		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
-		};
-	}
-
-	async loadFields() {
-		if (!this.db) return;
-
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readonly');
-			const fieldStates = tx.objectStore('fieldStates');
-			const blobStore = tx.objectStore('uploadBlobs');
-			const request = fieldStates.getAll();
-
-			request.onsuccess = (e) => {
-				e.target.result.forEach(field => {
-					let uploads = field.uploads;
-					let uploadIds = uploads.map(upload => upload.id);
-					field.uploads = new Set(uploadIds);
-					this.fields.set(field.key, field);
-					uploads.forEach(upload => {
-						this.uploads.set(upload.id, upload);
-					});
-				});
-				this.notify('uploads-loaded', { items: Array.from(this.uploads.values()) });
-				resolve();
-			};
-
-			const blobRequest = blobStore.getAll();
-
-			blobRequest.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					this.uploadBlobs.set(item.id, item);
-				});
-				this.notify('blobs-loaded', { items: Array.from(this.uploadBlobs.values()) });
-				resolve();
-			};
-		});
-	}
-
-	getUpload(uploadId) {
-		return this.uploads.get(uploadId);
-	}
-
-	clearField(fieldId) {
-		let uploads = Array.from(this.fields.uploads);
-		uploads.forEach(upload => {
-			this.uploads.delete(upload);
-		});
-		this.fields.delete(fieldId);
-		if (this.db) {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-			tx.objectStore('fieldStates').delete(fieldId);
-			uploads.forEach(upload => {
-				tx.objectStore('uploadBlobs').delete(upload);
-			});
-		}
-	}
-
-	updateFieldStatus(fieldId, status) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		field.uploads.forEach(upload => {
-			console.log('Attempting to set upload to status: ', status);
-			this.updateUploadStatus(upload, status);
-		});
-
-		// Update UI based on status
-		const container = field.ui.field.field;
-		if (container) {
-			container.dataset.uploadStatus = status;
-
-			// Show/hide relevant UI elements
-			const submitBtn = container.querySelector('.submit-uploads');
-			if (submitBtn) {
-				submitBtn.disabled = status === 'uploading' || status === 'processing';
-			}
-		}
-	}
-
-	/**
-	 * Handle successful upload completion
-	 */
-	handleUploadComplete(operation) {
-		const response = operation.response;
-		if (!response?.uploads) return;
-
-		// Map server IDs to uploads
-		response.uploads.forEach(serverUpload => {
-			const upload = this.uploads.get(serverUpload.upload_id);
-			if (upload) {
-				upload.attachmentId = serverUpload.attachment_id;
-				this.updateUploadStatus(serverUpload.upload_id, 'completed');
-				this.uploads.set(upload.id, upload);
-
-				// Clear from cache since it's now on server
-				this.clearUpload(upload.id);
-			}
-		});
-
-		// Persist updated field state
-		const fieldKey = operation.data.get('field_key');
-		if (fieldKey) {
-			this.persistFieldState(fieldKey);
-		}
-	}
-
-	/**
-	 * Clear individual upload from cache after successful server upload
-	 */
-	async clearUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		// Clean up preview URL
-		if (upload.preview?.startsWith('blob:')) {
-			URL.revokeObjectURL(upload.preview);
-		}
-
-		this.persistFieldState(upload.fieldId);
-		// Remove from memory
-		this.uploads.delete(uploadId);
-		this.uploadBlobs.delete(uploadId);
-
-		// Remove from IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').delete(uploadId);
-		}
-	}
-
-	/**
-	 * Store upload with DataStore integration
-	 */
-	async setUpload(fieldId, file, uploadId = null) {
-		if (!uploadId) {
-			uploadId = this.generateUploadId();
-		}
-		const upload = {
-			id: uploadId,
-			fieldId: fieldId,
-			groupId: null,
-			originalFile: file,
-			processedFile: null,
-			status: 'received',
-			progress: { percent: 0, message: 'Received...' },
-			preview: URL.createObjectURL(file),
-			createdAt: Date.now(),
-			meta: {
-				title: '',
-				alt_text: '',
-				caption: '',
-				originalName: file.name,
-				originalType: file.type,
-				originalSize: file.size
-			},
-			changes: {}
-		};
-
-		// Add to field
-		const field = this.fields.get(fieldId);
-		if (!field) {
-			console.error(`Field ${fieldId} not found`);
-			return null;
-		}
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadId);
-
-		upload.element = this.createImageElement(upload, field.type==='groupable');
-		upload.ui = window.uiFromSelectors(this.selectors.item, upload.element);
-
-		// Store in memory
-		this.uploads.set(uploadId, upload);
-		this.updateImageUI(uploadId);
-
-		// Persist to DataStore
-		await this.persistFieldState(fieldId);
-
-		return upload;
-	}
-
-	/**
-	 * Get uploads for a field, optionally cleaned for storage
-	 * @param {string} fieldId
-	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
-	 * @returns {Array}
-	 */
-	getFieldUploads(fieldId, clean = false) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.uploads) return [];
-
-		return Array.from(field.uploads)
-			.map(uploadId => {
-				const upload = this.uploads.get(uploadId);
-				if (!upload) return null;
-
-				if (clean) {
-					// Return cleaned version without DOM references
-					return {
-						id: upload.id,
-						fieldId: upload.fieldId,
-						status: upload.status,
-						preview: upload.preview,
-						attachmentId: upload.attachmentId,
-						operationId: upload.operationId,
-						groupId: upload.groupId || null,
-						meta: {
-							originalName: upload.meta?.originalName || upload.originalFile?.name,
-							size: upload.meta?.size || upload.originalFile?.size,
-							type: upload.meta?.type || upload.originalFile?.type,
-							title: upload.meta?.title,
-							alt: upload.meta?.alt,
-							caption: upload.meta?.caption
-						}
-					};
-				}
-
-				// Return full upload object
-				return upload;
-			})
-			.filter(Boolean);
-	}
-
-	/**
-	 * Persist upload to DataStore
-	 */
-	async persistFieldState(fieldId) {
-		if (!this.db) return;
-
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Create clean field config
-		const { ui, ...cleanConfig } = field;
-
-		const fieldState = {
-			fieldId: fieldId,
-			timestamp: Date.now(),
-
-			config: {
-				...cleanConfig,
-				fieldName: field.name,
-				dataField: field.ui?.field?.field?.dataset?.field
-			},
-
-			// Recovery context with normalized URL
-			context: {
-				url: this.normalizeUrl(window.location.href),
-				fullUrl: window.location.href, // Keep for reference
-				modalType: this.getModalType(field),
-				formId: field.formId,
-				// **FIX**: Store additional identifiers
-				fieldSelector: `.field.upload[data-field="${field.name}"]`
-			},
-
-			// Uploads (cleaned of DOM references and blob URLs)
-			uploads: this.getFieldUploads(fieldId, true).map(upload => {
-				// **FIX**: Don't store blob URLs as they become invalid
-				const { preview, element, location, ...cleanUpload } = upload;
-				return cleanUpload;
-			}),
-
-			// Groups structure
-			groups: Array.from(this.groups.entries())
-				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
-				.map(([id, data]) => ({
-					id: data.id,
-					uploads: Array.from(data.uploads),
-					meta: data.meta || {},
-					changes: data.changes || {}
-				}))
-		};
-
-		try {
-			const tx = this.db.transaction(['fieldStates'], 'readwrite');
-			await tx.objectStore('fieldStates').put(fieldState);
-		} catch (error) {
-			console.error('Failed to persist field state:', error);
-		}
-	}
-
-	normalizeUrl(url) {
-		try {
-			const urlObj = new URL(url);
-			// Return just the origin + pathname (no query string or hash)
-			return urlObj.origin + urlObj.pathname;
-		} catch (e) {
-			return url;
-		}
-	}
-	/*******************************************************************************
-	 RESTORE FUNCTIONALITY
-	 *******************************************************************************/
-	async checkPendingUploads() {
-		console.log('Checking for pending uploads');
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const fieldStore = tx.objectStore('fieldStates');
-
-		const allFieldStates = await new Promise(resolve => {
-			const request = fieldStore.getAll();
-			request.onsuccess = () => resolve(request.result);
-		});
-
-		console.log('All Field States', allFieldStates);
-
-		// ADD DETAILED LOGGING HERE:
-		allFieldStates.forEach(field => {
-			console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
-			field.uploads.forEach((upload, idx) => {
-				console.log(`  Upload ${idx}:`, {
-					id: upload.id,
-					status: upload.status,
-					operationId: upload.operationId,
-					hasOperationId: !!upload.operationId
-				});
-			});
-		});
-
-		// Filter for pending uploads (not yet sent to server)
-		const pendingFields = allFieldStates.filter(field =>
-			field.uploads.some(upload =>
-				// If no operationId, it hasn't been sent to server yet
-				!upload.operationId &&
-				// And it's been processed locally
-				(upload.status === 'completed' ||
-					upload.status === 'processed' ||
-					upload.status === 'local_processing' ||
-					upload.status === 'processed-original')
-			)
-		);
-
-		console.log('Pending Fields: ', pendingFields);
-
-		if (pendingFields.length === 0) return;
-
-		// Show recovery notification
-		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) {
-			message = `${totalGroups} organized group(s) with ${totalUploads} upload(s) ready to submit.`;
-		} 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) {
-			console.log('Field to restore:', field);
-			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 (const upload of field.uploads) {
-
-				let uploadItem = window.getTemplate('uploadItem');
-				if (!uploadItem) continue;
-				//
-				// 	const imgEl = uploadItem.querySelector('img');
-				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
-				//
-				const blobData = await this.getBlobData(upload.id);
-
-
-				if (blobData) {
-					try {
-						// Create new blob URL from stored data
-						const blob = new Blob([blobData.data], { type: blobData.type });
-						const previewUrl = URL.createObjectURL(blob);
-
-						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;
-
-						let subtype = this.getSubtypeFromMime(blobData.type);
-						console.log(subtype);
-						uploadItem.dataset.subtype = subtype;
-						switch (subtype) {
-							case 'image':
-								[
-									img.src,
-									img.alt
-								] = [
-									previewUrl,
-									upload.originalFile?.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);
-		}
-
-		// Event handlers
-		const restoreBtn = notification.querySelector('.restore-selected');
-		if (restoreBtn) {
-			restoreBtn.addEventListener('click', () => {
-				const selectedUploads = this.getSelectedRestorationUploads(notification);
-				if (selectedUploads.length === 0) {
-					// this.notifications.add('No uploads selected for restoration', 'warning');
-					return;
-				}
-				this.restoreSelectedUploads(selectedUploads);
-
-				// Clean up blob URLs before removing notification
-				this.cleanupRestoreNotificationUrls(notification);
-				notification.remove();
-			});
-		}
-
-		const dismissBtn = notification.querySelector('.dismiss-cache-check');
-		if (dismissBtn) {
-			dismissBtn.addEventListener('click', () => {
-				sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(pendingFields));
-				// this.notifications.add('Uploads saved for later restoration', 'info');
-
-				// Clean up blob URLs
-				this.cleanupRestoreNotificationUrls(notification);
-				notification.remove();
-			});
-		}
-
-		const clearBtn = notification.querySelector('.restart-uploads');
-		if (clearBtn) {
-			clearBtn.addEventListener('click', async () => {
-				const confirmed = confirm('This will permanently delete all cached uploads. Continue?');
-				if (confirmed) {
-					await this.clearCachedUploads(pendingFields);
-
-					// Clean up blob URLs
-					this.cleanupRestoreNotificationUrls(notification);
-					notification.remove();
-				}
-			});
-		}
-
-		document.querySelector('main').appendChild(notification);
-		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.restoreModal.subscribe((event, data) => {
-			if (event === 'modal-close') {
-				this.cleanupStoredRestoration();
-			}
-		});
-	}
-
-	cleanupStoredRestoration() {
-		//TODO delete saved uploads from cache, cleanup blobs
-	}
-
-	cleanupRestoreNotificationUrls(notification) {
-		notification.querySelectorAll('[data-preview-url]').forEach(item => {
-			const url = item.dataset.previewUrl;
-			if (url && url.startsWith('blob:')) {
-				URL.revokeObjectURL(url);
-			}
-		});
-	}
-
-	getSelectedRestorationUploads(notificationEl) {
-		const selected = [];
-		const checkboxes = notificationEl.querySelectorAll('.restore-checkbox:checked');
-
-		checkboxes.forEach(checkbox => {
-			const item = checkbox.closest('label');
-			if (item) {
-				selected.push({
-					uploadId: item.dataset.uploadId,
-					fieldId: item.dataset.fieldId
-				});
-			}
-		});
-
-		return selected;
-	}
-
-	async restoreSelectedUploads(selectedUploads) {
-		// Group by field
-		const byField = new Map();
-		selectedUploads.forEach(item => {
-			if (!byField.has(item.fieldId)) {
-				byField.set(item.fieldId, []);
-			}
-			byField.get(item.fieldId).push(item.uploadId);
-		});
-
-		// Get full field states from IndexedDB
-		if (!this.db) {
-			// this.notifications.add('Cannot restore: Database not available', 'error');
-			return;
-		}
-
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const store = tx.objectStore('fieldStates');
-
-		for (const [fieldId, uploadIds] of byField.entries()) {
-			const request = store.get(fieldId);
-			const fieldState = await new Promise(resolve => {
-				request.onsuccess = () => resolve(request.result);
-				request.onerror = () => resolve(null);
-			});
-
-			if (fieldState) {
-				// Filter to only selected uploads
-				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
-				await this.restoreField(fieldState);
-			}
-		}
-
-		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
-	}
-
-	async restoreField(fieldState) {
-		const { config, context, uploads, groups } = 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.fields.has(fieldKey)) {
-			fieldKey = this.registerUploader(fieldElement, config);
-		}
-
-		const field = this.fields.get(fieldKey);
-		if (!field) {
-			console.error('Failed to register field for restoration');
-			return;
-		}
-
-		// ADDED: Ensure UI groups structure is initialized
-		if (!field.ui.groups) {
-			field.ui.groups = {};
-		}
-		if (!field.ui.groups.groups) {
-			field.ui.groups.groups = new Map();
-		}
-
-		// Make sure we have the container and empty group references
-		if (!field.ui.groups.container) {
-			field.ui.groups.container = fieldElement.querySelector('.item-grid.groups');
-		}
-		if (!field.ui.groups.empty) {
-			field.ui.groups.empty = fieldElement.querySelector('.empty-group');
-		}
-
-		// Restore uploads
-		for (const uploadData of uploads) {
-			await this.restoreUpload(field, uploadData);
-		}
-
-		// Restore groups
-		if (groups && groups.length > 0) {
-			await this.restoreGroups(field, groups, uploads);
-		}
-
-		// Update UI
-		this.updateFieldState(fieldKey);
-		this.maybeLockUploads(fieldKey);
-
-		// Queue for upload if needed (should not happen for post_group)
-		if (config.mode === 'direct' && config.destination !== 'post_group') {
-			await this.queueUpload(fieldKey);
-		}
-	}
-
-	async restoreUpload(field, uploadData) {
-		// Try to get blob data from IndexedDB
-		const blobData = await this.getBlobData(uploadData.id);
-
-		if (blobData) {
-			// Recreate file from blob data
-			const file = new File(
-				[blobData.data],
-				blobData.name,
-				{ type: blobData.type, lastModified: blobData.lastModified }
-			);
-
-			uploadData.originalFile = file;
-			uploadData.processedFile = file;
-
-			// **FIX**: Create fresh blob URL since old one is invalid
-			uploadData.preview = URL.createObjectURL(file);
-		} else {
-			console.warn('Blob data not found for upload:', uploadData.id);
-			return; // Skip this upload if we can't restore the file
-		}
-
-		// Add to field
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadData.id);
-
-		// Recreate DOM element
-		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
-		uploadData.element = this.createImageElement({
-			...uploadData,
-			subtype: subtype
-		}, field.destination === 'post_group');
-
-		// Restore to correct location
-		let location;
-		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
-			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
-		} else {
-			location = field.ui.field.preview;
-		}
-
-		if (location) {
-			location.appendChild(uploadData.element);
-			uploadData.location = location;
-		}
-
-		// Store in memory
-		this.uploads.set(uploadData.id, uploadData);
-	}
-
-	async restoreFieldStates(fieldStates) {
-		// Group by URL
-		const byUrl = new Map();
-		fieldStates.forEach(field => {
-			if (!byUrl.has(field.context.url)) {
-				byUrl.set(field.context.url, []);
-			}
-			byUrl.get(field.context.url).push(field);
-		});
-
-		// If all on current page, restore directly
-		if (byUrl.size === 1 && byUrl.has(window.location.href)) {
-			for (const fieldState of fieldStates) {
-				await this.restoreField(fieldState);
-			}
-			// this.notifications.add(`Restored ${fieldStates.length} field(s)`, 'success');
-		} else {
-			// Store intent to restore and navigate
-			sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(fieldStates));
-
-			// Navigate to first URL
-			const firstUrl = byUrl.keys().next().value;
-			if (window.location.href !== firstUrl) {
-				window.location.href = firstUrl;
-			}
-		}
-	}
-
-	async restoreGroups(field, groups, uploads) {
-		// Ensure the groups.groups Map exists
-		if (!field.ui.groups.groups) {
-			field.ui.groups.groups = new Map();
-		}
-
-		for (const groupData of groups) {
-			// Create group element
-			const groupElement = this.createGroupElement(groupData.id, field.key);
-
-			// Store in field UI Map
-			field.ui.groups.groups.set(groupData.id, groupElement);
-
-			// Insert into DOM
-			if (field.ui.groups.container && field.ui.groups.empty) {
-				field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-			} else if (field.ui.groups.container) {
-				field.ui.groups.container.appendChild(groupElement);
-			}
-
-			// FIXED: Create proper group structure matching createGroup()
-			this.groups.set(groupData.id, {
-				id: groupData.id,
-				fieldId: field.key,
-				element: groupElement,
-				uploads: new Set(groupData.uploads), // FIXED: was groupData.uploadIds
-				meta: groupData.meta || {},
-				changes: groupData.changes || {}
-			});
-
-			// Move uploads to group
-			// FIXED: use groupData.uploads instead of groupData.uploadIds
-			groupData.uploads.forEach(uploadId => {
-				const upload = uploads.find(u => u.id === uploadId);
-				if (upload && upload.element) {
-					const groupGrid = groupElement.querySelector('.item-grid');
-					if (groupGrid) {
-						groupGrid.appendChild(upload.element);
-						upload.location = groupGrid;
-						upload.groupId = groupData.id;
-					}
-				}
-			});
-		}
-	}
-
-	async getBlobData(uploadId) {
-		if (!this.db) return null;
-
-		const tx = this.db.transaction(['uploadBlobs'], 'readonly');
-		const request = tx.objectStore('uploadBlobs').get(uploadId);
-
-		return new Promise(resolve => {
-			request.onsuccess = () => resolve(request.result);
-			request.onerror = () => resolve(null);
-		});
-	}
-
-	async openModalForRestore(context) {
-		const { modalType, formId } = context;
-
-		// Find and click the appropriate button to open the modal
-		let trigger = null;
-
-		switch(modalType) {
-			case 'create':
-				trigger = document.querySelector('[data-action="create"]');
-				break;
-			case 'edit':
-				// Need to find the specific edit button
-				trigger = document.querySelector(`[data-action="edit"][data-id="${context.itemId}"]`);
-				break;
-			case 'bulkEdit':
-				trigger = document.querySelector('[data-action="bulk-edit"]');
-				break;
-		}
-
-		if (trigger) {
-			trigger.click();
-
-			// Wait for modal to open
-			await new Promise(resolve => setTimeout(resolve, 300));
-		}
-	}
-
-	async clearCachedUploads(fieldStates) {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-
-		for (const field of fieldStates) {
-			// Delete field state
-			await tx.objectStore('fieldStates').delete(field.fieldId);
-
-			// Delete all associated blobs
-			for (const upload of field.uploads) {
-				await tx.objectStore('uploadBlobs').delete(upload.id);
-
-				// Clean up preview URLs
-				if (upload.preview?.startsWith('blob:')) {
-					URL.revokeObjectURL(upload.preview);
-				}
-			}
-		}
-
-		// this.notifications.add('Cached uploads cleared', 'info');
-	}
-
-// Check for restoration intent on page load
-	async checkRestorationIntent() {
-		const restoreData = sessionStorage.getItem('jvb_restore_uploads');
-		if (!restoreData) return;
-
-		const fieldStates = JSON.parse(restoreData);
-		const currentUrlFields = fieldStates.filter(f => f.context.url === window.location.href);
-
-		if (currentUrlFields.length > 0) {
-			for (const fieldState of currentUrlFields) {
-				await this.restoreField(fieldState);
-			}
-
-			// Remove restored fields from session storage
-			const remaining = fieldStates.filter(f => f.context.url !== window.location.href);
-			if (remaining.length > 0) {
-				sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(remaining));
-			} else {
-				sessionStorage.removeItem('jvb_restore_uploads');
-			}
-
-			// this.notifications.add(`Restored ${currentUrlFields.length} field(s)`, 'success');
-		}
-	}
-	/*******************************************************************************
-	 GROUP FUNCTIONALITY
-	 Includes selection, dragging, and grouping logic
-	 *******************************************************************************/
-	/**
-	 *
-	 * @param {string} uploadId as defined by setUpload
-	 * @param {HTMLElement|null} target The target location
-	 * @param {boolean} persist whethet to cache this change
-	 */
-	addImageToGroup(uploadId, target = null, persist = true) {
-		let upload = this.getUpload(uploadId);
-		if(!upload) {
-			return;
-		}
-		let field = this.fields.get(upload.fieldId);
-		if (!field) {
-			return;
-		}
-		//Already in the Preview Grid, or already in the group we're moving to
-		if ((!target && upload.location === field.ui.field.preview) || target === upload.location) {
-			return;
-		}
-
-		if (upload.location) {
-			let groupId = upload.location.dataset.groupId;
-			if (groupId) {
-				let group = this.groups.get(groupId);
-				if (group && group.uploads) {
-					group.uploads.delete(uploadId);
-
-					// ADDED: Delete empty groups automatically
-					if (group.uploads.size === 0) {
-						this.removeGroup(groupId);
-					}
-				}
-			}
-		}
-
-		const checkbox = upload.element.querySelector('[name*="select-item"]');
-		if (checkbox) {
-			checkbox.checked = false;
-		}
-
-		upload.element.querySelector('[name="featured"]').hidden = !target;
-		//If no target, it's going to the preview grid
-		if (!target) {
-			target = field.ui.field.preview;
-		} else {
-			let groupId = target.dataset.groupId;
-			let group = this.groups.get(groupId);
-			if (!group) {
-				group = this.createGroup(upload.fieldId);
-			}
-			group.uploads.add(uploadId);
-		}
-
-
-		target.append(upload.element);
-		if (persist) {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	addSelectionToGroup(target) {
-		let field = this.getFieldFromElement(target);
-		if (!field) {
-			return;
-		}
-		if (this.selected.get(field.key).size === 0) {
-			return;
-		}
-		let group = this.getGroupFromElement(target);
-		if (!group) {
-			group = this.createGroup(field.key);
-		}
-
-		Array.from(this.selected).forEach(uploadId => {
-			this.addImageToGroup(uploadId, group.grid, false);
-		});
-
-		this.persistFieldState(group.fieldId);
-	}
-
-
-	/**
-	 * Remove an empty group from the field
-	 * @param {string} groupId - The group to remove
-	 * @param {boolean} confirm - ask for confirmation
-	 */
-	removeGroup(groupId, confirm = false) {
-		let group = this.groups.get(groupId);
-		if (!group) {
-			return;
-		}
-
-		if (confirm && group.uploads && group.uploads.size > 0) {
-			if(!window.confirm('This will delete this group. Any uploads in this group will return to the main grid. Are you sure?')){
-				return;
-			}
-		}
-
-		// Move any remaining uploads back to preview
-		if (group.uploads && group.uploads.size > 0) {
-			Array.from(group.uploads).forEach(uploadId => {
-				this.addImageToGroup(uploadId, null, false);
-			});
-		}
-
-		// Remove from groups Map
-		this.groups.delete(groupId);
-
-		// Remove DOM element
-		let groupElement = group.element;
-		if (groupElement) {
-			groupElement.remove();
-			this.a11y.announce('Group removed');
-		}
-
-		this.persistFieldState(group.fieldId);
-	}
-
-	/**
-	 * Create a new group
-	 */
-	createGroup(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		if (!field.groups) {
-			field.groups = [];
-		}
-
-		const groupId = `group_${Date.now()}`;
-		field.groups.push({
-			id: groupId,
-			title: '',
-			uploads: []
-		});
-
-		// Create the group element
-		const groupElement = this.createGroupElement(groupId, fieldId);
-
-		if (!groupElement) return null;
-
-		// Store in the groups Map with full group data structure
-		this.groups.set(groupId, {
-			id: groupId,
-			fieldId: fieldId,
-			element: groupElement,
-			uploads: new Set(),
-			meta: {},
-			changes: {}
-		});
-
-		// Add to UI
-		const container = field.ui.groups.container;
-		if (container) {
-			const emptyGroup = container.querySelector('.empty-group');
-			if (emptyGroup) {
-				container.insertBefore(groupElement, emptyGroup);
-			} else {
-				container.appendChild(groupElement);
-			}
-		}
-
-		this.persistFieldState(field.key);
-
-		return groupElement;
-	}
-
-
-	/**
-	 * Remove upload from group
-	 */
-	removeFromGroup(fieldId, uploadId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const group = field.groups.find(g => g.id === groupId);
-		if (!group) return;
-
-		group.uploads = group.uploads.filter(id => id !== uploadId);
-
-		this.renderGroupUI(fieldId);
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Update group title
-	 */
-	updateGroupTitle(fieldId, groupId, title) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const group = field.groups.find(g => g.id === groupId);
-		if (!group) return;
-
-		group.title = title;
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Delete group
-	 */
-	deleteGroup(fieldId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		field.groups = field.groups.filter(g => g.id !== groupId);
-
-		this.renderGroupUI(fieldId);
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Render group UI
-	 */
-	renderGroupUI(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const container = field.ui.group.container;
-		if (!container) {
-			console.warn('Groups container not found for field:', fieldId);
-			return;
-		}
-
-		// Clear existing
-		window.removeChildren(container);
-
-		// Render each group
-		field.groups.forEach(group => {
-			const groupEl = this.createGroupElement(fieldId, group);
-			container.appendChild(groupEl);
-		});
-	}
-
-	createGroupElement(groupId, fieldId) {
-		let groupElement = window.getTemplate('imageGroup');
-		if (!groupElement) return;
-
-		groupElement.dataset.groupId = groupId;
-		groupElement.dataset.fieldId = fieldId;
-
-		let fields = window.getTemplate('groupMetadata');
-		const fieldsContainer = groupElement.querySelector('.fields');
-		if (fieldsContainer && fields) {
-			fieldsContainer.append(fields);
-
-			// Set unique IDs and names for form fields
-			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
-			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
-
-			if (titleInput) {
-				titleInput.id = `${groupId}_title`;
-				titleInput.name = `${groupId}[post_title]`;
-			}
-			if (excerptInput) {
-				excerptInput.id = `${groupId}_excerpt`;
-				excerptInput.name = `${groupId}[post_excerpt]`;
-			}
-			let field = this.fields.get(fieldId);
-			if (field.content !== '') {
-				let summary = groupElement.querySelector('summary');
-				summary.textContent = field.content + ' Fields';
-			}
-		} else {
-			groupElement.querySelector('details').remove();
-		}
-
-		const gridContainer = groupElement.querySelector('.item-grid.group');
-		if (gridContainer) {
-			gridContainer.dataset.groupId = groupId;
-		}
-
-		return groupElement;
-	}
-
-	handleSelectAll(element, checked = null) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return;
-
-		const handler = this.selectionHandlers.get(field.key);
-		if (!handler) return;
-
-		// Use element's checked state if not provided
-		if (checked === null) {
-			checked = element.checked;
-		}
-
-		handler.selectAll(checked);
-		this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
-	}
-
-	clearAllSelections(field) {
-		const handler = this.selectionHandlers.get(field.key);
-		if (handler) {
-			handler.clearSelection();
-		}
-	}
-
-	getSelectedUploads(element) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return [];
-
-		const handler = this.selectionHandlers.get(field.key);
-		return handler ? handler.getSelected() : [];
-	}
-
-	removeSelection(button) {
-		let fieldId = this.getFieldIdFromElement(button);
-
-		const selectedUploads = this.getSelectedUploads(button);
-		if (selectedUploads.length === 0) {
-			this.notify('No uploads selected', 'warning');
-			return;
-		}
-
-		selectedUploads.forEach(upload => {
-			this.removeUpload(fieldId, upload);
-		});
-	}
-
-	removeUpload(fieldId, uploadId) {
-		const field = this.fields.get(fieldId);
-		const upload = this.uploads.get(uploadId);
-
-		if (!field || !upload) return;
-
-		// Remove from field
-		field.uploads?.delete(uploadId);
-
-		// Remove from group if grouped
-		if (upload.groupId) {
-			const group = this.groups.get(upload.groupId);
-			if (group && group.uploads) {
-				group.uploads.delete(uploadId);
-
-				if (group.uploads.size === 0) {
-					this.removeGroup(upload.groupId);
-				}
-			}
-		}
-
-		// Clean up element
-		upload.element?.remove();
-
-		// Clean up memory
-		this.clearUpload(uploadId);
-
-		// Update field state after removal
-		this.updateFieldState(fieldId);
-
-		// Update UI
-		this.maybeLockUploads(fieldId);
-		const handler = this.selectionHandlers.get(field.key);
-		if (handler) {
-			handler.deselect(uploadId);
-		}
-
-		this.a11y.announce('Upload removed');
-	}
-
-	/**************************************************************************
-	 META
-	 Handled separately, in case it is edited in the middle of processing images
-	 **************************************************************************/
-
-	/**************************************************************************
-	 SUBSCRIBERS
-	 **************************************************************************/
-	/**
-	 * Event system
-	 */
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	handleBeforeUnload(e) {
-		// Check for any uploads in processing or pending state
-		const unsavedUploads = Array.from(this.uploads.values()).filter(upload =>
-			upload.status === 'processing' ||
-			upload.status === 'pending' ||
-			upload.status === 'uploading'
-		);
-
-		if (unsavedUploads.length > 0) {
-			const message = 'You have uploads in progress. Are you sure you want to leave?';
-			e.preventDefault();
-			e.returnValue = message;
-			return message;
-		}
-	}
-	/**************************************************************************
-	 CLEANUP
-	 **************************************************************************/
-	cleanup() {
-		this.clearListeners();
-		if (this.hasGroups) {
-			this.clearGroupListeners();
-		}
-		this.compressionWorker = null;
-		this.subscribers.clear();
-	}
-}
-
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbUploads = new UploadManager();
-});
diff --git a/assets/js/concise/UserInteractions.js b/assets/js/concise/UserInteractions.js
new file mode 100644
index 0000000..22babd7
--- /dev/null
+++ b/assets/js/concise/UserInteractions.js
@@ -0,0 +1,290 @@
+/**
+ * FrontendInteractions - Unified class for frontend user interactions
+ * Handles: Favourites, Votes, and related user actions
+ */
+class UserInteractions {
+	constructor() {
+		if (!window.auth.getUser()) {
+			return; // Don't initialize if not logged in
+		}
+
+		// Initialize favourites store
+		this.favouritesStore = window.jvbStore.register(
+			'favourites',
+			{
+				storeName: 'favourites',
+				endpoint: 'favourites',
+				indexes: [
+					{name: 'content', keyPath: 'content'},
+					{name: 'listId', keyPath: 'listId'},
+				],
+				TTL: 6 * 60 * 1000,
+				showLoading: false,
+				filters: {
+					user: window.auth.getUser(),
+					content: 'all',
+					order: 'desc',
+					orderby: 'date',
+					page: 1,
+					all: true,
+				}
+			}
+		);
+
+		// Initialize favourites lists store
+		this.listsStore = window.jvbStore.register(
+			'favourites_lists',
+			{
+				storeName: 'lists',
+				keyPath: 'listId',
+				endpoint: 'favourites/lists',
+				TTL: 6 * 60 * 1000,
+			}
+		);
+
+		// Initialize votes store
+		this.votesStore = window.jvbStore.register(
+			'votes',
+			{
+				storeName: 'votes',
+				endpoint: 'votes',
+				useIndexedDB: true,
+				TTL: 6 * 60 * 1000,
+				showLoading: false
+			}
+		);
+
+		this.setupEventListeners();
+		this.favouritesStore.fetch();
+	}
+
+	setupEventListeners() {
+		// Subscribe to favourites updates
+		this.favouritesStore.subscribe((event, data) => {
+			switch (event) {
+				case 'data-fetched':
+				case 'data-cached':
+				case 'items-updated':
+				case 'item-stored':
+					// Could handle UI updates here
+					break;
+			}
+		});
+	}
+
+	/**
+	 * Toggle favourite status
+	 * @param {HTMLElement} button - Button element with data attributes
+	 */
+	toggleFavourite(button) {
+		if (!window.auth.getUser()) {
+			window.location.href = jvbSettings.redirect + '&action=register&type=favourites';
+			return;
+		}
+
+		// Toggle UI immediately
+		button.classList.toggle('favourited');
+		const action = button.classList.contains('favourited') ? 'add' : 'remove';
+		const message = button.classList.contains('favourited')
+			? `Added ${button.dataset.type} to favourites.`
+			: `Removed ${button.dataset.type} from favourites.`;
+
+		window.jvbA11y.announce(message);
+
+		// Update button icon
+		button.innerHTML = jvbSettings.icons[button.classList.contains('favourited') ? 'heart-filled' : 'heart'];
+
+		// Save to store
+		this.favouritesStore.setItem(button.dataset.id, {
+			target_id: button.dataset.id,
+			action: action,
+			type: button.dataset.type,
+			artist: button.dataset.artist,
+		});
+	}
+
+	/**
+	 * Handle vote action
+	 * @param {HTMLElement} button - Vote button element
+	 */
+	handleVote(button) {
+		if (!window.auth.getUser()) {
+			window.location.href = jvbSettings.redirect + '&action=register&type=vote';
+			return;
+		}
+
+		// Queue the vote operation
+		window.jvbQueue.handleVote(button);
+
+		const parent = button.closest('.vote');
+		const alreadyVoted = parent.querySelector('.voted');
+
+		// Handle previous vote if exists
+		if (alreadyVoted) {
+			const count = alreadyVoted.querySelector('.count');
+			if (alreadyVoted.classList.contains('up')) {
+				count.textContent = parseInt(count.textContent) - 1;
+			} else {
+				count.textContent = parseInt(count.textContent) + 1;
+			}
+			alreadyVoted.classList.remove('voted');
+		}
+
+		// Update current vote
+		button.classList.add('voted');
+		const count = button.querySelector('.count');
+		if (button.classList.contains('up')) {
+			count.textContent = parseInt(count.textContent) + 1;
+		} else {
+			count.textContent = parseInt(count.textContent) - 1;
+		}
+	}
+
+	/**
+	 * Check if an item is favourited
+	 * @param {string} content - Content type
+	 * @param {string|number} id - Item ID
+	 * @returns {boolean}
+	 */
+	isFavourited(content, id) {
+		if (!window.auth.getUser()) {
+			return false;
+		}
+		if (typeof window.userFavourites === 'undefined') {
+			return false;
+		}
+		if (typeof window.userFavourites[content] === 'undefined') {
+			return false;
+		}
+		return window.userFavourites[content]?.has(id);
+	}
+
+	/**
+	 * Check if user has voted on an item
+	 * @param {string} content - Content type
+	 * @param {string|number} id - Item ID
+	 * @returns {string} - 'up', 'down', or ''
+	 */
+	checkVoteStatus(content, id) {
+		if (!window.auth.getUser()) {
+			return '';
+		}
+		let status = '';
+		if (window.userVotes && window.userVotes[content]?.has(id)) {
+			status = window.userVotes[content].get(id);
+		}
+		return status;
+	}
+}
+
+// Lazy initialization using requestIdleCallback for better performance
+function initFrontendInteractions() {
+	if (window.auth.getUser()) {
+		window.jvbInteractions = new FrontendInteractions();
+	}
+}
+
+// Initialize after DOM is ready but without blocking render
+if ('requestIdleCallback' in window) {
+	requestIdleCallback(async function() {
+		window.auth.subscribe((event) => {
+			if (event === 'auth-loaded') {
+				if (document.readyState === 'loading') {
+					document.addEventListener('DOMContentLoaded', initFrontendInteractions);
+				} else {
+					initFrontendInteractions();
+				}
+			}
+		});
+	});
+} else {
+	// Fallback for browsers without requestIdleCallback
+	if (document.readyState === 'loading') {
+		document.addEventListener('DOMContentLoaded', initFrontendInteractions);
+	} else {
+		setTimeout(initFrontendInteractions, 1);
+	}
+}
+
+/**
+ * Global helper functions for backwards compatibility
+ */
+window.toggleFavourite = function(button) {
+	if (!window.jvbInteractions) {
+		console.warn('FrontendInteractions not initialized');
+		return;
+	}
+	window.jvbInteractions.toggleFavourite(button);
+}
+
+window.handleVote = function(button) {
+	if (!window.jvbInteractions) {
+		console.warn('FrontendInteractions not initialized');
+		return;
+	}
+	window.jvbInteractions.handleVote(button);
+}
+
+window.isFavourited = function(content, id) {
+	if (!window.jvbInteractions) {
+		return false;
+	}
+	return window.jvbInteractions.isFavourited(content, id);
+}
+
+window.checkVoteStatus = function(content, id) {
+	if (!window.jvbInteractions) {
+		return '';
+	}
+	return window.jvbInteractions.checkVoteStatus(content, id);
+}
+
+
+/**
+ * Formats vote from template
+ * @param item
+ * @param status
+ * @returns {Node|ActiveX.IXMLDOMNode|boolean}
+ */
+window.formatVote = function(item, status) {
+	let vote = window.getTemplate('voteButton');
+
+	vote.dataset.itemId = item.id;
+	vote.dataset.content = item.content;
+	let up =vote.querySelector('button.up');
+	let down =vote.querySelector('button.down');
+
+	if(status === 'up'){
+		up.classList.add('voted');
+	}
+	if(status === 'down'){
+		down.classList.add('voted');
+	}
+	if(item.upvotes > 0){
+		up.querySelector('.count').textContent = item.upvotes;
+	}
+	if(item.downvotes > 0){
+		down.querySelector('.count').textContent = '-'+item.downvotes;
+	}
+
+	return vote;
+}
+
+
+/**
+ * Tests if user has voted for this item
+ * @param content
+ * @param id
+ * @returns {string}
+ */
+window.checkVoteStatus = function(content, id){
+	if(!window.auth.getUser()){
+		return '';
+	}
+	let status = '';
+	if(window.userVotes && window.userVotes[content]?.has(id)){
+		status = window.userVotes[content].get(id);
+	}
+
+	return status;
+}
diff --git a/assets/js/concise/UserSettings.js b/assets/js/concise/UserSettings.js
index 51763b0..9c9f2b0 100644
--- a/assets/js/concise/UserSettings.js
+++ b/assets/js/concise/UserSettings.js
@@ -6,7 +6,7 @@
 
 		this.debouncer = window.debouncer;
 
-		this.isLoggedIn = jvbSettings.currentUser !== null;
+		this.isLoggedIn = window.auth.getUser() !== null;
 
 		this.initListeners();
 		this.loadSettings();
@@ -103,11 +103,11 @@
 			return;
 		}
 		const headers = {
-			'X-WP-Nonce': jvbSettings?.nonce,
+			'X-WP-Nonce': window.auth.getNonce(),
 			'Content-Type': 'application/json'
 		};
 		const body = {
-			user: jvbSettings.currentUser,
+			user: window.auth.getUser(),
 			setting: name,
 			value: value
 		};
@@ -152,8 +152,12 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', function() {
-	window.jvbUserSettings = new UserSettings();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbUserSettings = new UserSettings();
+		}
+	});
 });
 //
 // // Theme switching functionality
@@ -182,18 +186,18 @@
 // 		localStorage.setItem('theme', isDark ? 'dark' : 'light');
 //
 // 		// If user is logged in, save preference
-// 		if (jvbSettings.currentUser !== null) {
+// 		if (window.auth.getUser() !== null) {
 // 			try {
 // 				await fetch(`${jvbSettings.api}settings`, {
 // 					method: 'POST',
 // 					headers: {
 // 						'Content-Type': 'application/json',
-// 						'X-WP-Nonce': jvbSettings.nonce,
-// 						'action_nonce': jvbSettings.dash,
+// 						'X-WP-Nonce': window.auth.getNonce(),
+// 						'action_nonce': window.auth.getNonce('dash'),
 // 					},
 // 					body: JSON.stringify({
 // 						dark_mode: isDark,
-// 						user: jvbSettings.currentUser
+// 						user: window.auth.getUser()
 // 					})
 // 				});
 // 			} catch (error) {
diff --git a/assets/js/dash/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
similarity index 77%
rename from assets/js/dash/UtilityFunctions.js
rename to assets/js/concise/UtilityFunctions.js
index 566e1b1..3b9e355 100644
--- a/assets/js/dash/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -19,7 +19,8 @@
 	}
 }
 /**
- * Format a time value to "X time ago" format
+ * Format a time value as relative time (past or future)
+ * Handles both "X time ago" and "in X time" formats
  *
  * @param {string|Date} dateStr Date to format
  * @returns {string} Formatted time string
@@ -27,60 +28,47 @@
 window.formatTimeAgo = function(dateStr) {
 	const date = dateStr instanceof Date ? dateStr : new Date(dateStr);
 	const now = new Date();
-	const seconds = Math.floor((now - date) / 1000);
+	const diffMs = date - now;
+	const isPast = diffMs < 0;
+
+	// Work with absolute values for calculations
+	const seconds = Math.floor(Math.abs(diffMs) / 1000);
 	const minutes = Math.floor(seconds / 60);
 	const hours = Math.floor(minutes / 60);
 	const days = Math.floor(hours / 24);
 
-	if (hours < 24) {
+	// Just now (within 1 minute either way)
+	if (minutes === 0) {
+		return 'Just now';
+	}
+
+	// Format the time components
+	let timeStr = '';
+
+	if (seconds < 10) {
+		timeStr = 'a moment';
+	} else if (seconds < 60) {
+		timeStr = 'less than a minute'
+	} else if (minutes < 5) {
+		timeStr = 'a few minutes';
+	} else if (hours < 24) {
 		if (hours === 0) {
-			return minutes === 0 ? 'Just now' : `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`;
+			// Minutes only
+			timeStr = `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`;
+		} else {
+			// Hours
+			timeStr = `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
 		}
-		return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`;
+	} else if (days < 7) {
+		// Days
+		timeStr = `${days} ${days === 1 ? 'day' : 'days'}`;
+	} else {
+		// More than a week - just show the date
+		return date.toLocaleDateString();
 	}
 
-	if (days < 7) {
-		return `${days} ${days === 1 ? 'day' : 'days'} ago`;
-	}
-
-	return date.toLocaleDateString();
-}
-
-/**
- * Format a future date for display
- *
- * @param {string|Date} dateStr Future date
- * @returns {string} Formatted string
- */
-window.formatTimeSoon = function(dateStr) {
-	const date = dateStr instanceof Date ? dateStr : new Date(dateStr);
-	const now = new Date();
-
-	// Handle past dates
-	if (date <= now) {
-		return "Just now";
-	}
-
-	const seconds = Math.floor((date - now) / 1000);
-	const minutes = Math.floor(seconds / 60);
-
-	if (seconds < 60) {
-		return "In a moment";
-	}
-
-	if (minutes < 5) {
-		return "In a few minutes";
-	}
-
-	if (minutes < 20) {
-		return "Coming up soon";
-	}
-
-	if (minutes < 60) {
-		return "In about half an hour";
-	}
-
-	return "Later today";
+	// Add appropriate prefix/suffix based on past or future
+	return isPast ? `${timeStr} ago` : `in ${timeStr}`;
 }
 
 /**
@@ -136,55 +124,6 @@
 }
 
 /**
- * Formats vote from template
- * @param item
- * @param status
- * @returns {Node|ActiveX.IXMLDOMNode|boolean}
- */
-window.formatVote = function(item, status) {
-	let vote = window.getTemplate('voteButton');
-
-	vote.dataset.itemId = item.id;
-	vote.dataset.content = item.content;
-	let up =vote.querySelector('button.up');
-	let down =vote.querySelector('button.down');
-
-	if(status === 'up'){
-		up.classList.add('voted');
-	}
-	if(status === 'down'){
-		down.classList.add('voted');
-	}
-	if(item.upvotes > 0){
-		up.querySelector('.count').textContent = item.upvotes;
-	}
-	if(item.downvotes > 0){
-		down.querySelector('.count').textContent = '-'+item.downvotes;
-	}
-
-	return vote;
-}
-
-
-/**
- * Tests if user has voted for this item
- * @param content
- * @param id
- * @returns {string}
- */
-window.checkVoteStatus = function(content, id){
-	if(!jvbSettings.currentUser){
-		return '';
-	}
-	let status = '';
-	if(window.userVotes && window.userVotes[content]?.has(id)){
-		status = window.userVotes[content].get(id);
-	}
-
-	return status;
-}
-
-/**
  * Gets a clone of an icon element if it exists for efficient DOM manipulation
  * @param icon
  * @returns {Node | ActiveX.IXMLDOMNode}
@@ -211,15 +150,6 @@
 }
 
 /**
- * Tests for empty object
- * @param obj
- * @returns {boolean}
- */
-window.isEmptyObject = function(obj) {
-	return Object.keys(obj).length === 0;
-}
-
-/**
  * Format a number with comma separator (e.g., 1,234)
  * @param {number} num - Number to format
  * @returns {string} - Formatted number
@@ -261,17 +191,6 @@
 }
 
 /**
- * Truncate text to a specific length with ellipsis
- * @param {string} text - Text to truncate
- * @param {number} length - Maximum length
- * @returns {string} - Truncated text
- */
-window.truncateText = function(text, length = 100) {
-	if (!text || text.length <= length) return text;
-	return text.substring(0, length) + '...';
-}
-
-/**
  * Should be faster than setting innerHTML = ''
  * @param node
  */
@@ -296,7 +215,7 @@
 
 	// If same day, just show one date
 	if (start.toDateString() === end.toDateString()) {
-		return start.toLocaleDateString('en-US', {
+		return start.toLocaleDateString('en-CA', {
 			year: 'numeric',
 			month: 'short',
 			day: 'numeric'
@@ -305,43 +224,16 @@
 
 	// If same month and year, show range with month once
 	if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) {
-		return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.getDate()}, ${end.getFullYear()}`;
+		return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })} - ${end.getDate()}, ${end.getFullYear()}`;
 	}
 
 	// If same year, show full range with year once
 	if (start.getFullYear() === end.getFullYear()) {
-		return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${end.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}, ${end.getFullYear()}`;
+		return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })} - ${end.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })}, ${end.getFullYear()}`;
 	}
 
 	// Different years, show full dates
-	return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} - ${end.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
-}
-
-/**
- * Debounce function to limit frequent calls
- * @param {Function} func - Function to debounce
- * @param {number} wait - Wait time in milliseconds
- * @returns {Function} - Debounced function
- */
-window.debounce = function(func, wait = 300) {
-	let timeout;
-	return function(...args) {
-		clearTimeout(timeout);
-		timeout = setTimeout(() => func.apply(this, args), wait);
-	};
-}
-
-window.throttle = function(func, limit) {
-	let inThrottle;
-	return function() {
-		const args = arguments;
-		const context = this;
-		if (!inThrottle) {
-			func.apply(context, args);
-			inThrottle = true;
-			setTimeout(() => inThrottle = false, limit);
-		}
-	}
+	return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric', year: 'numeric' })} - ${end.toLocaleDateString('en-CA', { month: 'short', day: 'numeric', year: 'numeric' })}`;
 }
 
 
@@ -737,60 +629,6 @@
 	return !isNaN(parseFloat(n)) && isFinite(n);
 };
 
-window.handleListField = function (elem, value) {
-	if (!Array.isArray(value)) {
-		elem.remove();
-		return;
-	}
-	let li = elem.querySelector('li');
-	value.forEach((v) => {
-		let l = li.cloneNode(true);
-		l.textContent = v;
-		elem.append(l);
-	});
-	li.remove();
-};
-
-window.handleTextField = function (elem, value) {
-	if (typeof value !== "string") {
-		elem.remove();
-		return;
-	}
-	elem.textContent = value;
-};
-
-window.handleImageField = function (elem, value) {
-	if (!Array.isArray(value) || value === 0) {
-		elem.remove();
-		return;
-	}
-	let img = (elem.tagName === 'IMG') ? elem : elem.querySelector('img');
-	if (!img) {
-		elem.remove();
-		return;
-	}
-	img.alt = value.alt;
-	img.src = value.thumbnail;
-	img.dataset.small = value.small;
-	img.dataset.medium = value.medium;
-	img.dataset.large = value.full;
-};
-
-window.handleGalleryField = function (elem, value)
-{
-	if (!Array.isArray(value)) {
-		elem.remove();
-		return;
-	}
-	let img = elem.querySelector('img');
-	value.forEach((v) => {
-		let i = img.cloneNode(true);
-		window.handleImageField(i, v);
-		elem.append(i);
-	});
-	img.remove();
-};
-
 /**
  *
  * @param {object} selectors
@@ -843,3 +681,75 @@
 	}
 }
 window.debouncer = new DebouncedActions();
+
+
+// -----------------------------------------------------
+// Scroll direction + scroll progress
+// -----------------------------------------------------
+const body = document.body;
+const docEl = document.documentElement;
+const progressBar = document.querySelector('.scroll-progress .bar');
+
+let lastY = window.scrollY || docEl.scrollTop || 0;
+let direction = -1;
+let ticking = false;
+let maxScroll = 0;
+
+function updateMaxScroll() {
+	maxScroll = Math.max(0, docEl.scrollHeight - window.innerHeight);
+}
+
+function updateScrollProgress(y) {
+	if (!progressBar) return;
+
+	const progress = maxScroll > 0 ? y / maxScroll : 0;
+	const clamped = Math.max(0, Math.min(1, progress));
+
+	progressBar.style.transform = `scaleX(${clamped})`;
+}
+
+function onScrollFrame() {
+	const y = window.scrollY || docEl.scrollTop || 0;
+
+	// Direction: 1 = down, -1 = up, keep existing if no movement
+	if (y > lastY) {
+		direction = 1;
+	} else if (y < lastY) {
+		direction = -1;
+	}
+
+	lastY = y;
+
+	// Only add scroll-up when actually below top & moving up
+	document.body.classList.toggle('scroll-up', direction < 0 && y > 0);
+
+	// Update progress bar
+	updateScrollProgress(y);
+
+	ticking = false;
+}
+
+// Throttled scroll listener
+window.addEventListener(
+	'scroll',
+	() => {
+		if (!ticking) {
+			ticking = true;
+			requestAnimationFrame(onScrollFrame);
+		}
+	},
+	{ passive: true }
+);
+
+// Debounced resize to recalc scrollable height
+window.addEventListener('resize', () => {
+	window.debouncer.schedule('recalc-max-scroll', () => {
+		updateMaxScroll();
+		updateScrollProgress(window.scrollY || docEl.scrollTop || 0);
+	}, 20);
+});
+
+// Initial setup
+updateMaxScroll();
+updateScrollProgress(lastY);
+
diff --git a/assets/js/concise/View.js b/assets/js/concise/View.js
index ca6c99e..423d8a4 100644
--- a/assets/js/concise/View.js
+++ b/assets/js/concise/View.js
@@ -19,7 +19,7 @@
 			grid: new Map(),
 			table: new Map(),
 		}
-		this.currentView = 'grid';
+		this.currentView = this.container.dataset.view ?? 'grid';
 		this.selectedItems = new Set();
 		this.subscribers = new Set();
 
@@ -161,15 +161,6 @@
 	}
 
 	/**
-	 * Handle data updates from store
-	 */
-	handleDataUpdate(data) {
-		console.log(data);
-		const items = data.data?.items || data.items || [];
-		this.render(items);
-	}
-
-	/**
 	 * Handle items update
 	 */
 	handleItemsUpdate() {
@@ -185,6 +176,7 @@
 
 		// Handle empty state
 		if (items.length === 0) {
+			console.log('Nothing to show');
 			this.renderEmpty();
 			return;
 		}
@@ -293,7 +285,10 @@
 	}
 
 	toggleTable(on) {
-		this.ui.table.selectedColumns.hidden = !on;
+		if (this.ui.table.selectedColumns) {
+			this.ui.table.selectedColumns.hidden = !on;
+		}
+
 		if (on && !this.ui.table.table) {
 			let table = window.getTemplate('contentTable');
 			this.container.append(table);
@@ -317,7 +312,10 @@
 				window.removeChildren(this.ui.table.body);
 			}
 		}
-		this.ui.table.selectedColumns.hidden = !on;
+
+		if (this.ui.table.selectedColumns) {
+			this.ui.table.selectedColumns.hidden = !on;
+		}
 	}
 
 	toggleGrid() {
@@ -360,17 +358,32 @@
 			row.querySelector('.select-item').value,
 			row.querySelector('.select-item').checked,
 			row.querySelector('.select-item + label').htmlFor,
-			row.querySelector(`input[name="post_status"][value="${item.status}"]`).checked
 		] = [
 			item.id,
 			item.id,
 			this.selectedItems.has(`${item.id}`),
 			item.id,
-			item.status
 		];
+		let status = row.querySelector(`input[name="post_status"][value="${item.status}"]`);
+		if (status) {
+			status.checked = true;
+		}
 
-		// Let jvbPopulate do its thing - NO prefixing needed!
-		new window.jvbPopulate(row, item.fields, item.images);
+		if (Object.hasOwn(this.ui.table.table.dataset, 'edit')) {
+			new window.jvbPopulate(row, item.fields, item.images);
+		} else {
+			for (let [key, value] of Object.entries(item)) {
+				let col = row.querySelector(`[data-field="${key}"]`);
+				if (col) {
+					let p = col.querySelector('p');
+					if (col.dataset.fieldType === 'date') {
+						value = window.formatTimeAgo(value);
+					}
+					p.textContent = value;
+				}
+			}
+		}
+
 
 		// Clean up after population
 		this.cleanupTableRow(row);
diff --git a/assets/js/concise/navigation.js b/assets/js/concise/navigation.js
index ab1e141..fb06d10 100644
--- a/assets/js/concise/navigation.js
+++ b/assets/js/concise/navigation.js
@@ -60,12 +60,15 @@
 		if (this.navs.size === 0) {
 			return;
 		}
-		if (this.openNav && !e.target.closest(this.openNav)) {
-			this.toggleNav(false);
+		if (this.openNav && e.target.closest(`#${this.openNav}`) === null) {
+			this.toggleNav(false, this.openNav);
 		}
-		if (!e.target.closest(... this.navIDs())) {
-			return;
-		}
+
+		// if (!e.target.closest(this.openNav)) {
+		// 	console.log('Not closest nav ids');
+		// 	console.log(this.navIDs());
+		// 	return;
+		// }
 
 		let toggle = e.target.closest('.toggle.main');
 		if (toggle) {
@@ -82,7 +85,6 @@
 	}
 
 	handleHoverOn(e) {
-		console.log(e.target);
 		let nav =  e.target.closest('nav');
 		if (nav) {
 			this.toggleNav(true, nav.id);
@@ -94,7 +96,6 @@
 	}
 
 	handleHoverOff(e) {
-		console.log(e.target);
 		let nav =  e.target.closest('nav');
 		if (nav) {
 			this.toggleNav(false, nav.id);
@@ -128,11 +129,13 @@
 				this.openNav = null;
 			}
 			document.removeEventListener('keydown', this.escapeListener);
-			Array.from(nav.submenus).forEach(submenu => {
-				if(submenu.classList.contains('open')) {
-					this.toggleSubmenu(false, submenu);
-				}
-			});
+			if (!nav.nav.classList.contains('sidebar')) {
+				Array.from(nav.submenus).forEach(submenu => {
+					if(submenu.classList.contains('open')) {
+						this.toggleSubmenu(false, submenu);
+					}
+				});
+			}
 		}
 
 		nav.nav.ariaExpanded = on;
diff --git a/assets/js/on-this-page.js b/assets/js/concise/on-this-page.js
similarity index 100%
rename from assets/js/on-this-page.js
rename to assets/js/concise/on-this-page.js
diff --git a/assets/js/concise/quill.js b/assets/js/concise/quill.js
index 847f8eb..3f16b31 100644
--- a/assets/js/concise/quill.js
+++ b/assets/js/concise/quill.js
@@ -207,7 +207,7 @@
 										{
 											method: 'POST',
 											headers: {
-												'X-WP-Nonce': jvbSettings.nonce
+												'X-WP-Nonce': window.auth.getNonce()
 											},
 											body: formData
 										}
diff --git a/assets/js/dash/UploadManager.js b/assets/js/dash/UploadManager.js
index 9434ea8..138b070 100644
--- a/assets/js/dash/UploadManager.js
+++ b/assets/js/dash/UploadManager.js
@@ -2818,7 +2818,7 @@
 			let changes = window.getDifferences.map(this.oldUploads, metaData);
 
 
-			if (window.isEmptyObject(changes)) return;
+			if (Object.keys(changes).length === 0) return;
 
 			try {
 				const operation = {
diff --git a/assets/js/min/ContentManager.min.js b/assets/js/min/ContentManager.min.js
index 7f51975..3655dd5 100644
--- a/assets/js/min/ContentManager.min.js
+++ b/assets/js/min/ContentManager.min.js
@@ -1 +1 @@
-window.contentManager=class{constructor(e){this.config={content:"",plural:"",taxonomies:{},selectors:{container:".items-list",grid:".item-grid:not(.preview)",uploadZone:".file-upload-wrapper",statusFilters:".status-filters",dateFilters:".date-filters",taxonomyFilters:".taxonomy-filters",viewControls:".view-controls",bulkControls:".bulk-controls",scrollSentinel:".scroll-sentinel",editModal:".edit-modal",bulkEditModal:".bulk-edit-modal",clearButton:".clear-filters"},createPostPerFile:!0,uploadConfig:{mode:"direct",allowMultiple:!0,createPostPerFile:!0,maxSize:5242880,allowedTypes:["image/jpeg","image/png","image/gif","image/webp"]},...e},this.resetCache=!1,this.queueManager=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.error=window.jvbError,this.state={selected:new Set,filters:{status:"all",taxonomies:{},date:null},view:localStorage.getItem(`${this.config.content}_view`)||"grid",loading:!1},this.queue={all:{items:new Map,page:1,hasMore:!0,totalPages:0},draft:{items:new Map,page:1,hasMore:!0,totalPages:0},publish:{items:new Map,page:1,hasMore:!0,totalPages:0},trash:{items:new Map,page:1,hasMore:!0,totalPages:0}},this.init()}async init(){this.elements={},Object.entries(this.config.selectors).forEach((([e,t])=>{this.elements[e]=document.querySelector(t)})),this.config.uploadConfig&&(this.fileUploader=new window.jvbFileUploader({...this.config.uploadConfig,content:this.config.content,fieldName:null})),this.initStatusFilters(),this.initDateFilters(),this.initTaxonomyFilters(),this.initClearFilters(),this.initViewControls(),this.initBulkControls(),this.initInfiniteScroll(),this.initModals(),await this.loadContent()}queueContentUpdate(e,t){const s={type:"content_update",data:{posts:{[e]:{content:this.config.content,...t}},content:this.config.content}};this.queueManager.addToQueue(s),this.updateLocalState(e,t)}queueBulkUpdate(e,t){const s={};e.forEach((e=>{s[e]={content:this.config.content,...t}}));const i={user:jvbSettings.currentUser,type:"content_update",data:{posts:s}};this.queueManager.addToQueue(i),e.forEach((e=>this.updateLocalState(e,t)))}updateLocalState(e,t){const s=this.queue[this.state.filters.status].items.get(e);if(s){Object.assign(s,t),this.queue[this.state.filters.status].items.set(e,s);const i=this.elements.grid.querySelector(`[data-id="${e}"]`);i&&this.updateItemElement(i,s)}}processFormData(e){const t={};for(const[s,i]of e.entries())if("status"===s)t.status=i;else if(s.startsWith("taxonomy_")){const e=s.replace("taxonomy_","");t.taxonomies||(t.taxonomies={}),t.taxonomies[e]=Array.isArray(i)?i:[i]}else t[s]=i;return t}updateItemElement(e,t){e.classList.remove("draft","publish","trash"),e.classList.add(t.status);const s=e.querySelector(".action-status");s&&(removeChildren(s),s.append(getIcon(t.status))),t.taxonomies&&e.querySelectorAll(".label-group").forEach((e=>{const s=e.dataset.taxonomy;if(s&&t.taxonomies[s]){const i=t.taxonomies[s].terms;e.querySelector(".terms").innerHTML=this.renderTerms(i)}}))}handleItemAction(e,t){const s=t.dataset.id;switch(e){case"edit":this.editModal.handleOpen(),this.openEditModal(t),this.editModal.form&&new FormFields(this.editModal.form,{onSave:this.editModal.onSave(),itemID:t.dataset.id});break;case"restore":this.queueContentUpdate(s,{status:"draft"}),t.remove();break;case"trash":this.queueContentUpdate(s,{status:"trash"}),t.remove();break;case"delete":confirm(`Hold up! Are you sure you want to permanently delete this ${this.config.content}?\n\nThis is a forever kind of deal - no taking it back.`)&&(this.queueContentUpdate(s,{status:"delete"}),t.remove());break;case"toggle-status":const e="publish"===t.dataset.status?"draft":"publish";this.queueContentUpdate(s,{status:e}),t.dataset.status=e,removeChildren(t.querySelector(".action-status")),t.querySelector(".action-status").append(getIcon(e))}}async handleBulkOperation(e,t){window.jvbLoading.show("Processing bulk changes...");try{const s={};t.forEach((t=>{s[t]={content:this.config.content,status:e},["delete","trash","restore"].includes(e)&&document.querySelector('[data-id="'+t+'"]').remove()})),this.queueManager.addToQueue({type:"content_update",data:{posts:s}}),this.clearSelection(),this.showNotification("Bulk changes queued for processing")}catch(e){console.error("Bulk operation failed:",e),this.showNotification("Failed to queue bulk operation","error")}finally{window.jvbLoading.hide()}}getQueryKey(){return JSON.stringify({status:this.state.filters.status,page:this.state.page,filters:this.state.filters})}toggleItemSelection(e,t){const s=e.dataset.id;t?(this.state.selected.add(s),e.classList.add("selected"),e.querySelector("input[type=checkbox]").checked=!0):(this.state.selected.delete(s),e.classList.remove("selected"),e.querySelector("input[type=checkbox]").checked=!1)}async loadContent(e=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const t=this.state.filters.status;console.log("Loading Page: "),console.log(this.queue[t].page);const s=new URLSearchParams;s.set("type",this.config.content),s.set("page",this.queue[t].page),s.set("filters",JSON.stringify(this.state.filters)),s.set("user",jvbSettings.currentUser),e&&(this.queue[t].page=1,this.queue[t].items.clear(),removeChildren(this.elements.grid),this.elements.grid.classList.remove("empty"));const i=await this.cache.fetchWithCache(`${jvbSettings.api}content?`+s,{method:"GET",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash}},{context:jvbSettings.currentUser+"-"+this.config.content,forceRefresh:!1});i.total>0?(this.elements.grid.classList.remove("empty"),i.items.forEach((e=>{this.queue[t].items.set(e.id,e)})),this.queue[t].page++,this.queue[t].totalPages=i.total_pages,this.queue[t].hasMore=this.queue[t].page<i.total_pages):(this.elements.grid.classList.add("empty"),this.elements.grid.innerHTML=`<div class="empty-state"><h3>${jvbSettings.icons[this.config.content]}Nothing here${jvbSettings.icons[this.config.content]}</h3><p>It doesn't look like you have any ${this.config.plural} yet.</p><p><small><i>Add some by uploading images above.</i></small></p></div>`,this.queue[t].page=1,this.queue[t].hasMore=!1),this.renderContent()}catch(e){console.error("Error loading content:",e),this.loadingManager.showError("Failed to load content")}finally{this.state.loading=!1,this.loadingManager.hide()}}renderContent(){const e=this.state.filters.status,t=this.queue[e].items;t.size>0&&(this.elements.grid.classList.remove("empty"),this.elements.grid.querySelector(".empty-state")&&removeChildren(this.elements.grid));const s=document.createDocumentFragment();t.forEach((t=>{const i=this.elements.grid.querySelector(`[data-id="${t.id}"]`);if(i){if(t.view!==this.state.view){const e=this.createItemElement(t);t.view=this.state.view,i.replaceWith(e)}}else{const e=this.createItemElement(t);t.view=this.state.view,s.appendChild(e)}this.queue[e].items.set(t.id,t)})),s.children.length>0&&this.elements.grid.appendChild(s)}createItemElement(e){let t=window.getTemplate(this.state.view+"View");t.classList.add(e.status),t.dataset.id=e.id,t.dataset.fields=JSON.stringify(e.fields),t.dataset.status=e.status,t.dataset.img=e.thumbnail;let s=t.querySelector(".gallery");if(e.images){t.dataset.images=e.images;let o=s.querySelector("img");for(var i of e.images){let e=o.cloneNode(!0);e.src=i.src,i.alt&&(e.alt=i.alt),s.appendChild(e)}o.remove()}else s.remove();let o=[],a=t.querySelector(".taxonomies"),n=a.querySelector(".label-group"),l=n.querySelector(".tax"),r=!1;for(let s in e.taxonomies){if(Object.keys(e.taxonomies[s].terms).length>0){r=!0,t.dataset[s]=JSON.stringify(e.taxonomies[s].terms);let i=n.cloneNode(!0),o=jvbSettings.icons[s];for(var c in i.innerHTML=o+i.innerHTML,i.querySelector(".screen-reader-text").textContent=e.taxonomies[s].name,e.taxonomies[s].terms){let e=l.cloneNode(!0);e.textContent=c.name,i.appendChild(e)}}else t.dataset[s]=JSON.stringify({});o.push(s)}r?(n.remove(),l.remove()):a.remove(),0===Object.keys(this.config.taxonomies).length&&(this.config.taxonomies=o);let d=t.querySelector("img");d.src=e.thumbnail,e.alt&&(d.alt=e.alt),t.querySelector(".date").textContent=formatDate(e.date);let u="Hide "+e.icon;"draft"===e.status&&(u="Show "+e.icon);let h=t.querySelector('button[data-action="toggle-status"]');return h.prepend(getIcon(e.status)),h.title=u,this.initItemEventListeners(t),t}initItemEventListeners(e){e.addEventListener("click",(t=>t.target.closest(".item-select")?(t.preventDefault(),this.toggleItemSelection(e,!e.classList.contains("selected")),void this.updateBulkControls()):t.target.closest(".action")?(t.preventDefault(),void this.handleItemAction(t.target.closest(".action").dataset.action,e)):void 0))}initInfiniteScroll(){this.elements.scrollSentinel&&new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.queue[this.state.filters.status].hasMore&&this.loadContent(!1)}))})).observe(this.elements.scrollSentinel)}initStatusFilters(){const e=this.elements.container.querySelector(".controls");e&&e.addEventListener("change",(e=>{if("radio"===e.target.type&&"status-filters"===e.target.name){const t=e.target.id;t!==this.state.filters.status&&(this.state.filters.status=t,this.updateBulkActionOptions(),0===this.queue[t].items.size?this.loadContent(!0):this.renderContent())}}))}initDateFilters(){const e=this.elements.container.querySelector("select.date-filter"),t=this.elements.container.querySelector(".date-range");let s;if(e&&(this.hasFilters=!0,e.addEventListener("change",(e=>{const i=e.target.value;if(s=i,"custom"===i)return void t.showModal();t.close();const o=t.querySelector(".month-select");o&&(o.value=""),this.setDateFilter(i)})),e.addEventListener("click",(i=>{"custom"===s&&"custom"===e.value&&t.showModal()}))),t){const e=t.querySelector(".date-start"),s=t.querySelector(".date-end"),i=t.querySelector(".month-select");i&&i.addEventListener("change",(e=>{const[s,i]=e.target.value.split("-");if(s&&i){const e=new Date(s,i-1,1),o=new Date(s,i,0);o.setHours(23,59,59,999),this.setDateFilter("custom",e,o),t.close()}}));const o=()=>{const i=e.value,o=s.value;if(i&&o){const e=new Date(i),s=new Date(o);s.setHours(23,59,59,999),this.setDateFilter("custom",e,s),t.close()}};e.addEventListener("change",o),s.addEventListener("change",o)}}setDateFilter(e,t=null,s=null){const i=new Date;i.setHours(23,59,59,999);let o=t,a=s||i;if(!t&&""!==e)switch(o=new Date,e){case"today":o.setHours(0,0,0,0);break;case"week":o.setDate(i.getDate()-7);break;case"month":o.setMonth(i.getMonth()-1);break;case"year":o.setFullYear(i.getFullYear()-1)}this.state.filters.date=e?{range:{after:o.toISOString(),before:a.toISOString()},custom:"custom"===e}:{range:null,custom:!1},this.updateClearFiltersButton(),this.state.page=1,this.loadContent()}initTaxonomyFilters(){const e=this.elements.container.querySelectorAll(".filter[data-taxonomy]");e.length&&(this.hasFilters=!0,e.forEach((e=>{e.addEventListener("change",(e=>{const t=e.target.dataset.taxonomy,s=e.target.value;s?this.state.filters.taxonomies[t]=[parseInt(s)]:delete this.state.filters.taxonomies[t],this.updateClearFiltersButton(),this.state.page=1,this.loadContent(!0)}))})))}updateClearFiltersButton(){const e=document.querySelector(this.config.selectors.clearButton);if(!e)return;const t=Object.keys(this.state.filters.taxonomies).length>0||null!==this.state.filters.date.range;e.hidden=!t}clearAllFilters(){this.elements.container.querySelectorAll(".filter[data-taxonomy]").forEach((e=>e.value=""));const e=this.elements.container.querySelector("select.date-filter");e&&(e.value=""),this.state.filters={date:{range:null,custom:!1},taxonomies:{}},this.updateClearFiltersButton(),this.state.page=1,this.loadContent(!0)}initClearFilters(){this.config.selectors.clearButton&&document.querySelector(this.config.selectors.clearButton).addEventListener("click",(()=>this.clearAllFilters()))}initViewControls(){const e=this.elements.container.querySelector(".view-controls");if(!e)return;e.addEventListener("change",(e=>{const t=e.target;"radio"===t.type&&(this.setView(t.value),this.loadContent(!0))}));const t=localStorage.getItem(`${this.config.content}_view`)||"grid",s=e.querySelector(`input[value="${t}"]`);s&&(s.checked=!0,this.setView(t))}setView(e){this.state.view=e;const t=new Set(this.state.selected);this.elements.grid.classList.remove("grid-view","list-view"),this.elements.grid.classList.add(`${e}-view`),localStorage.setItem(`${this.config.content}_view`,e),this.loadContent(!0),t.forEach((e=>{const t=this.elements.grid.querySelector(`[data-id="${e}"]`);if(t){const e=t.querySelector('input[type="checkbox"]');e&&(e.checked=!0,t.classList.add("selected"))}})),this.updateBulkControls()}initBulkControls(){if(!this.elements.bulkControls)return;this.selectAll=this.elements.bulkControls.querySelector(".select-all"),this.selectAll&&this.selectAll.addEventListener("change",(()=>{this.getVisibleItems().forEach((e=>{this.toggleItemSelection(e,this.selectAll.checked)})),this.updateBulkControls()}));const e=this.elements.bulkControls.querySelector(".bulk-action-select"),t=this.elements.bulkControls.querySelector(".apply-bulk");t&&e&&(this.updateBulkActionOptions(),this.elements.container.querySelector(".status-filters"),t.addEventListener("click",(()=>{const t=e.value;if(!t)return;const s=Array.from(this.state.selected);switch(t){case"restore":this.handleBulkOperation("restore",s);break;case"delete":confirm(`Hold up! Are you sure you want to permanently delete these ${this.config.plural}?\n\nThis is a forever kind of deal - no taking it back.`)&&this.handleBulkOperation("delete",s);break;case"trash":this.handleBulkOperation("trash",s);break;case"edit":this.openBulkEditModal();const e=document.querySelector(".bulk-edit-modal");if(e){const t=e.querySelector(".selected-count");t&&(t.textContent=`( ${s.length} items )`);const i=e.querySelector(".selected");if(i){let e="";s.forEach((t=>{let s=this.elements.grid.querySelector('[data-id="'+t+'"]');e+='<input type="checkbox" id="selected-'+t+'" name="posts" value="'+t+'" checked><label for="selected-'+t+'"><img width="100%" height="auto" src="'+s.dataset.img+'"></label>'})),i.innerHTML=e}}break;case"publish":case"draft":this.handleBulkOperation(t,s)}e.value=""})));const s=this.elements.bulkControls.querySelector(".cancel-bulk");s&&s.addEventListener("click",(()=>{this.clearSelection()}))}updateBulkActionOptions(){const e=this.elements.bulkControls.querySelector(".bulk-action-select");e&&("trash"===this.state.filters.status?e.innerHTML='\n            <option value="">Bulk Actions...</option>\n            <option value="restore">Restore</option>\n            <option value="delete">Permanently Delete</option>\n        ':e.innerHTML='\n            <option value="">Bulk Actions...</option>\n            <option value="edit">Edit</option>\n            <option value="publish">Show</option>\n            <option value="draft">Hide</option>\n            <option value="trash">Scrap</option>\n        ')}initModals(){this.elements.editModal&&(this.editModal=new window.jvbModal(this.elements.editModal,{open:!1,close:this.elements.editModal.querySelector(".cancel"),save:this.elements.editModal.querySelector(".save"),onSave:()=>{const e=new FormData(this.elements.editModal.querySelector("form"));let t={};const s=this.elements.editModal.querySelectorAll(".taxonomies .jvb-selector");let i=Object.fromEntries(e);s.forEach((e=>{const s=e.dataset.taxonomy.replace(jvbSettings.base||"jvb_","");if(delete i["edit-"+s],e.__instance){const i=e.__instance.selectedItems;i&&Object.keys(i).length>0&&(t[s]=Object.keys(i).join(","))}})),i.taxonomies=t;for(let[e,t]of Object.entries(i))(""===t||window.isEmptyObject(t))&&delete i[e];this.queueContentUpdate(this.elements.editModal.dataset.id,i)}}));const e=this.elements.bulkEditModal;if(e){let t=!1;const s=e.querySelector("form");s?.addEventListener("change",(()=>{t=!0})),e.addEventListener("keydown",(s=>{"Escape"===s.key&&(s.preventDefault(),this.handleModalClose(e,t))})),e.addEventListener("click",(s=>{s.target===e&&this.handleModalClose(e,t)})),e.querySelector(".cancel")?.addEventListener("click",(()=>{this.handleModalClose(e,t),this.clearSelection()})),e.querySelector(".save")?.addEventListener("click",(()=>{const i=new FormData(s),o=Array.from(i.getAll("posts")),a={};""===i.get("term_name")&&(i.delete("term_name"),i.delete("select_parent"));let n={};e.querySelectorAll(".taxonomies .jvb-selector").forEach((e=>{const t=e.dataset.taxonomy.replace(jvbSettings.base||"jvb_","");if(e.__instance){const s=e.__instance.selectedItems;s&&Object.keys(s).length>0&&(n[t]=Object.keys(s).join(","))}})),o.forEach((e=>{a[e]={append:!0,content:this.config.content,status:i.get("bulk_status"),taxonomies:n}})),this.queueManager.addToQueue({type:"content_update",data:{posts:a}}),t=!1,e.close(),this.clearSelection()})),e.addEventListener("submit",(i=>{const o=new FormData(s),a=Array.from(o.getAll("posts")),n={};""===o.get("term_name")&&(o.delete("term_name"),o.delete("select_parent"));let l={};for(const e of this.config.taxonomies)l[e]=o.getAll(e),o.delete(e);a.forEach((e=>{n[e]={append:!0,content:this.config.content,status:o.get("bulk_status"),taxonomies:l}})),this.queueManager.addToQueue({type:"content_update",data:{posts:n}}),t=!1,e.close(),this.clearSelection()}))}this.openEditModal=e=>{console.log("Openening whatsit");const t=this.editModal.modal;if(!t)return;console.log("continuing");let s=e.dataset.id;t.dataset.id=s;let i=JSON.parse(e.dataset.fields),o=e.dataset.status;t.querySelector("input#set-"+o).checked=!0;for(let s in i){let o=i[s];o&&(t.querySelector("[name="+s+"]").value=o,"featured_image"===s&&(console.log(e),t.querySelector("[data-field=featured_image] .image-display").classList.add("has-image"),t.querySelector("[data-field=featured_image] .image-display img").src=e.dataset.img))}t.querySelector(".image")&&document.querySelectorAll(".image").forEach((e=>{const t=e.dataset.field,i=e.querySelector(".file-upload-container"),o=(new window.jvbFileUploader(e,{mode:"direct",content:this.config.content,postID:s,fieldName:t,type:"image_upload",selectors:{dropZone:i,uploader:e},onSuccess:t=>this.handleImageUploadSuccess(t,e),onError:t=>this.handleImageUploadError(t,e)}),e.querySelector(".remove-image"));o&&o.addEventListener("click",(()=>{this.handleImageRemove(e)}));const a=e.querySelector(".replace-image");a&&a.addEventListener("click",(()=>{e.querySelector('input[type="file"]').click()}))})),t.querySelector(".gallery")&&document.querySelectorAll(".gallery").forEach((t=>{const i=t.dataset.field,o=t.querySelector(".gallery-preview");e.dataset.images&&e.dataset.images.split(",").forEach((e=>{this.addToGalleryPreview(e,o)})),new window.jvbFileUploader(t,{mode:"gallery",selectors:{dropZone:t.querySelector(".file-upload-container"),previewGrid:o,uploader:t},type:"image_upload",content:this.config.content,postID:s,fieldName:i,onUploadComplete:e=>{const s=t.querySelector('input[type="hidden"]'),i=s.value?s.value.split(","):[],a=e.data.map((e=>e.attachment_id));s.value=[...i,...a].join(","),e.data.forEach((e=>{const t=document.createElement("div");t.className="preview-item",t.dataset.id=e.attachment_id,t.draggable=!0,t.innerHTML=`\n                        <img src="${e.url}" alt="Upload preview">\n                        <button type="button" class="remove-preview">\n                            ${jvbSettings.icons.delete}\n                        </button>\n                        <button type="button" class="move-image">\n                            ${jvbSettings.icons.grab}\n                        </button>\n                    `,o.appendChild(t)})),s.dispatchEvent(new Event("change",{bubbles:!0}))}}),new Sortable(o,{animation:150,handle:".move-image",onEnd:()=>{const e=t.querySelector('input[type="hidden"]'),s=[...o.querySelectorAll(".preview-item")].map((e=>e.dataset.id));e.value=s.join(","),e.dispatchEvent(new Event("change",{bubbles:!0}))}})})),t.querySelector(".taxonomies")&&t.querySelectorAll(".taxonomies .jvb-selector").forEach((t=>{let s=t.dataset.taxonomy,i=(t.classList.contains("hierarchical"),JSON.parse(t.dataset.config)),o=e.dataset[s]?JSON.parse(e.dataset[s]):{},a=i.common;t.__instance=new window.jvbSelector(t,{title:"Select "+s+"(s)",selected:o,common:a,allowMultiple:i.multiple,createNew:!0})})),t.showModal()},this.openBulkEditModal=()=>{const t=this.elements.bulkEditModal;if(!t)return;const s=this.state.selected,i=t.querySelector(".selected-count");i&&(i.textContent=`(${s.length} items)`),e.querySelectorAll(".taxonomies .jvb-selector").forEach((e=>{const t=e.dataset.taxonomy,s=(e.classList.contains("hierarchical"),JSON.parse(e.dataset.config));e.__instance=new window.jvbSelector(e,{title:`Select ${t}(s)`,values:{},allowMultiple:s.multiple,appendMode:!0,createNew:!0})})),t.showModal()}}handleModalClose(e,t){return t?!!confirm("You have unsaved changes. Are you sure you want to close this window?")&&(e.querySelectorAll(".gallery").forEach((e=>{e.__uploader&&(e.__uploader.cleanup(),delete e.__uploader)})),e.close(),!0):(e.close(),!0)}addToGalleryPreview(e,t){const s=document.createElement("div");return s.className="preview-item",s.draggable=!0,s.innerHTML=`\n        <img src="${e}" alt="Upload preview">\n        <div class="upload-status">\n            <div class="upload-progress"></div>\n        </div>\n        <button type="button" class="remove-preview" title="Remove Image">\n            ${jvbSettings.icons.delete}\n        </button>\n        <button type="button" class="move-image" title="Reorder Image">\n            ${jvbSettings.icons.grab}\n        </button>\n    `,t.appendChild(s),s}handleImageUploadSuccess(e,t){if(!e.data||!e.data.length)return;const s=t.querySelector(".image-display");removeChildren(s),s.classList.add("has-image");let i=[];e.data.forEach((e=>{let t=new Image;t.src=e.url,i.push(e.attachment_id),s.appendChild(t)})),t.querySelector('input[type="hidden"]').value=i.join(","),t.querySelector(".file-upload-container").hidden=!0,this.showNotification("Image updated successfully")}handleImageUploadError(e,t){console.error("Upload error:",e),this.showNotification("Failed to upload image","error"),t.querySelector(".file-upload-container").hidden=!1;const s=t.querySelector(".file-error");s&&(s.textContent="")}handleImageRemove(e){const t=e.querySelector(".image-display"),s=t.querySelector("img"),i=e.querySelector('input[type="hidden"]'),o=e.querySelector(".file-upload-container");i.value="",s.src="",t.classList.remove("has-image"),o.hidden=!1,this.showNotification("Image removed")}clearSelection(){this.getVisibleItems().forEach((e=>this.toggleItemSelection(e,!1))),this.state.selected.clear(),this.selectAll.checked=!1,this.updateBulkControls()}updateBulkControls(){const e=this.state.selected.size>0;this.elements.grid.classList.toggle("selecting",e),this.elements.bulkControls.classList.toggle("has-selection",e),this.elements.bulkControls.querySelector(".bulk-actions").hidden=!e,e&&document.addEventListener("keydown",(e=>{"Escape"===e.key&&this.state.selected.size>0&&(this.clearSelection(),this.showNotification("Selection cleared"))}));const t=this.elements.bulkControls.querySelector(".selected-count");t&&(t.textContent=e?`( ${this.state.selected.size} selected )`:"")}getVisibleItems(){return Array.from(this.elements.grid.querySelectorAll(".item:not([hidden])"))}showNotification(e,t="success"){window.jvbNotifications?window.jvbNotifications.showPopupNotification({message:e,type:t,priority:"medium",duration:3e3}):alert(e)}};
\ No newline at end of file
+window.contentManager=class{constructor(e){this.config={content:"",plural:"",taxonomies:{},selectors:{container:".items-list",grid:".item-grid:not(.preview)",uploadZone:".file-upload-wrapper",statusFilters:".status-filters",dateFilters:".date-filters",taxonomyFilters:".taxonomy-filters",viewControls:".view-controls",bulkControls:".bulk-controls",scrollSentinel:".scroll-sentinel",editModal:".edit-modal",bulkEditModal:".bulk-edit-modal",clearButton:".clear-filters"},createPostPerFile:!0,uploadConfig:{mode:"direct",allowMultiple:!0,createPostPerFile:!0,maxSize:5242880,allowedTypes:["image/jpeg","image/png","image/gif","image/webp"]},...e},this.resetCache=!1,this.queueManager=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.error=window.jvbError,this.state={selected:new Set,filters:{status:"all",taxonomies:{},date:null},view:localStorage.getItem(`${this.config.content}_view`)||"grid",loading:!1},this.queue={all:{items:new Map,page:1,hasMore:!0,totalPages:0},draft:{items:new Map,page:1,hasMore:!0,totalPages:0},publish:{items:new Map,page:1,hasMore:!0,totalPages:0},trash:{items:new Map,page:1,hasMore:!0,totalPages:0}},this.init()}async init(){this.elements={},Object.entries(this.config.selectors).forEach((([e,t])=>{this.elements[e]=document.querySelector(t)})),this.config.uploadConfig&&(this.fileUploader=new window.jvbFileUploader({...this.config.uploadConfig,content:this.config.content,fieldName:null})),this.initStatusFilters(),this.initDateFilters(),this.initTaxonomyFilters(),this.initClearFilters(),this.initViewControls(),this.initBulkControls(),this.initInfiniteScroll(),this.initModals(),await this.loadContent()}queueContentUpdate(e,t){const s={type:"content_update",data:{posts:{[e]:{content:this.config.content,...t}},content:this.config.content}};this.queueManager.addToQueue(s),this.updateLocalState(e,t)}queueBulkUpdate(e,t){const s={};e.forEach((e=>{s[e]={content:this.config.content,...t}}));const i={user:window.auth.getUser(),type:"content_update",data:{posts:s}};this.queueManager.addToQueue(i),e.forEach((e=>this.updateLocalState(e,t)))}updateLocalState(e,t){const s=this.queue[this.state.filters.status].items.get(e);if(s){Object.assign(s,t),this.queue[this.state.filters.status].items.set(e,s);const i=this.elements.grid.querySelector(`[data-id="${e}"]`);i&&this.updateItemElement(i,s)}}processFormData(e){const t={};for(const[s,i]of e.entries())if("status"===s)t.status=i;else if(s.startsWith("taxonomy_")){const e=s.replace("taxonomy_","");t.taxonomies||(t.taxonomies={}),t.taxonomies[e]=Array.isArray(i)?i:[i]}else t[s]=i;return t}updateItemElement(e,t){e.classList.remove("draft","publish","trash"),e.classList.add(t.status);const s=e.querySelector(".action-status");s&&(removeChildren(s),s.append(getIcon(t.status))),t.taxonomies&&e.querySelectorAll(".label-group").forEach((e=>{const s=e.dataset.taxonomy;if(s&&t.taxonomies[s]){const i=t.taxonomies[s].terms;e.querySelector(".terms").innerHTML=this.renderTerms(i)}}))}handleItemAction(e,t){const s=t.dataset.id;switch(e){case"edit":this.editModal.handleOpen(),this.openEditModal(t),this.editModal.form&&new FormFields(this.editModal.form,{onSave:this.editModal.onSave(),itemID:t.dataset.id});break;case"restore":this.queueContentUpdate(s,{status:"draft"}),t.remove();break;case"trash":this.queueContentUpdate(s,{status:"trash"}),t.remove();break;case"delete":confirm(`Hold up! Are you sure you want to permanently delete this ${this.config.content}?\n\nThis is a forever kind of deal - no taking it back.`)&&(this.queueContentUpdate(s,{status:"delete"}),t.remove());break;case"toggle-status":const e="publish"===t.dataset.status?"draft":"publish";this.queueContentUpdate(s,{status:e}),t.dataset.status=e,removeChildren(t.querySelector(".action-status")),t.querySelector(".action-status").append(getIcon(e))}}async handleBulkOperation(e,t){window.jvbLoading.show("Processing bulk changes...");try{const s={};t.forEach((t=>{s[t]={content:this.config.content,status:e},["delete","trash","restore"].includes(e)&&document.querySelector('[data-id="'+t+'"]').remove()})),this.queueManager.addToQueue({type:"content_update",data:{posts:s}}),this.clearSelection(),this.showNotification("Bulk changes queued for processing")}catch(e){console.error("Bulk operation failed:",e),this.showNotification("Failed to queue bulk operation","error")}finally{window.jvbLoading.hide()}}getQueryKey(){return JSON.stringify({status:this.state.filters.status,page:this.state.page,filters:this.state.filters})}toggleItemSelection(e,t){const s=e.dataset.id;t?(this.state.selected.add(s),e.classList.add("selected"),e.querySelector("input[type=checkbox]").checked=!0):(this.state.selected.delete(s),e.classList.remove("selected"),e.querySelector("input[type=checkbox]").checked=!1)}async loadContent(e=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const t=this.state.filters.status;console.log("Loading Page: "),console.log(this.queue[t].page);const s=new URLSearchParams;s.set("type",this.config.content),s.set("page",this.queue[t].page),s.set("filters",JSON.stringify(this.state.filters)),s.set("user",window.auth.getUser()),e&&(this.queue[t].page=1,this.queue[t].items.clear(),removeChildren(this.elements.grid),this.elements.grid.classList.remove("empty"));const i=await this.cache.fetchWithCache(`${jvbSettings.api}content?`+s,{method:"GET",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("dash")}},{context:window.auth.getUser()+"-"+this.config.content,forceRefresh:!1});i.total>0?(this.elements.grid.classList.remove("empty"),i.items.forEach((e=>{this.queue[t].items.set(e.id,e)})),this.queue[t].page++,this.queue[t].totalPages=i.total_pages,this.queue[t].hasMore=this.queue[t].page<i.total_pages):(this.elements.grid.classList.add("empty"),this.elements.grid.innerHTML=`<div class="empty-state"><h3>${jvbSettings.icons[this.config.content]}Nothing here${jvbSettings.icons[this.config.content]}</h3><p>It doesn't look like you have any ${this.config.plural} yet.</p><p><small><i>Add some by uploading images above.</i></small></p></div>`,this.queue[t].page=1,this.queue[t].hasMore=!1),this.renderContent()}catch(e){console.error("Error loading content:",e),this.loadingManager.showError("Failed to load content")}finally{this.state.loading=!1,this.loadingManager.hide()}}renderContent(){const e=this.state.filters.status,t=this.queue[e].items;t.size>0&&(this.elements.grid.classList.remove("empty"),this.elements.grid.querySelector(".empty-state")&&removeChildren(this.elements.grid));const s=document.createDocumentFragment();t.forEach((t=>{const i=this.elements.grid.querySelector(`[data-id="${t.id}"]`);if(i){if(t.view!==this.state.view){const e=this.createItemElement(t);t.view=this.state.view,i.replaceWith(e)}}else{const e=this.createItemElement(t);t.view=this.state.view,s.appendChild(e)}this.queue[e].items.set(t.id,t)})),s.children.length>0&&this.elements.grid.appendChild(s)}createItemElement(e){let t=window.getTemplate(this.state.view+"View");t.classList.add(e.status),t.dataset.id=e.id,t.dataset.fields=JSON.stringify(e.fields),t.dataset.status=e.status,t.dataset.img=e.thumbnail;let s=t.querySelector(".gallery");if(e.images){t.dataset.images=e.images;let o=s.querySelector("img");for(var i of e.images){let e=o.cloneNode(!0);e.src=i.src,i.alt&&(e.alt=i.alt),s.appendChild(e)}o.remove()}else s.remove();let o=[],a=t.querySelector(".taxonomies"),n=a.querySelector(".label-group"),l=n.querySelector(".tax"),r=!1;for(let s in e.taxonomies){if(Object.keys(e.taxonomies[s].terms).length>0){r=!0,t.dataset[s]=JSON.stringify(e.taxonomies[s].terms);let i=n.cloneNode(!0),o=jvbSettings.icons[s];for(var c in i.innerHTML=o+i.innerHTML,i.querySelector(".screen-reader-text").textContent=e.taxonomies[s].name,e.taxonomies[s].terms){let e=l.cloneNode(!0);e.textContent=c.name,i.appendChild(e)}}else t.dataset[s]=JSON.stringify({});o.push(s)}r?(n.remove(),l.remove()):a.remove(),0===Object.keys(this.config.taxonomies).length&&(this.config.taxonomies=o);let d=t.querySelector("img");d.src=e.thumbnail,e.alt&&(d.alt=e.alt),t.querySelector(".date").textContent=formatDate(e.date);let u="Hide "+e.icon;"draft"===e.status&&(u="Show "+e.icon);let h=t.querySelector('button[data-action="toggle-status"]');return h.prepend(getIcon(e.status)),h.title=u,this.initItemEventListeners(t),t}initItemEventListeners(e){e.addEventListener("click",(t=>t.target.closest(".item-select")?(t.preventDefault(),this.toggleItemSelection(e,!e.classList.contains("selected")),void this.updateBulkControls()):t.target.closest(".action")?(t.preventDefault(),void this.handleItemAction(t.target.closest(".action").dataset.action,e)):void 0))}initInfiniteScroll(){this.elements.scrollSentinel&&new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.queue[this.state.filters.status].hasMore&&this.loadContent(!1)}))})).observe(this.elements.scrollSentinel)}initStatusFilters(){const e=this.elements.container.querySelector(".controls");e&&e.addEventListener("change",(e=>{if("radio"===e.target.type&&"status-filters"===e.target.name){const t=e.target.id;t!==this.state.filters.status&&(this.state.filters.status=t,this.updateBulkActionOptions(),0===this.queue[t].items.size?this.loadContent(!0):this.renderContent())}}))}initDateFilters(){const e=this.elements.container.querySelector("select.date-filter"),t=this.elements.container.querySelector(".date-range");let s;if(e&&(this.hasFilters=!0,e.addEventListener("change",(e=>{const i=e.target.value;if(s=i,"custom"===i)return void t.showModal();t.close();const o=t.querySelector(".month-select");o&&(o.value=""),this.setDateFilter(i)})),e.addEventListener("click",(i=>{"custom"===s&&"custom"===e.value&&t.showModal()}))),t){const e=t.querySelector(".date-start"),s=t.querySelector(".date-end"),i=t.querySelector(".month-select");i&&i.addEventListener("change",(e=>{const[s,i]=e.target.value.split("-");if(s&&i){const e=new Date(s,i-1,1),o=new Date(s,i,0);o.setHours(23,59,59,999),this.setDateFilter("custom",e,o),t.close()}}));const o=()=>{const i=e.value,o=s.value;if(i&&o){const e=new Date(i),s=new Date(o);s.setHours(23,59,59,999),this.setDateFilter("custom",e,s),t.close()}};e.addEventListener("change",o),s.addEventListener("change",o)}}setDateFilter(e,t=null,s=null){const i=new Date;i.setHours(23,59,59,999);let o=t,a=s||i;if(!t&&""!==e)switch(o=new Date,e){case"today":o.setHours(0,0,0,0);break;case"week":o.setDate(i.getDate()-7);break;case"month":o.setMonth(i.getMonth()-1);break;case"year":o.setFullYear(i.getFullYear()-1)}this.state.filters.date=e?{range:{after:o.toISOString(),before:a.toISOString()},custom:"custom"===e}:{range:null,custom:!1},this.updateClearFiltersButton(),this.state.page=1,this.loadContent()}initTaxonomyFilters(){const e=this.elements.container.querySelectorAll(".filter[data-taxonomy]");e.length&&(this.hasFilters=!0,e.forEach((e=>{e.addEventListener("change",(e=>{const t=e.target.dataset.taxonomy,s=e.target.value;s?this.state.filters.taxonomies[t]=[parseInt(s)]:delete this.state.filters.taxonomies[t],this.updateClearFiltersButton(),this.state.page=1,this.loadContent(!0)}))})))}updateClearFiltersButton(){const e=document.querySelector(this.config.selectors.clearButton);if(!e)return;const t=Object.keys(this.state.filters.taxonomies).length>0||null!==this.state.filters.date.range;e.hidden=!t}clearAllFilters(){this.elements.container.querySelectorAll(".filter[data-taxonomy]").forEach((e=>e.value=""));const e=this.elements.container.querySelector("select.date-filter");e&&(e.value=""),this.state.filters={date:{range:null,custom:!1},taxonomies:{}},this.updateClearFiltersButton(),this.state.page=1,this.loadContent(!0)}initClearFilters(){this.config.selectors.clearButton&&document.querySelector(this.config.selectors.clearButton).addEventListener("click",(()=>this.clearAllFilters()))}initViewControls(){const e=this.elements.container.querySelector(".view-controls");if(!e)return;e.addEventListener("change",(e=>{const t=e.target;"radio"===t.type&&(this.setView(t.value),this.loadContent(!0))}));const t=localStorage.getItem(`${this.config.content}_view`)||"grid",s=e.querySelector(`input[value="${t}"]`);s&&(s.checked=!0,this.setView(t))}setView(e){this.state.view=e;const t=new Set(this.state.selected);this.elements.grid.classList.remove("grid-view","list-view"),this.elements.grid.classList.add(`${e}-view`),localStorage.setItem(`${this.config.content}_view`,e),this.loadContent(!0),t.forEach((e=>{const t=this.elements.grid.querySelector(`[data-id="${e}"]`);if(t){const e=t.querySelector('input[type="checkbox"]');e&&(e.checked=!0,t.classList.add("selected"))}})),this.updateBulkControls()}initBulkControls(){if(!this.elements.bulkControls)return;this.selectAll=this.elements.bulkControls.querySelector(".select-all"),this.selectAll&&this.selectAll.addEventListener("change",(()=>{this.getVisibleItems().forEach((e=>{this.toggleItemSelection(e,this.selectAll.checked)})),this.updateBulkControls()}));const e=this.elements.bulkControls.querySelector(".bulk-action-select"),t=this.elements.bulkControls.querySelector(".apply-bulk");t&&e&&(this.updateBulkActionOptions(),this.elements.container.querySelector(".status-filters"),t.addEventListener("click",(()=>{const t=e.value;if(!t)return;const s=Array.from(this.state.selected);switch(t){case"restore":this.handleBulkOperation("restore",s);break;case"delete":confirm(`Hold up! Are you sure you want to permanently delete these ${this.config.plural}?\n\nThis is a forever kind of deal - no taking it back.`)&&this.handleBulkOperation("delete",s);break;case"trash":this.handleBulkOperation("trash",s);break;case"edit":this.openBulkEditModal();const e=document.querySelector(".bulk-edit-modal");if(e){const t=e.querySelector(".selected-count");t&&(t.textContent=`( ${s.length} items )`);const i=e.querySelector(".selected");if(i){let e="";s.forEach((t=>{let s=this.elements.grid.querySelector('[data-id="'+t+'"]');e+='<input type="checkbox" id="selected-'+t+'" name="posts" value="'+t+'" checked><label for="selected-'+t+'"><img width="100%" height="auto" src="'+s.dataset.img+'"></label>'})),i.innerHTML=e}}break;case"publish":case"draft":this.handleBulkOperation(t,s)}e.value=""})));const s=this.elements.bulkControls.querySelector(".cancel-bulk");s&&s.addEventListener("click",(()=>{this.clearSelection()}))}updateBulkActionOptions(){const e=this.elements.bulkControls.querySelector(".bulk-action-select");e&&("trash"===this.state.filters.status?e.innerHTML='\n            <option value="">Bulk Actions...</option>\n            <option value="restore">Restore</option>\n            <option value="delete">Permanently Delete</option>\n        ':e.innerHTML='\n            <option value="">Bulk Actions...</option>\n            <option value="edit">Edit</option>\n            <option value="publish">Show</option>\n            <option value="draft">Hide</option>\n            <option value="trash">Scrap</option>\n        ')}initModals(){this.elements.editModal&&(this.editModal=new window.jvbModal(this.elements.editModal,{open:!1,close:this.elements.editModal.querySelector(".cancel"),save:this.elements.editModal.querySelector(".save"),onSave:()=>{const e=new FormData(this.elements.editModal.querySelector("form"));let t={};const s=this.elements.editModal.querySelectorAll(".taxonomies .jvb-selector");let i=Object.fromEntries(e);s.forEach((e=>{const s=e.dataset.taxonomy.replace(jvbSettings.base||"jvb_","");if(delete i["edit-"+s],e.__instance){const i=e.__instance.selectedItems;i&&Object.keys(i).length>0&&(t[s]=Object.keys(i).join(","))}})),i.taxonomies=t;for(let[e,t]of Object.entries(i))""!==t&&0!==Object.keys(t).length||delete i[e];this.queueContentUpdate(this.elements.editModal.dataset.id,i)}}));const e=this.elements.bulkEditModal;if(e){let t=!1;const s=e.querySelector("form");s?.addEventListener("change",(()=>{t=!0})),e.addEventListener("keydown",(s=>{"Escape"===s.key&&(s.preventDefault(),this.handleModalClose(e,t))})),e.addEventListener("click",(s=>{s.target===e&&this.handleModalClose(e,t)})),e.querySelector(".cancel")?.addEventListener("click",(()=>{this.handleModalClose(e,t),this.clearSelection()})),e.querySelector(".save")?.addEventListener("click",(()=>{const i=new FormData(s),o=Array.from(i.getAll("posts")),a={};""===i.get("term_name")&&(i.delete("term_name"),i.delete("select_parent"));let n={};e.querySelectorAll(".taxonomies .jvb-selector").forEach((e=>{const t=e.dataset.taxonomy.replace(jvbSettings.base||"jvb_","");if(e.__instance){const s=e.__instance.selectedItems;s&&Object.keys(s).length>0&&(n[t]=Object.keys(s).join(","))}})),o.forEach((e=>{a[e]={append:!0,content:this.config.content,status:i.get("bulk_status"),taxonomies:n}})),this.queueManager.addToQueue({type:"content_update",data:{posts:a}}),t=!1,e.close(),this.clearSelection()})),e.addEventListener("submit",(i=>{const o=new FormData(s),a=Array.from(o.getAll("posts")),n={};""===o.get("term_name")&&(o.delete("term_name"),o.delete("select_parent"));let l={};for(const e of this.config.taxonomies)l[e]=o.getAll(e),o.delete(e);a.forEach((e=>{n[e]={append:!0,content:this.config.content,status:o.get("bulk_status"),taxonomies:l}})),this.queueManager.addToQueue({type:"content_update",data:{posts:n}}),t=!1,e.close(),this.clearSelection()}))}this.openEditModal=e=>{console.log("Openening whatsit");const t=this.editModal.modal;if(!t)return;console.log("continuing");let s=e.dataset.id;t.dataset.id=s;let i=JSON.parse(e.dataset.fields),o=e.dataset.status;t.querySelector("input#set-"+o).checked=!0;for(let s in i){let o=i[s];o&&(t.querySelector("[name="+s+"]").value=o,"featured_image"===s&&(console.log(e),t.querySelector("[data-field=featured_image] .image-display").classList.add("has-image"),t.querySelector("[data-field=featured_image] .image-display img").src=e.dataset.img))}t.querySelector(".image")&&document.querySelectorAll(".image").forEach((e=>{const t=e.dataset.field,i=e.querySelector(".file-upload-container"),o=(new window.jvbFileUploader(e,{mode:"direct",content:this.config.content,postID:s,fieldName:t,type:"image_upload",selectors:{dropZone:i,uploader:e},onSuccess:t=>this.handleImageUploadSuccess(t,e),onError:t=>this.handleImageUploadError(t,e)}),e.querySelector(".remove-image"));o&&o.addEventListener("click",(()=>{this.handleImageRemove(e)}));const a=e.querySelector(".replace-image");a&&a.addEventListener("click",(()=>{e.querySelector('input[type="file"]').click()}))})),t.querySelector(".gallery")&&document.querySelectorAll(".gallery").forEach((t=>{const i=t.dataset.field,o=t.querySelector(".gallery-preview");e.dataset.images&&e.dataset.images.split(",").forEach((e=>{this.addToGalleryPreview(e,o)})),new window.jvbFileUploader(t,{mode:"gallery",selectors:{dropZone:t.querySelector(".file-upload-container"),previewGrid:o,uploader:t},type:"image_upload",content:this.config.content,postID:s,fieldName:i,onUploadComplete:e=>{const s=t.querySelector('input[type="hidden"]'),i=s.value?s.value.split(","):[],a=e.data.map((e=>e.attachment_id));s.value=[...i,...a].join(","),e.data.forEach((e=>{const t=document.createElement("div");t.className="preview-item",t.dataset.id=e.attachment_id,t.draggable=!0,t.innerHTML=`\n                        <img src="${e.url}" alt="Upload preview">\n                        <button type="button" class="remove-preview">\n                            ${jvbSettings.icons.delete}\n                        </button>\n                        <button type="button" class="move-image">\n                            ${jvbSettings.icons.grab}\n                        </button>\n                    `,o.appendChild(t)})),s.dispatchEvent(new Event("change",{bubbles:!0}))}}),new Sortable(o,{animation:150,handle:".move-image",onEnd:()=>{const e=t.querySelector('input[type="hidden"]'),s=[...o.querySelectorAll(".preview-item")].map((e=>e.dataset.id));e.value=s.join(","),e.dispatchEvent(new Event("change",{bubbles:!0}))}})})),t.querySelector(".taxonomies")&&t.querySelectorAll(".taxonomies .jvb-selector").forEach((t=>{let s=t.dataset.taxonomy,i=(t.classList.contains("hierarchical"),JSON.parse(t.dataset.config)),o=e.dataset[s]?JSON.parse(e.dataset[s]):{},a=i.common;t.__instance=new window.jvbSelector(t,{title:"Select "+s+"(s)",selected:o,common:a,allowMultiple:i.multiple,createNew:!0})})),t.showModal()},this.openBulkEditModal=()=>{const t=this.elements.bulkEditModal;if(!t)return;const s=this.state.selected,i=t.querySelector(".selected-count");i&&(i.textContent=`(${s.length} items)`),e.querySelectorAll(".taxonomies .jvb-selector").forEach((e=>{const t=e.dataset.taxonomy,s=(e.classList.contains("hierarchical"),JSON.parse(e.dataset.config));e.__instance=new window.jvbSelector(e,{title:`Select ${t}(s)`,values:{},allowMultiple:s.multiple,appendMode:!0,createNew:!0})})),t.showModal()}}handleModalClose(e,t){return t?!!confirm("You have unsaved changes. Are you sure you want to close this window?")&&(e.querySelectorAll(".gallery").forEach((e=>{e.__uploader&&(e.__uploader.cleanup(),delete e.__uploader)})),e.close(),!0):(e.close(),!0)}addToGalleryPreview(e,t){const s=document.createElement("div");return s.className="preview-item",s.draggable=!0,s.innerHTML=`\n        <img src="${e}" alt="Upload preview">\n        <div class="upload-status">\n            <div class="upload-progress"></div>\n        </div>\n        <button type="button" class="remove-preview" title="Remove Image">\n            ${jvbSettings.icons.delete}\n        </button>\n        <button type="button" class="move-image" title="Reorder Image">\n            ${jvbSettings.icons.grab}\n        </button>\n    `,t.appendChild(s),s}handleImageUploadSuccess(e,t){if(!e.data||!e.data.length)return;const s=t.querySelector(".image-display");removeChildren(s),s.classList.add("has-image");let i=[];e.data.forEach((e=>{let t=new Image;t.src=e.url,i.push(e.attachment_id),s.appendChild(t)})),t.querySelector('input[type="hidden"]').value=i.join(","),t.querySelector(".file-upload-container").hidden=!0,this.showNotification("Image updated successfully")}handleImageUploadError(e,t){console.error("Upload error:",e),this.showNotification("Failed to upload image","error"),t.querySelector(".file-upload-container").hidden=!1;const s=t.querySelector(".file-error");s&&(s.textContent="")}handleImageRemove(e){const t=e.querySelector(".image-display"),s=t.querySelector("img"),i=e.querySelector('input[type="hidden"]'),o=e.querySelector(".file-upload-container");i.value="",s.src="",t.classList.remove("has-image"),o.hidden=!1,this.showNotification("Image removed")}clearSelection(){this.getVisibleItems().forEach((e=>this.toggleItemSelection(e,!1))),this.state.selected.clear(),this.selectAll.checked=!1,this.updateBulkControls()}updateBulkControls(){const e=this.state.selected.size>0;this.elements.grid.classList.toggle("selecting",e),this.elements.bulkControls.classList.toggle("has-selection",e),this.elements.bulkControls.querySelector(".bulk-actions").hidden=!e,e&&document.addEventListener("keydown",(e=>{"Escape"===e.key&&this.state.selected.size>0&&(this.clearSelection(),this.showNotification("Selection cleared"))}));const t=this.elements.bulkControls.querySelector(".selected-count");t&&(t.textContent=e?`( ${this.state.selected.size} selected )`:"")}getVisibleItems(){return Array.from(this.elements.grid.querySelectorAll(".item:not([hidden])"))}showNotification(e,t="success"){window.jvbNotifications?window.jvbNotifications.showPopupNotification({message:e,type:t,priority:"medium",duration:3e3}):alert(e)}};
\ No newline at end of file
diff --git a/assets/js/min/DashboardNavigator.min.js b/assets/js/min/DashboardNavigator.min.js
deleted file mode 100644
index 2d39a49..0000000
--- a/assets/js/min/DashboardNavigator.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{class e{constructor(){this.currentPage="",this.a11y=window.jvbA11y,this.mainContent=document.querySelector("main .replace"),this.loadingManager=window.jvbLoading,this.cache=JSON.parse(localStorage.getItem("dashboard-cache")||"{}"),this.cacheTimeout=3e5;const e=window.location.pathname.split("/dash/")[1];this.currentPage=e?e.replace("/",""):"",this.currentPage&&this.initPageContent(this.currentPage),this.initNavigation()}initNavigation(){const e=window.location.pathname.split("/dash/")[1];this.currentPage=e?e.replace("/",""):"",this.updateNavState(this.currentPage),window.addEventListener("popstate",(e=>{const t=e.state?.page||"";this.navigateTo(t,!0)})),document.querySelectorAll("a[data-dash]").forEach((e=>{e.addEventListener("click",(t=>{t.preventDefault(),this.navigateTo(e.dataset.page)}))}))}updateNavState(e){const t=document.querySelector(".dashboard-footer nav");t&&t.querySelectorAll("a").forEach((t=>{const n=t.dataset.page;t.parentElement.classList.toggle("current",n===e),n===e?t.setAttribute("aria-current","page"):t.removeAttribute("aria-current")}))}getCachedContent(e){const t=this.cache[e];return t?Date.now()-t.timestamp>this.cacheTimeout?(delete this.cache[e],localStorage.setItem("dashboard-cache",JSON.stringify(this.cache)),null):t.content:null}setCachedContent(e,t){e&&t&&(this.cache[e]={content:t,timestamp:Date.now()},localStorage.setItem("dashboard-cache",JSON.stringify(this.cache)))}async navigateTo(e,t=!1){try{if("dash"===e&&(e=""),e===this.currentPage)return;if(!t){const t=e?`/dash/${e}/`:"/dash";history.pushState({page:e},"",t)}const n=this.mainContent?.innerHTML;n&&this.setCachedContent(this.currentPage,n);const a=this.getCachedContent(e);if(a)return this.mainContent.innerHTML=a,this.currentPage=e,this.updateNavState(e),void this.initPageContent(e);window.location.href=e?`/dash/${e}/`:"/dash"}catch(e){console.error("Navigation error:",e),this.loadingManager&&this.loadingManager.showError(e.message||"Navigation failed")}finally{this.a11y.announce(`Currently on ${e} dashboard page.`)}}updateContent(e){this.mainContent.innerHTML=e.content,document.title=`${e.title} - edmonton.ink Dashboard`,this.currentPage&&this.initPageContent(this.currentPage)}initPageContent(e){switch(console.log(e),e){case"settings":this.initNorthehSettingsPage();break;case"bio":this.initBioPage();break;case"favourites":new window.favouritesManager;break;case"shop":this.initShopPage();break;case"news":new window.newsManager;break;case"events":new window.crud({content:"event"})}}initBioPage(){const e=document.querySelector("form");e&&(console.log(jvbSettings,"jvbSettings"),new window.formManager(e,{loadingManager:window.jvbLoading,objectId:jvbSettings.currentUser.artistID,highlights:{style:{max:3,name:"jvb_top_styles",label:"Highlight Styles",description:"Select up to 3 styles to highlight"}}}))}initNorthehSettingsPage(){new window.jvbTabs(document.querySelector(".replace"))}initSettingsPage(){const e=document.querySelector("form");window.jvbForm.addForm(e,{content:"artist",onSave:e=>{Object.hasOwn(e,"menu_section_order")&&(e.menu_section_order=JSON.stringify(e.menu_section_order)),e.user=jvbSettings.currentUser,window.jvbQueue.addToQueue({type:"user_settings",data:e})}})}initShopPage(){document.querySelector("form")&&new window.jvbShopManager}clearCache(){this.cache={}}}document.addEventListener("DOMContentLoaded",(()=>{window.dashboardNavigator=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/admin.min.js b/assets/js/min/admin.min.js
deleted file mode 100644
index 46abf96..0000000
--- a/assets/js/min/admin.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{class e{constructor(){this.queue=window.jvbQueue,this.loading=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.activeTab="artist",this.reset=!1,this.observer=null,this.form={},this.isSaving=!1,this.hasChanges=!1,this.trackedChanges=new Map,this.items=new Map,this.isLoading=!1,this.tabNav="vertical"===localStorage.getItem("jvbTabNav"),this.template=new Map,this.endpoints="myster",this.resetFilters(),this.hasMore=!0,this.maxPages=1,this.totalItems=0,this.initElements(),this.initEvents(),this.firstLoad=!1,this.firstLoad||(this.resetTable(),this.firstLoad=!0);let e=document.querySelectorAll("button.tab"),t={};this.modals={},e.forEach((e=>{let i=e.dataset.tab;t[i]=()=>{e.classList.contains("active")&&(this.activeTab=i),this.loading.setContent([this.activeTab]),this.resetTable(),this.resetFilters(),this.filters.content=i,localStorage.setItem("jvbAdminTab",i),this.loadItems(!0).then((()=>{}))},this.modals[i]=new window.jvbModal(document.querySelector(`dialog.edit-modal.${i}`,{open:!1,onSave:()=>this.saveEditModal.bind(this)})),this.items.set(i,new Map)})),this.tabs=new window.jvbTabs(document.querySelector(".replace"),t),this.loading.setContent([this.activeTab]),this.tabs.switchTab(this.activeTab),this.loadItems(),this.saveTimeout=null,this.SAVE_DELAY=5e3,this.debouncedSave=this.debouncedSave.bind(this)}resetFilters(){this.filters={page:1,order:"DESC",orderby:"name",content:this.activeTab}}resetTable(){removeChildren(this.grid);let e=window.getTemplate(`${this.activeTab}Table`).cloneNode(!0);this.row=`${this.activeTab}Row`;let t=e.querySelector("thead").cloneNode(!0),i=e.querySelector("tfoot");Array.from(t.children).forEach((e=>{i.appendChild(e.cloneNode(!0))})),this.grid.append(e),this.body=this.grid.querySelector("tbody"),this.grid.removeEventListener("change",this.boundChanges),this.boundChanges=this.trackChanges.bind(this),this.grid.addEventListener("change",this.boundChanges)}initElements(){this.container=document.querySelector(".replace"),this.grid=this.container.querySelector(".items-container"),this.tabToggle=this.container.querySelector("input#vertical"),this.tabToggle.checked=this.tabNav}trackChanges(e){this.hasChanges=!0;let t=e.target.closest("tr").id;this.trackedChanges.has(t)||this.trackedChanges.set(t,new Map);let i=e.target.name,a=e.target.value;this.trackedChanges.get(t).set(i,a),this.debouncedSave()}debouncedSave(){this.saveTimeout&&clearTimeout(this.saveTimeout),this.saveTimeout=setTimeout((()=>{this.processChanges()}),this.SAVE_DELAY)}async processChanges(){if(0!==this.trackedChanges.size)try{this.loading.showLoading();let e=t(this.trackedChanges);console.log("Saving changes:",e);const i=await fetch(`${jvbSettings.api}${this.endpoints}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbAdmin.nonce},body:JSON.stringify({user:jvbSettings.currentUser,data:e,content:this.activeTab})});if(!i.ok)throw new Error(`Server returned ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||"Unknown error");this.reset=!0,this.trackedChanges=new Map,this.hasChanges=!1,this.loadItems(),this.a11y.announce("Changes saved successfully")}catch(e){this.handleError(e,"saving changes")}finally{this.loading.hideLoading()}}initEvents(){this.tabToggle.addEventListener("change",(e=>{this.tabNav=e.target.checked;let t=e.target.checked?"vertical":"horizontal";localStorage.setItem("jvbTabNav",t),window.jvbA11y.announce(this.tabNav?"Changed to vertical navigation":"Changed to horizontal navigation")})),this.grid.addEventListener("keydown",(e=>{if("Tab"===e.key&&this.tabNav){let t=e.target.closest("td").dataset.id,i=e.target.closest("tr"),a=Array.from(this.body.querySelectorAll("tr")),s=a.indexOf(i),n=a.length;if(-1!==s&&s<n){e.preventDefault();let i=e.shiftKey?s-1:s+1;a[i].scrollIntoView({behavior:"smooth",block:"center",inline:"center"}),a[i].querySelector(`[data-id="${t}"] input`).focus()}s===n-5&&this.hasMore&&this.loadItems(!1)}})),this.clickListener=this.handleClick.bind(this),this.container.addEventListener("click",this.clickListener)}handleClick(e){if("button"!==e.target&&!e.target.closest('button[data-action="edit"]'))return;let t="button"===e.target?e.target.dataset.id:e.target.closest("button").dataset.id,i=this.items.get(this.activeTab).get(parseInt(t)),a=this.container.querySelector(`dialog.edit-modal.${this.activeTab}`);for(let[e,t]of Object.entries(i)){let i=a.querySelector(`[name="${e}"]`);i&&(i.value=t)}this.form.instance&&(this.form.instance=null),this.form.instance=this.handleForm(a.querySelector("form"),t),this.modals[this.activeTab].modal.dataset.id=i.id,this.modals[this.activeTab].handleOpen()}handleForm(e,t){return window.jvbForm.addForm(e,{onSave:this.saveEditModal.bind(this),itemID:t})}async saveEditModal(){if(this.isSaving)return;this.isSaving=!0;let e=this.modals[this.activeTab],t=e.modal.querySelector("form"),i=e.modal.dataset.id,a=(this.items.get(this.activeTab).get(parseInt(i)),new FormData(t));console.log(a);let s={};for(const[e,t]of a.entries())if(e.includes(":")){let i=e.split(":");s[i[0]]||(s[i[0]]={}),s[i[0]][i[1]]=t}else s[e]=t;console.log(s),a={},a[i]=s;try{await fetch(`${jvbSettings.api}${this.endpoints}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbAdmin.nonce},body:JSON.stringify({user:jvbSettings.currentUser,data:a,content:this.activeTab})})}catch(e){}finally{this.modals[this.activeTab].handleClose(),this.isSaving=!1}t.reset()}setupInfiniteScroll(){this.observer&&this.observer.disconnect();const e=this.body.lastElementChild;e&&(this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.hasMore&&!this.isLoading&&(console.log("Last row visible, loading more items"),this.loadItems(!1))}))}),{rootMargin:"200px 0px",threshold:.1}),this.observer.observe(e),console.log("Observing last row:",e))}async loadItems(e=!0){if(!this.isLoading&&this.hasMore)try{this.isLoading=!0,this.loading.showLoading(),e&&(this.filters.page=1,this.grid.classList.remove("empty"));const t=this.buildFilters();console.log(this.filters),console.log("Reset? ",this.reset);const i=await this.cache.fetchWithCache(`${jvbSettings.api}${this.endpoints}?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbAdmin.nonce}},{context:"admin",forceRefresh:!0});return console.log(i,"Fetched Data:"),this.renderItems(i.items||[],this.filters.page>1),[this.hasMore,this.totalItems,this.maxPages]=[i.has_more,i.total_items,i.total_pages],this.hasMore&&this.filters.page++,this.setupInfiniteScroll(),i}catch(e){throw this.handleError(e,"loading news"),e}finally{this.isLoading=!1,this.loading.hideLoading()}}buildFilters(){const e=JSON.parse(JSON.stringify(this.filters));let t={};for(var[i,a]of Object.entries(e))!1!==a&&null!==a&&(t[i]=a);return new URLSearchParams(t)}renderItems(e,t=!1){const i=document.createDocumentFragment(),a=s=>{const n=Math.min(s+10,e.length);for(let t=s;t<n;t++){const a=e[t],s=this.createItemElement(a);this.items.get(this.activeTab).set(a.id,a),i.appendChild(s)}n<e.length?requestAnimationFrame((()=>{a(n)})):(this.body.appendChild(i),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.hasMore),this.setupInfiniteScroll())};e.length>0?a(0):this.a11y.announceItems(0,t)}createItemElement(e){let t=window.getTemplate(this.row);t.id=e.id,t.querySelectorAll("td").forEach((t=>{let i=t.dataset.id,a=t.querySelector('input[type="text"]');a&&(a.value=e[i],t.querySelector("label").remove(),t.querySelector(".description").remove())}));let[i,a,s]=[t.querySelector('[data-id="actions"] input'),t.querySelector('[data-id="actions"] label'),t.querySelector('[data-id="actions"] button')];return[i.checked,i.id,a.htmlFor,s.dataset.id]=[e.public,`public-${e.id}`,`public-${e.id}`,e.id],t}createShopElement(e){let t=window.getTemplate(this.row);t.id=e.id;let[i,a,s,n,o,r,l,d,c,h,u,g,v,m,b,p,y,S,w,f]=[t.querySelector('[data-id="actions"] input'),t.querySelector('[data-id="actions"] label'),t.querySelector('[data-id="actions"] button'),t.querySelector('[data-id="term_name"] input'),t.querySelector('[data-id="owner"] input'),t.querySelector('[data-id="managers"] input'),t.querySelector('[data-id="city"] input'),t.querySelector('[data-id="location"] input'),t.querySelector('[data-id="established"] input'),t.querySelector('[data-id="phone"] input'),t.querySelector('[data-id="email"] input'),t.querySelector('[data-id="admin_contact"] input'),t.querySelector('[data-id="public_contact"] input'),t.querySelector('[data-id="links"] input'),t.querySelector('[data-id="rate"] input'),t.querySelector('[data-id="languages"] input'),t.querySelector('[data-id="keywords"] input'),t.querySelector('[data-id="slogan"] input'),t.querySelector('[data-id="insta_handle"] input'),t.querySelector('[data-id="followers"] input')];if([i.checked,i.id,a.htmlFor,s.dataset.id,n.value,o.value,r.value,l.value,d.value,c.value,h.value,u.value,g.value,v.value,b.value,S.value,w.value]=[e.public,`public-${e.id}`,`public-${e.id}`,e.id,e.term_name,e.owner,e.managers,e.city,e.location.address,e.established,e.phone,e.email,e.admin_contact,e.public_contact,e.rate,e.slogan,e.insta_handle],e.links.length>0){let t="";e.links.forEach((e=>{t+=`[${e.url}, ${e.title}, ${e.tracker}]`})),m.value=t.trim()}if(e.followers.length>0){let t="";e.followers.forEach((e=>{t+=`[${e.count}, ${e.source}, ${formatDate(e.checked)}]`})),f.value=t}if(e.keywords.length>0){let t=[];e.keywords.forEach((e=>{t.push(e.keyword)})),y.value=t.join(", ")}if(e.languages.length>0){let t=[];e.languages.forEach((e=>{t.push(e.language)})),p.value=t.join(", ")}return t}createPartnerElement(e){let t=window.getTemplate(this.row);t.id=e.id;let[i,a,s]=[t.querySelector('[data-id="actions"] input'),t.querySelector('[data-id="actions"] label'),t.querySelector('[data-id="actions"] button')];return[i.checked,i.id,a.htmlFor,s.dataset.id]=[e.public,`public-${e.id}`,`public-${e.id}`,e.id],t}createStyleElement(e){let t=window.getTemplate(this.row);t.id=e.id,t.querySelectorAll("td").forEach((t=>{let i=t.dataset.id,a=t.querySelector('input[type="text"]');a&&(a.value=e[i])}));let[i,a,s]=[t.querySelector('[data-id="actions"] input'),t.querySelector('[data-id="actions"] label'),t.querySelector('[data-id="actions"] button')];return[i.checked,i.id,a.htmlFor,s.dataset.id]=[e.public,`public-${e.id}`,`public-${e.id}`,e.id],t}createArtistElement(e){let t=window.getTemplate(this.row);t.id=e.id;let[i,a,s,n,o,r,l,d,c,h,u,g,v,m,b,p,y,S,w]=[t.querySelector('[data-id="actions"] input'),t.querySelector('[data-id="actions"] label'),t.querySelector('[data-id="actions"] button'),t.querySelector('[data-id="display_name"] input'),t.querySelector('[data-id="first_name"] input'),t.querySelector('[data-id="phone"] input'),t.querySelector('[data-id="email"] input'),t.querySelector('[data-id="links"] input'),t.querySelector('[data-id="admin_contact"] input'),t.querySelector('[data-id="public_contact"] input'),t.querySelector('[data-id="followers"] input'),t.querySelector('[data-id="insta_handle"] input'),t.querySelector('[data-id="type"] input'),t.querySelector('[data-id="city"] input'),t.querySelector('[data-id="shop"] input'),t.querySelector('[data-id="rate"] input'),t.querySelector('[data-id="languages"] input'),t.querySelector('[data-id="keywords"] input'),t.querySelector('[data-id="credentials"] input')];if([i.checked,i.id,a.htmlFor,s.dataset.id,n.value,o.value,r.value,l.value,c.value,h.value,v.value,m.value,b.value,g.value,p.value]=[e.public,`public-${e.id}`,`public-${e.id}`,e.id,e.display_name,e.first_name,e.phone,e.email,e.admin_contact,e.public_contact,e.type,e.city,e.shop,e.insta_handle,e.rate],e.links.length>0){let t="";e.links.forEach((e=>{t+=`[${e.url}, ${e.title}, ${e.tracker}]`})),d.value=t.trim()}if(e.followers.length>0){let t="";e.followers.forEach((e=>{t+=`[${e.count}, ${e.source}, ${formatDate(e.checked)}]`})),u.value=t}if(e.keywords.length>0){let t=[];e.keywords.forEach((e=>{t.push(e.keyword)})),S.value=t.join(", ")}if(e.languages.length>0){let t=[];e.languages.forEach((e=>{t.push(e.language)})),y.value=t.join(", ")}return t}handleError(e,t){console.error(`News error (${t}):`,e),window.jvbError&&window.jvbError.log(e,{component:"Admin",action:t}),window.jvbA11y&&window.jvbA11y.announce(`Error ${t}. ${e.message||"Please try again."}`)}}function t(e){return Array.from(e).reduce(((e,[i,a])=>(a instanceof Map&&(a=t(a)),e[i]=a,e)),{})}document.addEventListener("DOMContentLoaded",(()=>{new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/auth.min.js b/assets/js/min/auth.min.js
new file mode 100644
index 0000000..2a75dc0
--- /dev/null
+++ b/assets/js/min/auth.min.js
@@ -0,0 +1 @@
+window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey="jvb_auth_state",this.cacheMetaKey="jvb_auth_meta",this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return new Promise((t=>{const e=setInterval((()=>{this.initialized&&(clearInterval(e),t())}),50)}));this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const e=await t.json(),i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{}}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}}))}ready(){return this.initialized?Promise.resolve():new Promise((t=>{const e=this.subscribe((i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),t())}))}))}};
\ No newline at end of file
diff --git a/assets/js/min/bioManager.min.js b/assets/js/min/bioManager.min.js
index 6185178..c2ea8d2 100644
--- a/assets/js/min/bioManager.min.js
+++ b/assets/js/min/bioManager.min.js
@@ -1 +1 @@
-window.formManager=class{constructor(){this.form=document.querySelector(".replace form"),this.nav=document.querySelector(".form-sections"),this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.selectorInstances=new Map,this.highlightInstances=new Map,this.selectors={}}handleSave(e){null!==e&&(e.user=jvbSettings.currentUser,Object.hasOwn(e,"term_name")&&""===e.term_name&&(delete e.term_name,delete e.select_parent),window.jvbQueue.addToQueue({type:bioSettings.type,data:e}))}};
\ No newline at end of file
+window.formManager=class{constructor(){this.form=document.querySelector(".replace form"),this.nav=document.querySelector(".form-sections"),this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.selectorInstances=new Map,this.highlightInstances=new Map,this.selectors={}}handleSave(e){null!==e&&(e.user=window.auth.getUser(),Object.hasOwn(e,"term_name")&&""===e.term_name&&(delete e.term_name,delete e.select_parent),window.jvbQueue.addToQueue({type:bioSettings.type,data:e}))}};
\ No newline at end of file
diff --git a/assets/js/min/creator.min.js b/assets/js/min/creator.min.js
index 056e9f3..79ffc30 100644
--- a/assets/js/min/creator.min.js
+++ b/assets/js/min/creator.min.js
@@ -1 +1 @@
-window.jvbTaxCreator=class{constructor(e){this.selector=e,e.modal&&(this.createNew=e.modal.querySelector(".create-new-term"),this.toggle=e.modal.querySelector(".new-term-toggle"),this.form=this.createNew?.querySelector(".create-new-term-section")),this.initListeners(),this.form&&this.initTermCreation()}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){window.targetCheck(e,".create-new-term summary")&&(this.createNew.open&&this.createNew.querySelector('input[name="term_name"]').focus(),this.resetParentOptions()),window.targetCheck(e,".submit-term")&&this.handleTermCreation(e),window.targetCheck(e,".create-term")&&this.handleAutocompleteCreate(e)}async handleTermCreation(e){const t=this.selector.currentConfig?.taxonomy;if(!t)return;const r=this.form.querySelector('input[name="term_name"]').value.trim(),s=parseInt(this.form.querySelector("input#select_parent")?.value)||0;if(r)try{this.form.querySelector("button").disabled=!0;const e=await this.createTerm(r,s,t);if(e.success&&e.term){let o=e.term;this.createNew.open=!1,await this.selector.store.clearCache(),this.selector.store.data.set(o.id,{id:o.id,name:o.name,path:termPath,taxonomy:field.taxonomy,parent:0,count:0,hasChildren:!1,slug:o.slug||r.toLowerCase().replace(/\s+/g,"-")}),this.selector.addSelectedTermToModal(o.id,o.name,o.path||o.name),(this.selector.store.filters.parent||0)===s&&await this.selector.store.setFilters({taxonomy:t,parent:s,page:1,search:""}),this.form.querySelector('input[name="term_name"]').value="";const a=this.createNew.querySelector(".term-suggestions");a&&(a.hidden=!0),this.selector.store.cache.clear()}}catch(e){console.error("Error creating term:",e),this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleTermCreation"})}finally{this.form.querySelector("button").disabled=!1}}async handleAutocompleteCreate(e){const t=e.target.closest(".create-term"),r=this.selector.getFieldId(t),s=this.selector.fields.get(r);if(!s)return;const o=s.container.querySelector("input[data-autocomplete]"),a=o?.value.trim()||t.dataset.query;if(!a)return;const n=t.innerHTML;try{t.disabled=!0,t.textContent="Creating...";const e=await this.createTerm(a,0,s.taxonomy);if(e.success&&e.term){const t=e.term,r=t.path||t.name;s.selectedTerms.add(parseInt(t.id)),this.selector.store.data.set(t.id,{id:t.id,name:t.name,path:r,taxonomy:s.taxonomy,parent:0,count:0,hasChildren:!1,slug:t.slug||a.toLowerCase().replace(/\s+/g,"-")}),this.selector.addTermToDisplay(s.id,t.id,t.name,r),s.input.value=Array.from(s.selectedTerms).join(","),s.input.dispatchEvent(new Event("change",{bubbles:!0})),s.autocompleteDropdown.hidden=!0,o&&(o.value=""),this.selector.store.clearCache(),await this.selector.store.setFilters({taxonomy:s.taxonomy,page:1,search:"",parent:0})}else if("exists"===e.reason&&e.term){const t=e.term;s.selectedTerms.add(parseInt(t.id)),this.selector.addTermToDisplay(s.id,t.id,t.name,t.path||t.name),s.input.value=Array.from(s.selectedTerms).join(","),s.input.dispatchEvent(new Event("change",{bubbles:!0})),s.autocompleteDropdown.hidden=!0,o&&(o.value="")}}catch(e){console.error("Error creating term:",e),t.innerHTML=n,t.disabled=!1,this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleAutocompleteCreate"})}}initTermCreation(){this.form&&this.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}resetParentOptions(){const e=this.selector.currentConfig?.taxonomy;if(!e)return;let t=this.createNew.querySelector("#select_parent");if(!t)return;let r=t.querySelector("option");if(!r)return;window.removeChildren(t),t.append(r.cloneNode(!0));const s=this.selector.store.filters.parent||0;if(0!==s){const e=this.selector.store.data.get(s);if(e){let s=r.cloneNode(!0);s.value=e.id,s.textContent=e.name,t.append(s)}}const o=[];this.selector.store.data.forEach((t=>{t.taxonomy===e&&t.parent===s&&o.push(t)})),o.sort(((e,t)=>e.name.localeCompare(t.name))),o.forEach((e=>{let s=r.cloneNode(!0);s.id=`select-parent-${e.id}`,s.value=e.id,s.textContent="  — "+e.name,t.append(s)}))}async createTerm(e,t=0,r){try{await this.selector.store.setFilters({taxonomy:r,search:e,page:1,parent:0});const s=Array.from(this.selector.store.data.values()).find((t=>t.taxonomy===r&&t.name.toLowerCase()===e.toLowerCase()));if(s)return this.createNew&&this.showTermSuggestions([s],!0),{success:!1,reason:"exists",term:s};const o=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({taxonomy:r,name:e,parent:t})});if(!o.ok)throw new Error(`Server error: ${o.status}`);return await o.json()}catch(e){throw console.error("Error creating term:",e),e}}async searchExistingTerms(e,t){return new Promise((r=>{const s=(e,t)=>{"data-loaded"===e&&(this.selector.store.unsubscribe(s),r(t.data?.items||[]))};this.selector.store.subscribe(s),this.selector.store.setFilters({taxonomy:t,search:e,page:1,parent:0})}))}showTermSuggestions(e,t=!1){const r=this.createNew.querySelector(".term-suggestions")||this.createSuggestionContainer();window.removeChildren(r);const s=document.createElement("h4");s.textContent=t?"This term already exists:":"Similar terms already exist:",r.appendChild(s);const o=document.createElement("ul");o.className="term-suggestion-list",e.forEach((e=>{const t=document.createElement("li"),s=document.createElement("button");s.type="button",s.className="use-existing-term",s.setAttribute("data-id",e.id),s.textContent=e.path||e.name,s.addEventListener("click",(()=>{this.selector.addSelectedTermToModal(e.id,e.name,e.path||e.name),this.createNew.open=!1,r.hidden=!0,this.form.querySelector('input[name="term_name"]').value=""})),t.appendChild(s),o.appendChild(t)})),r.appendChild(o),r.hidden=!1}createSuggestionContainer(){const e=document.createElement("div");return e.className="term-suggestions",e.hidden=!0,this.createNew.querySelector("form").after(e),e}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler);const e=this.createNew?.querySelector(".loading-message.create-term");e&&(e.hidden=!0);const t=this.createNew?.querySelector(".term-suggestions");t&&(t.hidden=!0)}};
\ No newline at end of file
+window.jvbTaxCreator=class{constructor(e){this.selector=e,e.modal&&(this.createNew=e.modal.querySelector(".create-new-term"),this.toggle=e.modal.querySelector(".new-term-toggle"),this.form=this.createNew?.querySelector(".create-new-term-section")),this.initListeners(),this.form&&this.initTermCreation()}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){window.targetCheck(e,".create-new-term summary")&&(this.createNew.open&&this.createNew.querySelector('input[name="term_name"]').focus(),this.resetParentOptions()),window.targetCheck(e,".submit-term")&&this.handleTermCreation(e),window.targetCheck(e,".create-term")&&this.handleAutocompleteCreate(e)}async handleTermCreation(e){const t=this.selector.currentConfig?.taxonomy;if(!t)return;const r=this.form.querySelector('input[name="term_name"]').value.trim(),s=parseInt(this.form.querySelector("input#select_parent")?.value)||0;if(r)try{this.form.querySelector("button").disabled=!0;const e=await this.createTerm(r,s,t);if(e.success&&e.term){let o=e.term;this.createNew.open=!1,await this.selector.store.clearCache(),this.selector.store.data.set(o.id,{id:o.id,name:o.name,path:termPath,taxonomy:field.taxonomy,parent:0,count:0,hasChildren:!1,slug:o.slug||r.toLowerCase().replace(/\s+/g,"-")}),this.selector.addSelectedTermToModal(o.id,o.name,o.path||o.name),(this.selector.store.filters.parent||0)===s&&await this.selector.store.setFilters({taxonomy:t,parent:s,page:1,search:""}),this.form.querySelector('input[name="term_name"]').value="";const a=this.createNew.querySelector(".term-suggestions");a&&(a.hidden=!0),this.selector.store.cache.clear()}}catch(e){console.error("Error creating term:",e),this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleTermCreation"})}finally{this.form.querySelector("button").disabled=!1}}async handleAutocompleteCreate(e){const t=e.target.closest(".create-term"),r=this.selector.getFieldId(t),s=this.selector.fields.get(r);if(!s)return;const o=s.container.querySelector("input[data-autocomplete]"),a=o?.value.trim()||t.dataset.query;if(!a)return;const n=t.innerHTML;try{t.disabled=!0,t.textContent="Creating...";const e=await this.createTerm(a,0,s.taxonomy);if(e.success&&e.term){const t=e.term,r=t.path||t.name;s.selectedTerms.add(parseInt(t.id)),this.selector.store.data.set(t.id,{id:t.id,name:t.name,path:r,taxonomy:s.taxonomy,parent:0,count:0,hasChildren:!1,slug:t.slug||a.toLowerCase().replace(/\s+/g,"-")}),this.selector.addTermToDisplay(s.id,t.id,t.name,r),s.input.value=Array.from(s.selectedTerms).join(","),s.input.dispatchEvent(new Event("change",{bubbles:!0})),s.autocompleteDropdown.hidden=!0,o&&(o.value=""),this.selector.store.clearCache(),await this.selector.store.setFilters({taxonomy:s.taxonomy,page:1,search:"",parent:0})}else if("exists"===e.reason&&e.term){const t=e.term;s.selectedTerms.add(parseInt(t.id)),this.selector.addTermToDisplay(s.id,t.id,t.name,t.path||t.name),s.input.value=Array.from(s.selectedTerms).join(","),s.input.dispatchEvent(new Event("change",{bubbles:!0})),s.autocompleteDropdown.hidden=!0,o&&(o.value="")}}catch(e){console.error("Error creating term:",e),t.innerHTML=n,t.disabled=!1,this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleAutocompleteCreate"})}}initTermCreation(){this.form&&this.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}resetParentOptions(){const e=this.selector.currentConfig?.taxonomy;if(!e)return;let t=this.createNew.querySelector("#select_parent");if(!t)return;let r=t.querySelector("option");if(!r)return;window.removeChildren(t),t.append(r.cloneNode(!0));const s=this.selector.store.filters.parent||0;if(0!==s){const e=this.selector.store.data.get(s);if(e){let s=r.cloneNode(!0);s.value=e.id,s.textContent=e.name,t.append(s)}}const o=[];this.selector.store.data.forEach((t=>{t.taxonomy===e&&t.parent===s&&o.push(t)})),o.sort(((e,t)=>e.name.localeCompare(t.name))),o.forEach((e=>{let s=r.cloneNode(!0);s.id=`select-parent-${e.id}`,s.value=e.id,s.textContent="  — "+e.name,t.append(s)}))}async createTerm(e,t=0,r){try{await this.selector.store.setFilters({taxonomy:r,search:e,page:1,parent:0});const s=Array.from(this.selector.store.data.values()).find((t=>t.taxonomy===r&&t.name.toLowerCase()===e.toLowerCase()));if(s)return this.createNew&&this.showTermSuggestions([s],!0),{success:!1,reason:"exists",term:s};const o=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({taxonomy:r,name:e,parent:t})});if(!o.ok)throw new Error(`Server error: ${o.status}`);return await o.json()}catch(e){throw console.error("Error creating term:",e),e}}async searchExistingTerms(e,t){return new Promise((r=>{const s=(e,t)=>{"data-loaded"===e&&(this.selector.store.unsubscribe(s),r(t.data?.items||[]))};this.selector.store.subscribe(s),this.selector.store.setFilters({taxonomy:t,search:e,page:1,parent:0})}))}showTermSuggestions(e,t=!1){const r=this.createNew.querySelector(".term-suggestions")||this.createSuggestionContainer();window.removeChildren(r);const s=document.createElement("h4");s.textContent=t?"This term already exists:":"Similar terms already exist:",r.appendChild(s);const o=document.createElement("ul");o.className="term-suggestion-list",e.forEach((e=>{const t=document.createElement("li"),s=document.createElement("button");s.type="button",s.className="use-existing-term",s.setAttribute("data-id",e.id),s.textContent=e.path||e.name,s.addEventListener("click",(()=>{this.selector.addSelectedTermToModal(e.id,e.name,e.path||e.name),this.createNew.open=!1,r.hidden=!0,this.form.querySelector('input[name="term_name"]').value=""})),t.appendChild(s),o.appendChild(t)})),r.appendChild(o),r.hidden=!1}createSuggestionContainer(){const e=document.createElement("div");return e.className="term-suggestions",e.hidden=!0,this.createNew.querySelector("form").after(e),e}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler);const e=this.createNew?.querySelector(".loading-message.create-term");e&&(e.hidden=!0);const t=this.createNew?.querySelector(".term-suggestions");t&&(t.hidden=!0)}};
\ No newline at end of file
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index f067c97..3a56d7e 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.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:jvbSettings.dash},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,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&&(console.log("Queue Subscription in CRUD.js: ",t),"operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let 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)}window.isEmptyObject(l)||this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:jvbSettings.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.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(),window.isEmptyObject(s)||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.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions.value=""}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let 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",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e){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
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 0f078d3..b62c55f 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 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,httpHeaders:new Map,subscribers:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.config.headers={"X-WP-Nonce":jvbSettings?.nonce,...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),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),clearHttpHeaders:t=>this.clearHttpHeaders(e,t),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}normalizeForStorage(e){if(null==e)return e;if(e instanceof Set)return Array.from(e);if(e instanceof Map)return Object.fromEntries(e);if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e;if(e instanceof Date)return e;if(Array.isArray(e))return e.map((e=>this.normalizeForStorage(e)));if("object"==typeof e){const t={};for(const[s,r]of Object.entries(e))t[s]=this.normalizeForStorage(r);return t}return e}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}stripDOMReferences(e,t=new WeakSet){if(null==e)return e;const s=typeof e;if("string"===s||"number"===s||"boolean"===s)return e;if("object"===s&&t.has(e))return"[Circular]";if(e instanceof HTMLElement||e instanceof NodeList||e instanceof HTMLCollection||void 0!==e.nodeType)return null;if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return e;if(e instanceof Date)return e;if(Array.isArray(e))return t.add(e),e.map((e=>this.stripDOMReferences(e,t))).filter((e=>null!==e));if("object"===s){t.add(e);const s={};for(const[r,i]of Object.entries(e)){const e=this.stripDOMReferences(i,t);null!==e&&(s[r]=e)}return s}return e}async 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})}t.useHttpCaching&&!e.objectStoreNames.contains("headers")&&e.createObjectStore("headers",{keyPath:"key"})}loadStoreDataInBackground(e){const t=this.stores.get(e);if(!t?.db)return;const s=[this.loadStoreData(e),this.loadStoreCache(e),this.loadStoreHeaders(e)];Promise.all(s).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async loadStoreData(e){const t=this.stores.get(e);if(t?.db)return new Promise((s=>{const r=t.db.transaction([t.config.storeName],"readonly").objectStore(t.config.storeName).getAll();r.onsuccess=r=>{const i=r.target.result||[];i.forEach((e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.notify(e,"data-loaded",{count:i.length}),s(i)},r.onerror=()=>s([])}))}async loadStoreCache(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("cache"))return new Promise((e=>{const s=t.db.transaction(["cache"],"readonly").objectStore("cache").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)})),e()},s.onerror=()=>e()}))}async loadStoreHeaders(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("headers"))return new Promise((e=>{const s=t.db.transaction(["headers"],"readonly").objectStore("headers").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{t.httpHeaders.set(e.key,e)})),e()},s.onerror=()=>e()}))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers},o=t.httpHeaders.get(s);t.config.useHttpCaching&&o&&(o.etag&&(a["If-None-Match"]=o.etag),o.lastModified&&(a["If-Modified-Since"]=o.lastModified));const n=new AbortController;t.currentRequest=n;const c=await fetch(i,{method:"GET",headers:a,signal:n.signal});if(304===c.status&&r)return this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r;if(!c.ok)throw new Error(`HTTP ${c.status}: ${c.statusText}`);const d=await c.json();return t.config.useHttpCaching&&this.storeResponseHeaders(e,s,c),await this.processFetchedData(e,d,s),this.notify(e,"data-loaded",{cached:!1,items:d.items||[]}),d}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s){const r=this.stores.get(e),i=t.items||[];for(const t of i)await this.save(e,t);const a={key:s,items:i.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters}};r.cache.set(s,a),await this.saveToCache(e,s,a),r.lastResponse={has_more:t.has_more||!1,total:t.total||i.length,pages:t.pages||1}}async save(e,t){const s=this.stores.get(e);let r=this.normalizeForStorage(t);if(r.data instanceof FormData&&(r={...r,data:this.formDataToObject(r.data)}),r=this.stripDOMReferences(r),s.config.validateData){const t=this.validateSerializable(r);if(!t.valid)throw console.error(`Cannot save non-serializable data to store "${e}":`,t.error),new Error(`Non-serializable data: ${t.error}`)}const i=this.getItemKey(r,s.config.keyPath);if(s.data.set(i,t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.put(r)}return this.notify(e,"item-saved",{item:t,key:i}),i}validateSerializable(e,t="root"){if(null==e)return{valid:!0};const s=typeof e;if("string"===s||"number"===s||"boolean"===s)return{valid:!0};if("function"===s)return{valid:!1,error:`Function at ${t}`};if(e instanceof Date)return{valid:!0};if(e instanceof ArrayBuffer||ArrayBuffer.isView(e))return{valid:!0};if(e instanceof HTMLElement||e instanceof NodeList||e instanceof HTMLCollection||void 0!==e.nodeType)return{valid:!1,error:`DOM element at ${t}`};if(e instanceof FormData)return{valid:!1,error:`FormData at ${t}. Convert to object first.`};if(e instanceof Blob||e instanceof File)return{valid:!1,error:`Blob/File at ${t}. Handle file uploads separately.`};if(Array.isArray(e)){for(let s=0;s<e.length;s++){const r=this.validateSerializable(e[s],`${t}[${s}]`);if(!r.valid)return r}return{valid:!0}}if("object"===s){if(e instanceof Set)return{valid:!1,error:`Set at ${t}. Convert to Array first: Array.from(set)`};if(e instanceof Map)return{valid:!1,error:`Map at ${t}. Convert to Object first: Object.fromEntries(map)`};for(const[s,r]of Object.entries(e)){const e=this.validateSerializable(r,`${t}.${s}`);if(!e.valid)return e}return{valid:!0}}return{valid:!1,error:`Unknown type at ${t}: ${s}`}}async delete(e,t){const s=this.stores.get(e);if(s.data.delete(t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.delete(t)}this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);return r&&r.items?r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);if(t.data.clear(),t.cache.clear(),t.db){const e=t.db.transaction([t.config.storeName],"readwrite").objectStore(t.config.storeName);await e.clear()}this.notify(e,"data-cleared")}setFilter(e,t,s){const r=this.stores.get(e),i=r.filters[t];null==s||""===s?delete r.filters[t]:r.filters[t]=s,this.notify(e,"filters-changed",{filters:r.filters,changed:{key:t,oldValue:i,newValue:s}}),r.config.endpoint&&this.fetch(e)}async setFilters(e,t){const s=this.stores.get(e);Object.keys(t).some((e=>s.filters[e]!==t[e]))&&(s.filters={...s.filters,...t},this.notify(e,"filters-changed",{filters:s.filters,changed:t}),s.config.endpoint&&await this.fetch(e))}removeFilter(e,t){const s=this.stores.get(e),r=s.filters[t];void 0!==r&&(delete s.filters[t],this.notify(e,"filters-changed",{filters:s.filters,removed:{key:t,oldValue:r}}),s.config.endpoint&&this.fetch(e))}clearFilters(e){const t=this.stores.get(e),s={...t.filters};t.filters={...t.config.filters},this.notify(e,"filters-cleared",{oldFilters:s,filters:t.filters}),t.config.endpoint&&this.fetch(e)}clearCache(e){const t=this.stores.get(e);if(t.cache.clear(),t.db&&t.db.objectStoreNames.contains("cache")){t.db.transaction(["cache"],"readwrite").objectStore("cache").clear()}this.notify(e,"cache-cleared")}clearHttpHeaders(e,t=null){const s=this.stores.get(e);if(t){if(s.httpHeaders.delete(t),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").delete(t)}}else if(s.httpHeaders.clear(),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").clear()}}subscribe(e,t){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)}}))}storeResponseHeaders(e,t,s){const r=this.stores.get(e),i={key:t,etag:s.headers.get("ETag"),lastModified:s.headers.get("Last-Modified"),timestamp:Date.now()};if(r.httpHeaders.set(t,i),r.db&&r.db.objectStoreNames.contains("headers")){r.db.transaction(["headers"],"readwrite").objectStore("headers").put(i)}}async saveToCache(e,t,s){const r=this.stores.get(e);if(!r.db||!r.db.objectStoreNames.contains("cache"))return;const i=r.db.transaction(["cache"],"readwrite").objectStore("cache");await i.put(s)}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbStore=new e}))})();
\ No newline at end of file
+(()=>{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,httpHeaders:new Map,subscribers: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),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),clearHttpHeaders:t=>this.clearHttpHeaders(e,t),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}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})}t.useHttpCaching&&!e.objectStoreNames.contains("headers")&&e.createObjectStore("headers",{keyPath:"key"})}loadStoreDataInBackground(e){const t=this.stores.get(e);if(!t?.db)return;const s=[this.loadStoreData(e),this.loadStoreCache(e),this.loadStoreHeaders(e)];Promise.all(s).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async loadStoreData(e){const t=this.stores.get(e);if(t?.db)return new Promise((s=>{const r=t.db.transaction([t.config.storeName],"readonly").objectStore(t.config.storeName).getAll();r.onsuccess=r=>{const i=r.target.result||[];i.forEach((e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.notify(e,"data-loaded",{count:i.length}),s(i)},r.onerror=()=>s([])}))}async loadStoreCache(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("cache"))return new Promise((e=>{const s=t.db.transaction(["cache"],"readonly").objectStore("cache").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)})),e()},s.onerror=()=>e()}))}async loadStoreHeaders(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("headers"))return new Promise((e=>{const s=t.db.transaction(["headers"],"readonly").objectStore("headers").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{t.httpHeaders.set(e.key,e)})),e()},s.onerror=()=>e()}))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers},o=t.httpHeaders.get(s);t.config.useHttpCaching&&o&&(o.etag&&(a["If-None-Match"]=o.etag),o.lastModified&&(a["If-Modified-Since"]=o.lastModified));const n=new AbortController;t.currentRequest=n;const c=await fetch(i,{method:"GET",headers:a,signal:n.signal});if(304===c.status)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(!c.ok)throw new Error(`HTTP ${c.status}: ${c.statusText}`);const d=await c.json();return t.config.useHttpCaching&&this.storeResponseHeaders(e,s,c),await this.processFetchedData(e,d,s),this.notify(e,"data-loaded",{cached:!1,items:d.items||[]}),d}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s){const r=this.stores.get(e),i=t.items||[];if(r.db&&i.length>0){const e=r.db.transaction([r.config.storeName],"readwrite"),t=e.objectStore(r.config.storeName);for(const e of i){const s=this.processForStorage(e,r.config.validateData);if(s.valid){const i=this.getItemKey(s.data,r.config.keyPath);r.data.set(i,e),await t.put(s.data)}}await new Promise(((t,s)=>{e.oncomplete=()=>t(),e.onerror=()=>s(e.error)}))}const a={key:s,items:i.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters}};r.cache.set(s,a),await this.saveToCache(e,s,a),r.lastResponse={...t,has_more:t.has_more||!1,total:t.total||i.length,pages:t.pages||1,queue_stats:t.queue_stats||{}}}async save(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);if(s.data.set(a,t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.put(i)}return this.notify(e,"item-saved",{item:t,key:a}),a}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}`}:{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 r=Array.from(e);return this.processForStorage(r,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;null!==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;null!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:null}}async delete(e,t){const s=this.stores.get(e);if(s.data.delete(t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.delete(t)}this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);return r&&r.items?r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);if(t.data.clear(),t.cache.clear(),t.db){const e=t.db.transaction([t.config.storeName],"readwrite").objectStore(t.config.storeName);await e.clear()}this.notify(e,"data-cleared")}setFilter(e,t,s){const r=this.stores.get(e),i=r.filters[t];null==s||""===s?delete r.filters[t]:r.filters[t]=s,this.notify(e,"filters-changed",{filters:r.filters,changed:{key:t,oldValue:i,newValue:s}}),r.config.endpoint&&this.fetch(e)}async setFilters(e,t){const s=this.stores.get(e);Object.keys(t).some((e=>s.filters[e]!==t[e]))&&(s.filters={...s.filters,...t},this.notify(e,"filters-changed",{filters:s.filters,changed:t}),s.config.endpoint&&await this.fetch(e))}removeFilter(e,t){const s=this.stores.get(e),r=s.filters[t];void 0!==r&&(delete s.filters[t],this.notify(e,"filters-changed",{filters:s.filters,removed:{key:t,oldValue:r}}),s.config.endpoint&&this.fetch(e))}clearFilters(e){const t=this.stores.get(e),s={...t.filters};t.filters={...t.config.filters},this.notify(e,"filters-cleared",{oldFilters:s,filters:t.filters}),t.config.endpoint&&this.fetch(e)}clearCache(e){const t=this.stores.get(e);if(t.cache.clear(),t.db&&t.db.objectStoreNames.contains("cache")){t.db.transaction(["cache"],"readwrite").objectStore("cache").clear()}this.notify(e,"cache-cleared")}clearHttpHeaders(e,t=null){const s=this.stores.get(e);if(t){if(s.httpHeaders.delete(t),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").delete(t)}}else if(s.httpHeaders.clear(),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").clear()}}subscribe(e,t){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)}}))}storeResponseHeaders(e,t,s){const r=this.stores.get(e),i={key:t,etag:s.headers.get("ETag"),lastModified:s.headers.get("Last-Modified"),timestamp:Date.now()};if(r.httpHeaders.set(t,i),r.db&&r.db.objectStoreNames.contains("headers")){r.db.transaction(["headers"],"readwrite").objectStore("headers").put(i)}}async saveToCache(e,t,s){const r=this.stores.get(e);if(!r.db||!r.db.objectStoreNames.contains("cache"))return;const i=r.db.transaction(["cache"],"readwrite").objectStore("cache");await i.put(s)}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(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/error.min.js b/assets/js/min/error.min.js
index 1558e19..e7d3937 100644
--- a/assets/js/min/error.min.js
+++ b/assets/js/min/error.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.options={apiUrl:"",logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3,...e},this.retryCount=0}async log(e,t={},r=null){console.error("API Error:",e,t);const o=this.getErrorType(e),n=this.getErrorMessage(e,o);switch(this.options.logToServer&&await this.logErrorToServer(o,n,t),o){case"network":case"server":if(this.options.retryEnabled&&this.retryCount<this.options.maxRetries&&r)return this.retryCount++,this.retryWithBackoff(r);break;case"auth":this.handleAuthError();break;case"rate_limit":return this.handleRateLimitError(r)}return this.options.displayNotifications&&this.displayErrorNotification(n,o,r),r&&this.options.retryEnabled||(this.retryCount=0),{success:!1,error:o,message:n,context:t}}getErrorType(e){if("AbortError"===e.name)return"timeout";if(!navigator.onLine)return"offline";if(e.response){const t=e.response.status;if(t>=400&&t<500)return 401===t||403===t?"auth":429===t?"rate_limit":"client";if(t>=500)return"server"}return"network"}getErrorMessage(e,t){const r={network:"We couldn't connect to the server. Please check your connection and try again.",timeout:"The request took too long to complete. Please try again.",offline:"You appear to be offline. Please check your internet connection.",auth:"Your session may have expired. Please log in again.",rate_limit:"You've made too many requests. Please wait a moment and try again.",server:"We're experiencing technical difficulties. Please try again later.",client:"Something went wrong with your request. Please try again.",unknown:"An unexpected error occurred. Please try again."};return e.response&&e.response.data&&e.response.data.message?e.response.data.message:e.message?e.message:r[t]||r.unknown}async logErrorToServer(e,t,r){try{if(!this.options.apiUrl)return;const o=new FormData;o.append("error_type",e),o.append("message",t),o.append("context",JSON.stringify({...r,url:window.location.href,userAgent:navigator.userAgent,timestamp:(new Date).toISOString()})),await fetch(`${this.options.apiUrl}errors/log`,{method:"POST",headers:{"X-WP-Nonce":window.feedSettings?.nonce||""},body:o})}catch(e){console.warn("Failed to log error to server",e)}}displayErrorNotification(e,t,r){if(window.jvbNotifications){const t=[];return r&&t.push({label:"Try Again",icon:"refresh",action:r}),void window.jvbNotifications.queuePopupNotification({type:"error",message:e,icon:"alert",priority:"high",displayDuration:this.options.notificationDuration,actions:t})}alert(e)}handleAuthError(){window.feedSettings&&window.feedSettings.loginUrl?window.location.href=window.feedSettings.loginUrl:window.location.reload()}async handleRateLimitError(e){const t=2e3*(this.retryCount+1);if(await new Promise((e=>setTimeout(e,t))),e)return this.retryCount++,e()}async retryWithBackoff(e){const t=Math.min(1e3*Math.pow(2,this.retryCount),1e4);return this.options.displayNotifications&&this.displayRetryNotification(t),await new Promise((e=>setTimeout(e,t))),e()}displayRetryNotification(e){window.jvbNotifications&&window.jvbNotifications.queuePopupNotification({type:"info",message:`Retrying in ${e/1e3} seconds...`,icon:"refresh",priority:"medium",displayDuration:e})}resetRetryCount(){this.retryCount=0}collectUserFeedback(e){const t=document.createElement("dialog");return t.className="error-feedback-modal",t.innerHTML='\n            <h2>Help Us Improve</h2>\n            <p>We encountered an error. Would you like to tell us what happened?</p>\n            <form method="dialog" data-save="error">\n                <textarea placeholder="What were you trying to do when this error occurred?"></textarea>\n                <div class="actions">\n                    <button value="cancel">Skip</button>\n                    <button value="submit" class="primary">Send Feedback</button>\n                </div>\n            </form>\n        ',document.body.appendChild(t),new Promise((e=>{t.addEventListener("close",(()=>{const r="submit"===t.returnValue?t.querySelector("textarea").value:null;document.body.removeChild(t),e(r)})),t.showModal()}))}setupGlobalErrorHandling(){window.addEventListener("error",(e=>{this.log(e.error||new Error(e.message),{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno,type:"global_error"})})),window.addEventListener("unhandledrejection",(e=>{this.log(e.reason,{type:"unhandled_promise",message:e.reason?.message||"Unhandled promise rejection"})}))}}document.addEventListener("DOMContentLoaded",(function(){window.jvbError=new e({api:jvbSettings.api,logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3})}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){this.options={apiUrl:"",logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3,...e},this.retryCount=0}async log(e,t={},o=null){console.error("API Error:",e,t);const r=this.getErrorType(e),n=this.getErrorMessage(e,r);switch(this.options.logToServer&&await this.logErrorToServer(r,n,t),r){case"network":case"server":if(this.options.retryEnabled&&this.retryCount<this.options.maxRetries&&o)return this.retryCount++,this.retryWithBackoff(o);break;case"auth":this.handleAuthError();break;case"rate_limit":return this.handleRateLimitError(o)}return this.options.displayNotifications&&this.displayErrorNotification(n,r,o),o&&this.options.retryEnabled||(this.retryCount=0),{success:!1,error:r,message:n,context:t}}getErrorType(e){if("AbortError"===e.name)return"timeout";if(!navigator.onLine)return"offline";if(e.response){const t=e.response.status;if(t>=400&&t<500)return 401===t||403===t?"auth":429===t?"rate_limit":"client";if(t>=500)return"server"}return"network"}getErrorMessage(e,t){const o={network:"We couldn't connect to the server. Please check your connection and try again.",timeout:"The request took too long to complete. Please try again.",offline:"You appear to be offline. Please check your internet connection.",auth:"Your session may have expired. Please log in again.",rate_limit:"You've made too many requests. Please wait a moment and try again.",server:"We're experiencing technical difficulties. Please try again later.",client:"Something went wrong with your request. Please try again.",unknown:"An unexpected error occurred. Please try again."};return e.response&&e.response.data&&e.response.data.message?e.response.data.message:e.message?e.message:o[t]||o.unknown}async logErrorToServer(e,t,o){try{if(!this.options.apiUrl)return;const r={...o,url:window.location.href,pathname:window.location.pathname,userAgent:navigator.userAgent,timestamp:(new Date).toISOString(),viewport:`${window.innerWidth}x${window.innerHeight}`,component:o.component||this.extractComponentFromStack(o.stack),method:o.method||this.extractMethodFromStack(o.stack),stack:o.stack||o.error?.stack,isLoggedIn:window.auth.isAuthenticated(),source:"frontend"},n=new FormData;n.append("error_type",e),n.append("message",t),n.append("context",JSON.stringify(r)),await fetch(`${this.options.apiUrl}errors/log`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:n})}catch(e){console.warn("Failed to log error to server",e)}}extractComponentFromStack(e){if(!e)return"Unknown";const t=e.match(/at\s+(\w+)\./);return t?t[1]:"Unknown"}extractMethodFromStack(e){if(!e)return null;const t=e.match(/at\s+\w+\.(\w+)\s+/);return t?t[1]:null}displayErrorNotification(e,t,o){if(window.jvbNotifications){const t=[];return o&&t.push({label:"Try Again",icon:"refresh",action:o}),void window.jvbNotifications.queuePopupNotification({type:"error",message:e,icon:"alert",priority:"high",displayDuration:this.options.notificationDuration,actions:t})}alert(e)}handleAuthError(){window.jvbSettings&&window.jvbSettings.loginUrl?window.location.href=window.jvbSettings.loginUrl:window.location.reload()}async handleRateLimitError(e){const t=2e3*(this.retryCount+1);if(await new Promise((e=>setTimeout(e,t))),e)return this.retryCount++,e()}async retryWithBackoff(e){const t=Math.min(1e3*Math.pow(2,this.retryCount),1e4);return this.options.displayNotifications&&this.displayRetryNotification(t),await new Promise((e=>setTimeout(e,t))),e()}displayRetryNotification(e){window.jvbNotifications&&window.jvbNotifications.queuePopupNotification({type:"info",message:`Retrying in ${e/1e3} seconds...`,icon:"refresh",priority:"medium",displayDuration:e})}resetRetryCount(){this.retryCount=0}collectUserFeedback(e){const t=document.createElement("dialog");return t.className="error-feedback-modal",t.innerHTML='\n            <h2>Help Us Improve</h2>\n            <p>We encountered an error. Would you like to tell us what happened?</p>\n            <form method="dialog" data-save="error">\n                <textarea placeholder="What were you trying to do when this error occurred?"></textarea>\n                <div class="actions">\n                    <button value="cancel">Skip</button>\n                    <button value="submit" class="primary">Send Feedback</button>\n                </div>\n            </form>\n        ',document.body.appendChild(t),new Promise((e=>{t.addEventListener("close",(()=>{const o="submit"===t.returnValue?t.querySelector("textarea").value:null;document.body.removeChild(t),e(o)})),t.showModal()}))}setupGlobalErrorHandling(){window.addEventListener("error",(e=>{this.log(e.error||new Error(e.message),{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno,type:"global_error"})})),window.addEventListener("unhandledrejection",(e=>{this.log(e.reason,{type:"unhandled_promise",message:e.reason?.message||"Unhandled promise rejection"})}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbError=new e({api:jvbSettings.api,logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3}))}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/favouritesManager.min.js b/assets/js/min/favouritesManager.min.js
index e52cc4e..8a2a0fe 100644
--- a/assets/js/min/favouritesManager.min.js
+++ b/assets/js/min/favouritesManager.min.js
@@ -1 +1 @@
-window.favouritesManager=class{constructor(){this.queue=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.config={endpoints:{favourites:"favourites",lists:"favourites/lists",shares:"favourites/lists/shares"},selectors:{container:".favourites.container",itemsTab:'.tab-content[data-tab="items"]',listsTab:'.tab-content[data-tab="lists"]',grid:".item-grid",typeFilters:".type-filters",viewControls:".view-controls",bulkControls:".bulk-controls",selectAll:"#select-all",createListModal:".create-list-modal",addToListModal:".add-to-list-modal",shareListModal:".share-list-modal",noItems:".no-favourites",listContainer:".lists-container",listDetails:".list-details",loader:".favourites-loader"},defaultPage:1,defaultPerPage:24,defaultViewMode:"grid",refreshInterval:6e4,toastDuration:3e3},document.addEventListener("keydown",this.handleKeyDown.bind(this)),this.state={selectedItems:new Set,page:this.config.defaultPage,filter:{type:"all",order:"desc",orderBy:"date_added"},view:{mode:localStorage.getItem("favourites_view")||this.config.defaultViewMode,activeTab:"items"},pagination:{hasMore:!1,totalItems:0,totalPages:0},currentListId:null,loading:!1,initialized:!1},this.initDom(),this.initEvents(),this.loadInitialData(),this.state.initialized=!0}initDom(){this.container=document.querySelector(this.config.selectors.container),this.container?(this.grid=this.container.querySelector(this.config.selectors.grid),this.typeFilters=this.container.querySelector(this.config.selectors.typeFilters),this.viewControls=this.container.querySelector(this.config.selectors.viewControls),this.bulkControls=this.container.querySelector(this.config.selectors.bulkControls),this.listContainer=this.container.querySelector(this.config.selectors.listContainer),this.listDetails=this.container.querySelector(this.config.selectors.listDetails),this.loader=this.container.querySelector(this.config.selectors.loader),this.createListModal=document.querySelector(this.config.selectors.createListModal),this.addToListModal=document.querySelector(this.config.selectors.addToListModal),this.shareListModal=document.querySelector(this.config.selectors.shareListModal),this.grid&&this.state.view.mode&&this.grid.classList.add(`${this.state.view.mode}-view`)):console.warn("Favourites container not found")}initEvents(){if(this.typeFilters&&this.typeFilters.addEventListener("click",(t=>{const e=t.target.closest(".type-filter");e&&this.setFilterType(e.dataset.type)})),this.viewControls&&this.viewControls.addEventListener("click",(t=>{const e=t.target.closest(".view-toggle");e&&this.setView(e.dataset.view)})),this.container){const t=this.container.querySelector(this.config.selectors.selectAll);t&&t.addEventListener("change",(()=>{this.toggleSelectAll(t.checked)})),this.container.addEventListener("change",(t=>{t.target.matches(".item-select input[type=checkbox]")&&this.handleItemSelection(t.target)}));const e=this.container.querySelector(".bulk-action-select"),s=this.container.querySelector(".apply-bulk");e&&s&&s.addEventListener("click",(()=>{this.applyBulkAction(e.value)}));const i=this.container.querySelector(".cancel-bulk");i&&i.addEventListener("click",(()=>{this.clearSelection()}))}this.initModalEvents(),this.container.addEventListener("click",this.handleItemActions.bind(this)),this.grid&&this.setupInfiniteScroll()}initModalEvents(){if(this.createListModal){const t=this.createListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleCreateList(new FormData(t))}));const e=this.createListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.createListModal.close()}))}if(this.addToListModal){const t=this.addToListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleAddToList(new FormData(t))}));const e=this.addToListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.addToListModal.close()}))}if(this.shareListModal){const t=this.shareListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleShareList(new FormData(t))}));const e=this.shareListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.shareListModal.close()}));const s=this.shareListModal.querySelector(".add-email");s&&s.addEventListener("click",(()=>{const e=this.shareListModal.querySelector("#share-email");e&&e.value&&this.handleShareList(new FormData(t))}))}}setupInfiniteScroll(){let t=this.container.querySelector(".scroll-sentinel");t||(t=document.createElement("div"),t.className="scroll-sentinel",t.setAttribute("aria-hidden","true"),this.grid.parentNode.appendChild(t)),new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&this.state.pagination.hasMore&&!this.state.loading&&(this.state.page++,this.loadFavourites())}))}),{rootMargin:"200px"}).observe(t)}async loadInitialData(){this.loadingManager.show();try{await this.loadFavourites(),this.loadLists().catch((t=>{console.error("Error loading lists:",t)}))}catch(t){this.handleError(t,"loading initial data")}finally{this.loadingManager.hide()}}async loadFavourites(t=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const e=new URLSearchParams({page:this.state.page,per_page:this.config.defaultPerPage,type:"all"!==this.state.filter.type?this.state.filter.type:"",order:this.state.filter.order,orderby:this.state.filter.orderBy});t&&(this.state.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.favourites}?${e}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favouritesManager",forceRefresh:!0});return this.renderFavourites(s.favourites||[],this.state.page>1),s.counts&&this.updateTypeFilters(s.counts),s.pagination&&(this.state.pagination={hasMore:s.pagination.has_more,totalItems:s.pagination.total_items,totalPages:s.pagination.total_pages}),s}catch(t){throw this.handleError(t,"loading favourites"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderFavourites(t,e=!1){this.grid&&(0!==t.length||e?(this.hideEmptyState(),e||removeChildren(this.grid),t.forEach((t=>{const e=this.createItemElement(t);this.grid.appendChild(e),this.initItemFunctionality(e,t)})),window.jvbA11y&&window.jvbA11y.announce(`${e?"Added":"Loaded"} ${t.length} favourites`)):this.showEmptyState())}createItemElement(t){const e=document.createElement("div");e.className=`item ${t.type} favourited`,e.dataset.id=t.target_id,e.dataset.type=t.type;const s=sanitizeHtml(t.title||!1),i=sanitizeHtml(t.notes||"");let a="";return t.thumbnail&&(a=`\n                <div class="item-thumbnail">\n                    <a href="${t.url}">${t.thumbnail}</a>\n                </div>\n            `),e.innerHTML=`\n            <div class="item-select">\n                <input type="checkbox"\n                   class="favourite-checkbox"\n                    id="select-${t.target_id}"\n                    value="${t.target_id}">\n                <label for="select-${t.target_id}"><span class="screen-reader-text">Select this ${t.type}</span</label>\n            </div>\n\n            <button type="button" class="favourite-button favourited"\n                onclick="toggleFavourite(this)"\n                data-id="${t.target_id}"\n                data-type="${t.type}"\n                title="Remove from favourites">\n                ${jvbSettings.icons["heart-filled"]}\n            </button>\n\n            ${a}\n\n            <div class="item-info">\n                ${s?`<h3><a href="${t.url}">${s}</a></h3>`:`<a href="${t.url}">View Item</a>`}\n\n                ${t.author?`\n                <div class="item-artist">\n                    <span>By ${t.author.name}</span>\n                </div>`:""}\n\n                ${t.taxonomies?.length?`\n                <div class="taxonomy-lists">\n                    ${t.taxonomies.map((t=>`\n                        <div class="taxonomy-group">\n                            ${jvbSettings.icons[t.icon]}\n                            <ul>\n                                ${t.terms.slice(0,3).map((t=>`\n                                    <li>\n                                        <a href="${t.url}" ${t.umami_click}>\n                                            ${t.title}\n                                        </a>\n                                    </li>\n                                `)).join("")}\n                            </ul>\n                        </div>\n                    `)).join("")}\n                </div>\n            `:""}\n\n                <div class="notes-section">\n                    <button type="button" class="toggle-notes" aria-expanded="false">\n                        ${jvbSettings.icons.note||"Notes"}\n                        <span>Notes</span>\n                    </button>\n\n                    <div class="notes-content" hidden>\n                        <textarea class="notes-input"\n                            placeholder="Add notes about this item..."\n                            data-id="${t.target_id}"\n                            data-type="${t.type}">${i}</textarea>\n                        <button type="button" class="save-notes">Save Notes</button>\n                    </div>\n                </div>\n            </div>\n        `,e}initItemFunctionality(t,e){const s=t.querySelector(".toggle-notes"),i=t.querySelector(".notes-content");s&&i&&s.addEventListener("click",(()=>{const t="true"===s.ariaExpanded;s.ariaExpanded=!t.toString(),i.hidden=t,t||i.querySelector("textarea")?.focus()}));const a=t.querySelector(".save-notes"),n=t.querySelector(".notes-input");a&&n&&(a.addEventListener("click",(()=>{this.saveNotes(n)})),n.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.saveNotes(n))})))}saveNotes(t){if(!t)return;const e=t.value.trim(),s=t.dataset.id,i=t.dataset.type;s&&i&&(this.queue.addToQueue({type:"favourite_notes",data:{type:i,target_id:parseInt(s),notes:e}}),showToast("Notes saved"),this.a11y.announce("Notes saved"))}showEmptyState(t=!1){const e=this.container.querySelector(this.config.selectors.noItems)??this.createEmptyElement;e&&(e.hidden=!1),this.grid&&this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){const t=this.container.querySelector(".no-favourites");t&&t.remove(),this.grid&&this.grid.classList.remove("empty")}createEmptyElement(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ BLANK CANVAS ♡</h3>\n            <p>You haven't fallen in love with any pieces... yet!</p>\n            <p>Hit that heart icon when something stops your scroll.</p>\n            <p>Your dream collection is waiting to start.</p>\n        ",this.grid.after(e)}showEmptyListState(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ FULL OF POSSIBILITY ♡</h3>\n            <p>There's nothing in this list yet.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.after(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}async loadMoreItems(){!this.state.loading&&this.state.pagination.hasMore&&(this.state.page+=1,await this.loadFavourites())}updateTypeFilters(t){this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{const s=e.querySelector(".count");if(!s)return;const i=e.dataset.type;if("all"===i){const e=Object.values(t).reduce(((t,e)=>t+(parseInt(e)||0)),0);s.textContent=`(${e})`}else s.textContent=`(${t[i]||0})`}))}setFilterType(t){t!==this.state.filter.type&&(this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{e.classList.toggle("active",e.dataset.type===t),e.setAttribute("aria-selected",e.dataset.type===t)})),this.state.filter.type=t,this.state.page=1,this.loadFavourites(),window.jvbA11y&&window.jvbA11y.announce(`Filtered to show ${"all"===t?"all":t} items`))}setView(t){t!==this.state.view.mode&&(this.viewControls&&this.viewControls.querySelectorAll(".view-toggle").forEach((e=>{const s=e.dataset.view===t;e.setAttribute("aria-pressed",s)})),this.grid&&(this.grid.classList.remove("grid-view","list-view"),this.grid.classList.add(`${t}-view`)),this.state.view.mode=t,localStorage.setItem("favourites_view",t),window.jvbA11y&&window.jvbA11y.announce(`Changed to ${t} view`))}toggleSelectAll(t){const e=this.getVisibleItems();e.forEach((e=>{const s=e.querySelector('.item-select input[type="checkbox"]');s&&(s.checked=t,this.toggleItemSelection(s.value,t))})),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce(t?`Selected all ${e.length} items`:"Deselected all items")}getVisibleItems(){return this.grid?Array.from(this.grid.querySelectorAll(".item:not([hidden])")):[]}toggleItemSelection(t,e){e?this.state.selectedItems.add(t):this.state.selectedItems.delete(t);const s=this.grid.querySelector(`.item[data-id="${t}"]`);s&&s.classList.toggle("selected",e)}handleItemSelection(t){const e=t.checked,s=t.value;if(this.toggleItemSelection(s,e),this.updateBulkControls(),this.updateSelectAllState(),window.jvbA11y){const s=t.closest(".item"),i=s&&s.querySelector("h3")?.textContent||"item";window.jvbA11y.announce(e?`Selected ${i}`:`Deselected ${i}`)}}updateSelectAllState(){const t=this.container.querySelector(this.config.selectors.selectAll);if(!t)return;const e=this.getVisibleItems();if(0===e.length)return t.checked=!1,void(t.indeterminate=!1);const s=e.filter((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');return e&&e.checked})).length;0===s?(t.checked=!1,t.indeterminate=!1):s===e.length?(t.checked=!0,t.indeterminate=!1):(t.checked=!1,t.indeterminate=!0)}updateBulkControls(){if(!this.bulkControls)return;const t=this.bulkControls.querySelector(".bulk-actions");if(!t)return;const e=this.state.selectedItems.size>0;t.hidden=!e;const s=this.bulkControls.querySelector(".selected-count");s&&(s.textContent=e?`${this.state.selectedItems.size} selected`:"")}handleKeyDown(t){"Escape"===t.key&&this.state.selectedItems.size>0&&(t.preventDefault(),this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared using Escape key"))}clearSelection(){this.state.selectedItems.clear(),this.getVisibleItems().forEach((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');e&&(e.checked=!1),t.classList.remove("selected")}));const t=this.container.querySelector(this.config.selectors.selectAll);t&&(t.checked=!1,t.indeterminate=!1),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")}applyBulkAction(t){if(!t||0===this.state.selectedItems.size)return;switch(t){case"unfavourite":this.bulkUnfavourite();break;case"add-to-list":this.showAddToListModal();break;case"create-list":this.showCreateListModal();break;case"add-notes":this.showBulkNotesModal()}const e=this.container.querySelector(".bulk-action-select");e&&(e.value="")}handleItemActions(t){if(t.target.closest(".toggle-notes")){const e=t.target.closest(".toggle-notes"),s="true"===e.getAttribute("aria-expanded"),i=e.closest(".notes-section").querySelector(".notes-content");return e.setAttribute("aria-expanded",!s),i.hidden=s,!s&&i&&i.querySelector("textarea")?.focus(),void t.preventDefault()}if(t.target.closest(".save-notes")){const e=t.target.closest(".save-notes").closest(".notes-content").querySelector("textarea");return e&&this.saveNotes(e),void t.preventDefault()}if(t.target.closest(".view-list")){const e=t.target.closest(".view-list").closest(".list-card");return e&&e.dataset.id&&this.viewList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".share-list")){const e=t.target.closest(".share-list").closest(".list-card");return e&&e.dataset.id&&this.showShareModal(e.dataset.id),void t.preventDefault()}if(t.target.closest(".delete-list")){const e=t.target.closest(".delete-list").closest(".list-card");return e&&e.dataset.id&&this.deleteList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".back-to-lists"))return this.exitListView(),void t.preventDefault()}async loadLists(t=!0){try{this.state.loading=!0,this.loadingManager.show("Loading lists...");const t=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favourite-lists",forceRefresh:!1});return t.lists&&this.renderLists(t.lists),t}catch(t){throw this.handleError(t,"loading lists"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderLists(t){if(!this.listContainer)return;if(removeChildren(this.listContainer),!t||0===t.length)return void(this.listContainer.innerHTML='\n                <div class="no-lists">\n                    <h3>No Lists Yet</h3>\n                    <p>Select favourites from the main tab to organize into lists.</p>\n                </div>\n            ');const e=t.owned,s=t.shared;if(e.length>0){const t=document.createElement("details");t.className="lists-section owned-lists",t.open=!0,t.innerHTML="<summary>Your Lists:</summary>",e.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}if(s.length>0){const t=document.createElement("details");t.className="lists-section shared-lists",t.innerHTML="<summary>Lists Shared with You:</summary>",s.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}}createListCard(t){const e=document.createElement("div");e.className="list-card",e.dataset.id=t.id;const s="1"===t.is_shared;s&&e.classList.add("shared"),t.is_temp&&e.classList.add("temp"),t.is_owner;const i=sanitizeHtml(t.name||"Untitled List"),a=sanitizeHtml(t.description||"");return e.innerHTML=`\n            <div class="list-header">\n                <h3>${i}</h3>\n                <div class="list-actions">\n                    <button type="button" class="view-list" title="View List">\n                        ${jvbSettings.icons?.show||"View"}\n                    </button>\n                    ${s?"":`\n                        <button type="button" class="share-list" title="Share List">\n                            ${jvbSettings.icons?.share||"Share"}\n                        </button>\n                        <button type="button" class="delete-list" title="Delete List">\n                            ${jvbSettings.icons?.delete||"Delete"}\n                        </button>\n                    `}\n                </div>\n            </div>\n\n            ${a?`<p class="list-description">${a}</p>`:""}\n\n            <div class="list-meta">\n                <div class="meta-stats">\n                    <span class="item-count">${t.item_count||0} items</span>\n                    <span class="date">${formatDate(t.created_at)}</span>\n                </div>\n\n\n                ${s?`\n                    <div class="owner-info">\n                        Shared by ${t.owner_name||"another user"}\n                    </div>\n                `:t.share_count>0?`\n                    <div class="share-info">\n                        Shared with ${t.share_count} ${1===t.share_count?"person":"people"}\n                    </div>\n                `:""}\n            </div>\n        `,e}async viewList(t){try{this.state.loading=!0,this.loadingManager.show("Loading list..."),this.state.currentListId=t;const e=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});if(!e.list)throw new Error("List not found");this.showListDetails(e.list)}catch(t){this.handleError(t,"viewing list")}finally{this.state.loading=!1,this.loadingManager.hide()}}showListDetails(t){this.listDetails&&this.listContainer&&(console.log(t),this.listDetails.querySelector(".list-title").value=t.name||"Untitled List",this.listDetails.querySelector(".list-description").value=t.description||"",t.is_owner?this.listDetails.querySelector(".list-actions")||this.createListActions():this.listDetails.querySelector(".list-actions")?.remove(),removeChildren(this.grid),this.renderFavourites(t.items||[],!1),0===t.items.length&&this.showEmptyListState(),window.jvbA11y&&window.jvbA11y.announce(`Viewing list: ${t.name} with ${t.items?.length||0} items`))}createListActions(){const t=document.createElement("div");t.className="list-actions",t.innerHTML='\n            <button type="button" class="share-list" title="Share List">\n            <i class="icon icon-share-fat"></i>\n            <span>Share</span>\n        </button>\n        <button type="button" class="duplicate-list" title="Duplicate List">\n            <i class="icon icon-copy"></i>\n            <span>Duplicate</span>\n        </button>\n        <button type="button" class="delete-list" title="Delete List">\n            <i class="icon icon-trash"></i>\n            <span>Delete</span>\n        </button>\n        ',this.listDetails.insertBefore(t,this.listDetails.querySelector(".bulk-controls"))}exitListView(){this.listDetails&&this.listContainer&&(this.listDetails.hidden=!0,this.listContainer.hidden=!1,this.container.classList.remove("viewing-list"),this.state.currentListId=null,window.jvbA11y&&window.jvbA11y.announce("Returned to lists view"))}showCreateListModal(){this.createListModal&&(this.createListModal.querySelector("form")?.reset(),this.createListModal.showModal(),setTimeout((()=>{this.createListModal.querySelector("#list-name")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Create list dialog opened"))}showAddToListModal(){this.addToListModal&&(this.populateAddToListModal(),this.addToListModal.showModal(),window.jvbA11y&&window.jvbA11y.announce("Add to list dialog opened"))}async populateAddToListModal(){if(!this.addToListModal)return;const t=this.addToListModal.querySelector(".lists-options");if(t){removeChildren(t);try{const e=(await this.loadLists()).lists.owned;if(0===e.length)return t.innerHTML='\n                    <div class="no-lists">\n                        <p>You don\'t have any lists yet.</p>\n                        <button type="button" class="create-list-button">Create a list</button>\n                    </div>\n                ',void t.querySelector(".create-list-button")?.addEventListener("click",(()=>{this.addToListModal.close(),this.showCreateListModal()}));e.forEach((e=>{const s=document.createElement("div");s.className="list-option",s.innerHTML=`\n                    <input type="checkbox" id="${e.id}" name="list_ids[]" value="${e.id}">\n                    <label for="${e.id}">\n\n                        <span class="list-name">${sanitizeHtml(e.name)}</span>\n                        <span class="item-count">( ${e.item_count||0} items )</span>\n                    </label>\n                `,t.appendChild(s)}))}catch(e){t.innerHTML='\n                <div class="error-message">\n                    <p>Error loading lists. Please try again.</p>\n                </div>\n            ',console.error("Error loading lists for modal:",e)}}}async handleCreateList(t){const e=t.get("list_name"),s=t.get("list_description");if(e)try{this.showLoader("Creating list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_create",data:{name:e,description:s,items:t}}),showToast(`List "${e}" created`),this.a11y.announce(`List ${e} created with ${t.length} items`),this.createListModal.close(),this.clearSelection(),this.switchTab("lists")}catch(t){this.handleError(t,"creating list")}finally{this.hideLoader()}else showToast("Please enter a list name","error")}async handleAddToList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Adding to list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_add",data:{list_id:e.join(","),items:t}}),showToast(`Added to ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Added ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"adding to list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}async handleRemoveFromList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Removing from list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_remove",data:{list_id:e.join(","),items:t}}),showToast(`Removed from ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Removed ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"remove from list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}showShareModal(t){this.shareListModal&&(this.state.currentListId=t,this.shareListModal.querySelector("form")?.reset(),this.loadSharedUsers(t),this.shareListModal.showModal(),setTimeout((()=>{this.shareListModal.querySelector("#share-email")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Share list dialog opened"))}async loadSharedUsers(t){try{const e=this.shareListModal.querySelector(".shared-users");if(!e)return;e.innerHTML='<div class="loading">Loading shared users...</div>';const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});removeChildren(e),s.list&&s.list.shared_users&&s.list.shared_users.length>0?(s.list.shared_users.forEach((t=>{const s=document.createElement("div");s.className=`shared-user ${t.status}`,s.innerHTML=`\n                        <span class="user-email">${t.email}</span>\n                        ${"pending"===t.status?'<span class="pending-badge">Invitation sent</span>':`<span class="permission-badge">${t.permission_type||"view"}</span>`}\n                        <button type="button" class="remove-share" data-email="${t.email}">\n                            ${jvbSettings.icons?.delete||"Remove"}\n                        </button>\n                    `,e.appendChild(s)})),e.querySelectorAll(".remove-share").forEach((t=>{t.addEventListener("click",(()=>{this.unshareList(t.dataset.email)}))}))):e.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>'}catch(t){console.error("Error loading shared users:",t)}}async unshareList(t){if(confirm(`Remove ${t}'s access to this list?`))if(this.state.currentListId)try{this.showLoader("Removing access..."),this.queue.addToQueue({type:"favourite_list_unshare",data:{list_id:parseInt(this.state.currentListId),email:t}});const e=Array.from(this.shareListModal.querySelectorAll(".shared-user")).find((e=>e.querySelector(".user-email")?.textContent===t));e&&(e.classList.add("removing"),setTimeout((()=>{if(e.remove(),0===this.shareListModal.querySelectorAll(".shared-user").length){const t=this.shareListModal.querySelector(".shared-users");t&&(t.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>')}}),300)),showToast(`Removed ${t}'s access`),this.a11y.announce(`Removed ${t}'s access to list`)}catch(t){this.handleError(t,"removing share access")}finally{this.hideLoader()}else showToast("No list selected","error")}async deleteList(t){if(confirm("Are you sure you want to delete this list? This cannot be undone."))try{this.showLoader("Deleting list..."),this.queue.addToQueue({type:"favourite_list_delete",data:{list_id:parseInt(t)}});const e=this.container.querySelector(`.list-card[data-id="${t}"]`);e&&(e.classList.add("removing"),setTimeout((()=>{e.remove(),0===this.container.querySelectorAll(".list-card").length&&(this.listContainer.innerHTML='\n                                <div class="no-lists">\n                                    <h3>No Lists Yet</h3>\n                                    <p>Create your first list to organize your favourites!</p>\n                                </div>\n                            ')}),300)),showToast("List deleted"),this.a11y.announce("List deleted")}catch(t){this.handleError(t,"deleting list")}finally{this.hideLoader()}}showBulkNotesModal(){let t=document.querySelector(".bulk-notes-modal");t||(t=document.createElement("dialog"),t.className="bulk-notes-modal",t.innerHTML='\n                <form method="dialog" data-save="favourites">\n                    <h2>Add Notes to Selected Items</h2>\n\n                    <div class="field">\n                        <label for="bulk-notes">Notes (will be applied to all selected items)</label>\n                        <textarea id="bulk-notes" name="bulk_notes" rows="5"></textarea>\n                    </div>\n\n                    <div class="actions">\n                        <button type="button" class="cancel">Cancel</button>\n                        <button type="submit" class="save">Save Notes</button>\n                    </div>\n                </form>\n            ',document.body.appendChild(t),t.querySelector("form").addEventListener("submit",(e=>{e.preventDefault();const s=t.querySelector("#bulk-notes").value;this.saveBulkNotes(s),t.close()})),t.querySelector(".cancel").addEventListener("click",(()=>{t.close()}))),t.querySelector("form")?.reset(),t.showModal(),setTimeout((()=>{t.querySelector("#bulk-notes")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Add notes dialog opened")}saveBulkNotes(t){if(t)try{this.showLoader("Saving notes...");let e=Array.from(this.state.selectedItems.values());this.queue.addToQueue({type:"favourite_notes",data:{target_id:e.join(","),notes:t}}),showToast(`Notes saved for ${e.length} items`),this.a11y.announce(`Notes saved for ${e.length} items`),this.clearSelection()}catch(t){this.handleError(t,"saving bulk notes")}finally{this.hideLoader()}}async bulkUnfavourite(){if(confirm("Are you sure you want to remove these items from your favourites?"))try{this.showLoader("Removing from favourites...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);if(!s)return;const i=s.dataset.type;t.push({target_id:parseInt(e),type:i,action:"remove"})})),this.queue.addToQueue({type:"favourite_toggle",data:t});const e=[];this.state.selectedItems.forEach((t=>{const s=this.grid.querySelector(`.item[data-id="${t}"]`);if(!s)return;s.style.opacity="0",s.style.transform="scale(0.9)",s.style.transition="opacity 0.3s ease, transform 0.3s ease";const i=new Promise((t=>{setTimeout((()=>{s.remove(),t()}),300)}));e.push(i)})),await Promise.all(e),0===this.grid.children.length&&this.showEmptyState(),this.clearSelection(),showToast(`Removed ${t.length} items from favourites`),this.a11y.announce(`Removed ${t.length} items from favourites`)}catch(t){this.handleError(t,"removing favourites")}finally{this.hideLoader()}}async handleShareList(t){const e=t.get("share_email");if(e)if(this.validateEmail(e))try{this.showLoader("Sharing list..."),this.queue.addToQueue({type:"favourite_list_share",data:{list_id:parseInt(this.state.currentListId),email:e,permission_type:"view"}}),this.shareListModal.querySelector("#share-email").value="",this.loadSharedUsers(this.state.currentListId),showToast(`Invitation sent to ${e}`),this.a11y.announce(`Invitation sent to ${e}`)}catch(t){this.handleError(t,"sharing list")}finally{this.hideLoader()}else showToast("Please enter a valid email address","error");else showToast("Please enter an email address","error")}showLoader(t="Loading..."){if(!this.loader)return;const e=this.loader.querySelector(".loader-message");e&&(e.textContent=t),this.loader.hidden=!1}hideLoader(){this.loader&&(this.loader.hidden=!0)}showToast(t,e){window.jvbNotifications.showToast(t,e)}handleError(t,e){console.error(`Favourites error (${e}):`,t),showToast(`Error ${e}: ${t.message||"Something went wrong"}`,"error"),window.jvbError&&window.jvbError.log(t,{component:"FavouritesManager",action:e}),window.jvbA11y&&window.jvbA11y.announce(`Error ${e}. ${t.message||"Please try again."}`)}validateEmail(t){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)}};
\ No newline at end of file
+window.favouritesManager=class{constructor(){this.queue=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.config={endpoints:{favourites:"favourites",lists:"favourites/lists",shares:"favourites/lists/shares"},selectors:{container:".favourites.container",itemsTab:'.tab-content[data-tab="items"]',listsTab:'.tab-content[data-tab="lists"]',grid:".item-grid",typeFilters:".type-filters",viewControls:".view-controls",bulkControls:".bulk-controls",selectAll:"#select-all",createListModal:".create-list-modal",addToListModal:".add-to-list-modal",shareListModal:".share-list-modal",noItems:".no-favourites",listContainer:".lists-container",listDetails:".list-details",loader:".favourites-loader"},defaultPage:1,defaultPerPage:24,defaultViewMode:"grid",refreshInterval:6e4,toastDuration:3e3},document.addEventListener("keydown",this.handleKeyDown.bind(this)),this.state={selectedItems:new Set,page:this.config.defaultPage,filter:{type:"all",order:"desc",orderBy:"date_added"},view:{mode:localStorage.getItem("favourites_view")||this.config.defaultViewMode,activeTab:"items"},pagination:{hasMore:!1,totalItems:0,totalPages:0},currentListId:null,loading:!1,initialized:!1},this.initDom(),this.initEvents(),this.loadInitialData(),this.state.initialized=!0}initDom(){this.container=document.querySelector(this.config.selectors.container),this.container?(this.grid=this.container.querySelector(this.config.selectors.grid),this.typeFilters=this.container.querySelector(this.config.selectors.typeFilters),this.viewControls=this.container.querySelector(this.config.selectors.viewControls),this.bulkControls=this.container.querySelector(this.config.selectors.bulkControls),this.listContainer=this.container.querySelector(this.config.selectors.listContainer),this.listDetails=this.container.querySelector(this.config.selectors.listDetails),this.loader=this.container.querySelector(this.config.selectors.loader),this.createListModal=document.querySelector(this.config.selectors.createListModal),this.addToListModal=document.querySelector(this.config.selectors.addToListModal),this.shareListModal=document.querySelector(this.config.selectors.shareListModal),this.grid&&this.state.view.mode&&this.grid.classList.add(`${this.state.view.mode}-view`)):console.warn("Favourites container not found")}initEvents(){if(this.typeFilters&&this.typeFilters.addEventListener("click",(t=>{const e=t.target.closest(".type-filter");e&&this.setFilterType(e.dataset.type)})),this.viewControls&&this.viewControls.addEventListener("click",(t=>{const e=t.target.closest(".view-toggle");e&&this.setView(e.dataset.view)})),this.container){const t=this.container.querySelector(this.config.selectors.selectAll);t&&t.addEventListener("change",(()=>{this.toggleSelectAll(t.checked)})),this.container.addEventListener("change",(t=>{t.target.matches(".item-select input[type=checkbox]")&&this.handleItemSelection(t.target)}));const e=this.container.querySelector(".bulk-action-select"),s=this.container.querySelector(".apply-bulk");e&&s&&s.addEventListener("click",(()=>{this.applyBulkAction(e.value)}));const i=this.container.querySelector(".cancel-bulk");i&&i.addEventListener("click",(()=>{this.clearSelection()}))}this.initModalEvents(),this.container.addEventListener("click",this.handleItemActions.bind(this)),this.grid&&this.setupInfiniteScroll()}initModalEvents(){if(this.createListModal){const t=this.createListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleCreateList(new FormData(t))}));const e=this.createListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.createListModal.close()}))}if(this.addToListModal){const t=this.addToListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleAddToList(new FormData(t))}));const e=this.addToListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.addToListModal.close()}))}if(this.shareListModal){const t=this.shareListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleShareList(new FormData(t))}));const e=this.shareListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.shareListModal.close()}));const s=this.shareListModal.querySelector(".add-email");s&&s.addEventListener("click",(()=>{const e=this.shareListModal.querySelector("#share-email");e&&e.value&&this.handleShareList(new FormData(t))}))}}setupInfiniteScroll(){let t=this.container.querySelector(".scroll-sentinel");t||(t=document.createElement("div"),t.className="scroll-sentinel",t.setAttribute("aria-hidden","true"),this.grid.parentNode.appendChild(t)),new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&this.state.pagination.hasMore&&!this.state.loading&&(this.state.page++,this.loadFavourites())}))}),{rootMargin:"200px"}).observe(t)}async loadInitialData(){this.loadingManager.show();try{await this.loadFavourites(),this.loadLists().catch((t=>{console.error("Error loading lists:",t)}))}catch(t){this.handleError(t,"loading initial data")}finally{this.loadingManager.hide()}}async loadFavourites(t=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const e=new URLSearchParams({page:this.state.page,per_page:this.config.defaultPerPage,type:"all"!==this.state.filter.type?this.state.filter.type:"",order:this.state.filter.order,orderby:this.state.filter.orderBy});t&&(this.state.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.favourites}?${e}`,{method:"GET",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("favourites")}},{context:"favouritesManager",forceRefresh:!0});return this.renderFavourites(s.favourites||[],this.state.page>1),s.counts&&this.updateTypeFilters(s.counts),s.pagination&&(this.state.pagination={hasMore:s.pagination.has_more,totalItems:s.pagination.total_items,totalPages:s.pagination.total_pages}),s}catch(t){throw this.handleError(t,"loading favourites"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderFavourites(t,e=!1){this.grid&&(0!==t.length||e?(this.hideEmptyState(),e||removeChildren(this.grid),t.forEach((t=>{const e=this.createItemElement(t);this.grid.appendChild(e),this.initItemFunctionality(e,t)})),window.jvbA11y&&window.jvbA11y.announce(`${e?"Added":"Loaded"} ${t.length} favourites`)):this.showEmptyState())}createItemElement(t){const e=document.createElement("div");e.className=`item ${t.type} favourited`,e.dataset.id=t.target_id,e.dataset.type=t.type;const s=sanitizeHtml(t.title||!1),i=sanitizeHtml(t.notes||"");let a="";return t.thumbnail&&(a=`\n                <div class="item-thumbnail">\n                    <a href="${t.url}">${t.thumbnail}</a>\n                </div>\n            `),e.innerHTML=`\n            <div class="item-select">\n                <input type="checkbox"\n                   class="favourite-checkbox"\n                    id="select-${t.target_id}"\n                    value="${t.target_id}">\n                <label for="select-${t.target_id}"><span class="screen-reader-text">Select this ${t.type}</span</label>\n            </div>\n\n            <button type="button" class="favourite-button favourited"\n                onclick="toggleFavourite(this)"\n                data-id="${t.target_id}"\n                data-type="${t.type}"\n                title="Remove from favourites">\n                ${jvbSettings.icons["heart-filled"]}\n            </button>\n\n            ${a}\n\n            <div class="item-info">\n                ${s?`<h3><a href="${t.url}">${s}</a></h3>`:`<a href="${t.url}">View Item</a>`}\n\n                ${t.author?`\n                <div class="item-artist">\n                    <span>By ${t.author.name}</span>\n                </div>`:""}\n\n                ${t.taxonomies?.length?`\n                <div class="taxonomy-lists">\n                    ${t.taxonomies.map((t=>`\n                        <div class="taxonomy-group">\n                            ${jvbSettings.icons[t.icon]}\n                            <ul>\n                                ${t.terms.slice(0,3).map((t=>`\n                                    <li>\n                                        <a href="${t.url}" ${t.umami_click}>\n                                            ${t.title}\n                                        </a>\n                                    </li>\n                                `)).join("")}\n                            </ul>\n                        </div>\n                    `)).join("")}\n                </div>\n            `:""}\n\n                <div class="notes-section">\n                    <button type="button" class="toggle-notes" aria-expanded="false">\n                        ${jvbSettings.icons.note||"Notes"}\n                        <span>Notes</span>\n                    </button>\n\n                    <div class="notes-content" hidden>\n                        <textarea class="notes-input"\n                            placeholder="Add notes about this item..."\n                            data-id="${t.target_id}"\n                            data-type="${t.type}">${i}</textarea>\n                        <button type="button" class="save-notes">Save Notes</button>\n                    </div>\n                </div>\n            </div>\n        `,e}initItemFunctionality(t,e){const s=t.querySelector(".toggle-notes"),i=t.querySelector(".notes-content");s&&i&&s.addEventListener("click",(()=>{const t="true"===s.ariaExpanded;s.ariaExpanded=!t.toString(),i.hidden=t,t||i.querySelector("textarea")?.focus()}));const a=t.querySelector(".save-notes"),n=t.querySelector(".notes-input");a&&n&&(a.addEventListener("click",(()=>{this.saveNotes(n)})),n.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.saveNotes(n))})))}saveNotes(t){if(!t)return;const e=t.value.trim(),s=t.dataset.id,i=t.dataset.type;s&&i&&(this.queue.addToQueue({type:"favourite_notes",data:{type:i,target_id:parseInt(s),notes:e}}),showToast("Notes saved"),this.a11y.announce("Notes saved"))}showEmptyState(t=!1){const e=this.container.querySelector(this.config.selectors.noItems)??this.createEmptyElement;e&&(e.hidden=!1),this.grid&&this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){const t=this.container.querySelector(".no-favourites");t&&t.remove(),this.grid&&this.grid.classList.remove("empty")}createEmptyElement(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ BLANK CANVAS ♡</h3>\n            <p>You haven't fallen in love with any pieces... yet!</p>\n            <p>Hit that heart icon when something stops your scroll.</p>\n            <p>Your dream collection is waiting to start.</p>\n        ",this.grid.after(e)}showEmptyListState(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ FULL OF POSSIBILITY ♡</h3>\n            <p>There's nothing in this list yet.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.after(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}async loadMoreItems(){!this.state.loading&&this.state.pagination.hasMore&&(this.state.page+=1,await this.loadFavourites())}updateTypeFilters(t){this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{const s=e.querySelector(".count");if(!s)return;const i=e.dataset.type;if("all"===i){const e=Object.values(t).reduce(((t,e)=>t+(parseInt(e)||0)),0);s.textContent=`(${e})`}else s.textContent=`(${t[i]||0})`}))}setFilterType(t){t!==this.state.filter.type&&(this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{e.classList.toggle("active",e.dataset.type===t),e.setAttribute("aria-selected",e.dataset.type===t)})),this.state.filter.type=t,this.state.page=1,this.loadFavourites(),window.jvbA11y&&window.jvbA11y.announce(`Filtered to show ${"all"===t?"all":t} items`))}setView(t){t!==this.state.view.mode&&(this.viewControls&&this.viewControls.querySelectorAll(".view-toggle").forEach((e=>{const s=e.dataset.view===t;e.setAttribute("aria-pressed",s)})),this.grid&&(this.grid.classList.remove("grid-view","list-view"),this.grid.classList.add(`${t}-view`)),this.state.view.mode=t,localStorage.setItem("favourites_view",t),window.jvbA11y&&window.jvbA11y.announce(`Changed to ${t} view`))}toggleSelectAll(t){const e=this.getVisibleItems();e.forEach((e=>{const s=e.querySelector('.item-select input[type="checkbox"]');s&&(s.checked=t,this.toggleItemSelection(s.value,t))})),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce(t?`Selected all ${e.length} items`:"Deselected all items")}getVisibleItems(){return this.grid?Array.from(this.grid.querySelectorAll(".item:not([hidden])")):[]}toggleItemSelection(t,e){e?this.state.selectedItems.add(t):this.state.selectedItems.delete(t);const s=this.grid.querySelector(`.item[data-id="${t}"]`);s&&s.classList.toggle("selected",e)}handleItemSelection(t){const e=t.checked,s=t.value;if(this.toggleItemSelection(s,e),this.updateBulkControls(),this.updateSelectAllState(),window.jvbA11y){const s=t.closest(".item"),i=s&&s.querySelector("h3")?.textContent||"item";window.jvbA11y.announce(e?`Selected ${i}`:`Deselected ${i}`)}}updateSelectAllState(){const t=this.container.querySelector(this.config.selectors.selectAll);if(!t)return;const e=this.getVisibleItems();if(0===e.length)return t.checked=!1,void(t.indeterminate=!1);const s=e.filter((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');return e&&e.checked})).length;0===s?(t.checked=!1,t.indeterminate=!1):s===e.length?(t.checked=!0,t.indeterminate=!1):(t.checked=!1,t.indeterminate=!0)}updateBulkControls(){if(!this.bulkControls)return;const t=this.bulkControls.querySelector(".bulk-actions");if(!t)return;const e=this.state.selectedItems.size>0;t.hidden=!e;const s=this.bulkControls.querySelector(".selected-count");s&&(s.textContent=e?`${this.state.selectedItems.size} selected`:"")}handleKeyDown(t){"Escape"===t.key&&this.state.selectedItems.size>0&&(t.preventDefault(),this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared using Escape key"))}clearSelection(){this.state.selectedItems.clear(),this.getVisibleItems().forEach((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');e&&(e.checked=!1),t.classList.remove("selected")}));const t=this.container.querySelector(this.config.selectors.selectAll);t&&(t.checked=!1,t.indeterminate=!1),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")}applyBulkAction(t){if(!t||0===this.state.selectedItems.size)return;switch(t){case"unfavourite":this.bulkUnfavourite();break;case"add-to-list":this.showAddToListModal();break;case"create-list":this.showCreateListModal();break;case"add-notes":this.showBulkNotesModal()}const e=this.container.querySelector(".bulk-action-select");e&&(e.value="")}handleItemActions(t){if(t.target.closest(".toggle-notes")){const e=t.target.closest(".toggle-notes"),s="true"===e.getAttribute("aria-expanded"),i=e.closest(".notes-section").querySelector(".notes-content");return e.setAttribute("aria-expanded",!s),i.hidden=s,!s&&i&&i.querySelector("textarea")?.focus(),void t.preventDefault()}if(t.target.closest(".save-notes")){const e=t.target.closest(".save-notes").closest(".notes-content").querySelector("textarea");return e&&this.saveNotes(e),void t.preventDefault()}if(t.target.closest(".view-list")){const e=t.target.closest(".view-list").closest(".list-card");return e&&e.dataset.id&&this.viewList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".share-list")){const e=t.target.closest(".share-list").closest(".list-card");return e&&e.dataset.id&&this.showShareModal(e.dataset.id),void t.preventDefault()}if(t.target.closest(".delete-list")){const e=t.target.closest(".delete-list").closest(".list-card");return e&&e.dataset.id&&this.deleteList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".back-to-lists"))return this.exitListView(),void t.preventDefault()}async loadLists(t=!0){try{this.state.loading=!0,this.loadingManager.show("Loading lists...");const t=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}`,{method:"GET",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("favourites")}},{context:"favourite-lists",forceRefresh:!1});return t.lists&&this.renderLists(t.lists),t}catch(t){throw this.handleError(t,"loading lists"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderLists(t){if(!this.listContainer)return;if(removeChildren(this.listContainer),!t||0===t.length)return void(this.listContainer.innerHTML='\n                <div class="no-lists">\n                    <h3>No Lists Yet</h3>\n                    <p>Select favourites from the main tab to organize into lists.</p>\n                </div>\n            ');const e=t.owned,s=t.shared;if(e.length>0){const t=document.createElement("details");t.className="lists-section owned-lists",t.open=!0,t.innerHTML="<summary>Your Lists:</summary>",e.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}if(s.length>0){const t=document.createElement("details");t.className="lists-section shared-lists",t.innerHTML="<summary>Lists Shared with You:</summary>",s.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}}createListCard(t){const e=document.createElement("div");e.className="list-card",e.dataset.id=t.id;const s="1"===t.is_shared;s&&e.classList.add("shared"),t.is_temp&&e.classList.add("temp"),t.is_owner;const i=sanitizeHtml(t.name||"Untitled List"),a=sanitizeHtml(t.description||"");return e.innerHTML=`\n            <div class="list-header">\n                <h3>${i}</h3>\n                <div class="list-actions">\n                    <button type="button" class="view-list" title="View List">\n                        ${jvbSettings.icons?.show||"View"}\n                    </button>\n                    ${s?"":`\n                        <button type="button" class="share-list" title="Share List">\n                            ${jvbSettings.icons?.share||"Share"}\n                        </button>\n                        <button type="button" class="delete-list" title="Delete List">\n                            ${jvbSettings.icons?.delete||"Delete"}\n                        </button>\n                    `}\n                </div>\n            </div>\n\n            ${a?`<p class="list-description">${a}</p>`:""}\n\n            <div class="list-meta">\n                <div class="meta-stats">\n                    <span class="item-count">${t.item_count||0} items</span>\n                    <span class="date">${formatDate(t.created_at)}</span>\n                </div>\n\n\n                ${s?`\n                    <div class="owner-info">\n                        Shared by ${t.owner_name||"another user"}\n                    </div>\n                `:t.share_count>0?`\n                    <div class="share-info">\n                        Shared with ${t.share_count} ${1===t.share_count?"person":"people"}\n                    </div>\n                `:""}\n            </div>\n        `,e}async viewList(t){try{this.state.loading=!0,this.loadingManager.show("Loading list..."),this.state.currentListId=t;const e=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("favourites")}},{context:"list-item",forceRefresh:!1});if(!e.list)throw new Error("List not found");this.showListDetails(e.list)}catch(t){this.handleError(t,"viewing list")}finally{this.state.loading=!1,this.loadingManager.hide()}}showListDetails(t){this.listDetails&&this.listContainer&&(console.log(t),this.listDetails.querySelector(".list-title").value=t.name||"Untitled List",this.listDetails.querySelector(".list-description").value=t.description||"",t.is_owner?this.listDetails.querySelector(".list-actions")||this.createListActions():this.listDetails.querySelector(".list-actions")?.remove(),removeChildren(this.grid),this.renderFavourites(t.items||[],!1),0===t.items.length&&this.showEmptyListState(),window.jvbA11y&&window.jvbA11y.announce(`Viewing list: ${t.name} with ${t.items?.length||0} items`))}createListActions(){const t=document.createElement("div");t.className="list-actions",t.innerHTML='\n            <button type="button" class="share-list" title="Share List">\n            <i class="icon icon-share-fat"></i>\n            <span>Share</span>\n        </button>\n        <button type="button" class="duplicate-list" title="Duplicate List">\n            <i class="icon icon-copy"></i>\n            <span>Duplicate</span>\n        </button>\n        <button type="button" class="delete-list" title="Delete List">\n            <i class="icon icon-trash"></i>\n            <span>Delete</span>\n        </button>\n        ',this.listDetails.insertBefore(t,this.listDetails.querySelector(".bulk-controls"))}exitListView(){this.listDetails&&this.listContainer&&(this.listDetails.hidden=!0,this.listContainer.hidden=!1,this.container.classList.remove("viewing-list"),this.state.currentListId=null,window.jvbA11y&&window.jvbA11y.announce("Returned to lists view"))}showCreateListModal(){this.createListModal&&(this.createListModal.querySelector("form")?.reset(),this.createListModal.showModal(),setTimeout((()=>{this.createListModal.querySelector("#list-name")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Create list dialog opened"))}showAddToListModal(){this.addToListModal&&(this.populateAddToListModal(),this.addToListModal.showModal(),window.jvbA11y&&window.jvbA11y.announce("Add to list dialog opened"))}async populateAddToListModal(){if(!this.addToListModal)return;const t=this.addToListModal.querySelector(".lists-options");if(t){removeChildren(t);try{const e=(await this.loadLists()).lists.owned;if(0===e.length)return t.innerHTML='\n                    <div class="no-lists">\n                        <p>You don\'t have any lists yet.</p>\n                        <button type="button" class="create-list-button">Create a list</button>\n                    </div>\n                ',void t.querySelector(".create-list-button")?.addEventListener("click",(()=>{this.addToListModal.close(),this.showCreateListModal()}));e.forEach((e=>{const s=document.createElement("div");s.className="list-option",s.innerHTML=`\n                    <input type="checkbox" id="${e.id}" name="list_ids[]" value="${e.id}">\n                    <label for="${e.id}">\n\n                        <span class="list-name">${sanitizeHtml(e.name)}</span>\n                        <span class="item-count">( ${e.item_count||0} items )</span>\n                    </label>\n                `,t.appendChild(s)}))}catch(e){t.innerHTML='\n                <div class="error-message">\n                    <p>Error loading lists. Please try again.</p>\n                </div>\n            ',console.error("Error loading lists for modal:",e)}}}async handleCreateList(t){const e=t.get("list_name"),s=t.get("list_description");if(e)try{this.showLoader("Creating list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_create",data:{name:e,description:s,items:t}}),showToast(`List "${e}" created`),this.a11y.announce(`List ${e} created with ${t.length} items`),this.createListModal.close(),this.clearSelection(),this.switchTab("lists")}catch(t){this.handleError(t,"creating list")}finally{this.hideLoader()}else showToast("Please enter a list name","error")}async handleAddToList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Adding to list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_add",data:{list_id:e.join(","),items:t}}),showToast(`Added to ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Added ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"adding to list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}async handleRemoveFromList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Removing from list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_remove",data:{list_id:e.join(","),items:t}}),showToast(`Removed from ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Removed ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"remove from list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}showShareModal(t){this.shareListModal&&(this.state.currentListId=t,this.shareListModal.querySelector("form")?.reset(),this.loadSharedUsers(t),this.shareListModal.showModal(),setTimeout((()=>{this.shareListModal.querySelector("#share-email")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Share list dialog opened"))}async loadSharedUsers(t){try{const e=this.shareListModal.querySelector(".shared-users");if(!e)return;e.innerHTML='<div class="loading">Loading shared users...</div>';const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("favourites")}},{context:"list-item",forceRefresh:!1});removeChildren(e),s.list&&s.list.shared_users&&s.list.shared_users.length>0?(s.list.shared_users.forEach((t=>{const s=document.createElement("div");s.className=`shared-user ${t.status}`,s.innerHTML=`\n                        <span class="user-email">${t.email}</span>\n                        ${"pending"===t.status?'<span class="pending-badge">Invitation sent</span>':`<span class="permission-badge">${t.permission_type||"view"}</span>`}\n                        <button type="button" class="remove-share" data-email="${t.email}">\n                            ${jvbSettings.icons?.delete||"Remove"}\n                        </button>\n                    `,e.appendChild(s)})),e.querySelectorAll(".remove-share").forEach((t=>{t.addEventListener("click",(()=>{this.unshareList(t.dataset.email)}))}))):e.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>'}catch(t){console.error("Error loading shared users:",t)}}async unshareList(t){if(confirm(`Remove ${t}'s access to this list?`))if(this.state.currentListId)try{this.showLoader("Removing access..."),this.queue.addToQueue({type:"favourite_list_unshare",data:{list_id:parseInt(this.state.currentListId),email:t}});const e=Array.from(this.shareListModal.querySelectorAll(".shared-user")).find((e=>e.querySelector(".user-email")?.textContent===t));e&&(e.classList.add("removing"),setTimeout((()=>{if(e.remove(),0===this.shareListModal.querySelectorAll(".shared-user").length){const t=this.shareListModal.querySelector(".shared-users");t&&(t.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>')}}),300)),showToast(`Removed ${t}'s access`),this.a11y.announce(`Removed ${t}'s access to list`)}catch(t){this.handleError(t,"removing share access")}finally{this.hideLoader()}else showToast("No list selected","error")}async deleteList(t){if(confirm("Are you sure you want to delete this list? This cannot be undone."))try{this.showLoader("Deleting list..."),this.queue.addToQueue({type:"favourite_list_delete",data:{list_id:parseInt(t)}});const e=this.container.querySelector(`.list-card[data-id="${t}"]`);e&&(e.classList.add("removing"),setTimeout((()=>{e.remove(),0===this.container.querySelectorAll(".list-card").length&&(this.listContainer.innerHTML='\n                                <div class="no-lists">\n                                    <h3>No Lists Yet</h3>\n                                    <p>Create your first list to organize your favourites!</p>\n                                </div>\n                            ')}),300)),showToast("List deleted"),this.a11y.announce("List deleted")}catch(t){this.handleError(t,"deleting list")}finally{this.hideLoader()}}showBulkNotesModal(){let t=document.querySelector(".bulk-notes-modal");t||(t=document.createElement("dialog"),t.className="bulk-notes-modal",t.innerHTML='\n                <form method="dialog" data-save="favourites">\n                    <h2>Add Notes to Selected Items</h2>\n\n                    <div class="field">\n                        <label for="bulk-notes">Notes (will be applied to all selected items)</label>\n                        <textarea id="bulk-notes" name="bulk_notes" rows="5"></textarea>\n                    </div>\n\n                    <div class="actions">\n                        <button type="button" class="cancel">Cancel</button>\n                        <button type="submit" class="save">Save Notes</button>\n                    </div>\n                </form>\n            ',document.body.appendChild(t),t.querySelector("form").addEventListener("submit",(e=>{e.preventDefault();const s=t.querySelector("#bulk-notes").value;this.saveBulkNotes(s),t.close()})),t.querySelector(".cancel").addEventListener("click",(()=>{t.close()}))),t.querySelector("form")?.reset(),t.showModal(),setTimeout((()=>{t.querySelector("#bulk-notes")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Add notes dialog opened")}saveBulkNotes(t){if(t)try{this.showLoader("Saving notes...");let e=Array.from(this.state.selectedItems.values());this.queue.addToQueue({type:"favourite_notes",data:{target_id:e.join(","),notes:t}}),showToast(`Notes saved for ${e.length} items`),this.a11y.announce(`Notes saved for ${e.length} items`),this.clearSelection()}catch(t){this.handleError(t,"saving bulk notes")}finally{this.hideLoader()}}async bulkUnfavourite(){if(confirm("Are you sure you want to remove these items from your favourites?"))try{this.showLoader("Removing from favourites...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);if(!s)return;const i=s.dataset.type;t.push({target_id:parseInt(e),type:i,action:"remove"})})),this.queue.addToQueue({type:"favourite_toggle",data:t});const e=[];this.state.selectedItems.forEach((t=>{const s=this.grid.querySelector(`.item[data-id="${t}"]`);if(!s)return;s.style.opacity="0",s.style.transform="scale(0.9)",s.style.transition="opacity 0.3s ease, transform 0.3s ease";const i=new Promise((t=>{setTimeout((()=>{s.remove(),t()}),300)}));e.push(i)})),await Promise.all(e),0===this.grid.children.length&&this.showEmptyState(),this.clearSelection(),showToast(`Removed ${t.length} items from favourites`),this.a11y.announce(`Removed ${t.length} items from favourites`)}catch(t){this.handleError(t,"removing favourites")}finally{this.hideLoader()}}async handleShareList(t){const e=t.get("share_email");if(e)if(this.validateEmail(e))try{this.showLoader("Sharing list..."),this.queue.addToQueue({type:"favourite_list_share",data:{list_id:parseInt(this.state.currentListId),email:e,permission_type:"view"}}),this.shareListModal.querySelector("#share-email").value="",this.loadSharedUsers(this.state.currentListId),showToast(`Invitation sent to ${e}`),this.a11y.announce(`Invitation sent to ${e}`)}catch(t){this.handleError(t,"sharing list")}finally{this.hideLoader()}else showToast("Please enter a valid email address","error");else showToast("Please enter an email address","error")}showLoader(t="Loading..."){if(!this.loader)return;const e=this.loader.querySelector(".loader-message");e&&(e.textContent=t),this.loader.hidden=!1}hideLoader(){this.loader&&(this.loader.hidden=!0)}showToast(t,e){window.jvbNotifications.showToast(t,e)}handleError(t,e){console.error(`Favourites error (${e}):`,t),showToast(`Error ${e}: ${t.message||"Something went wrong"}`,"error"),window.jvbError&&window.jvbError.log(t,{component:"FavouritesManager",action:e}),window.jvbA11y&&window.jvbA11y.announce(`Error ${e}. ${t.message||"Please try again."}`)}validateEmail(t){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)}};
\ No newline at end of file
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index 73ca2d3..95629ca 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.config={collectFormData:!1,...e};const t=window.jvbStore.register("forms",{storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4,validateData:!0,delayFetch:!0});this.store=t.forms,this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.isTimeline=!1,window.crudManager&&window.crudManager.isTimeline&&(this.isTimeline=!0),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.processRepeaterField=this.processRepeaterField.bind(this),this.processGroupField=this.processGroupField.bind(this),this.processLocationField=this.processLocationField.bind(this),this.processRegularField=this.processRegularField.bind(this),this.init()}async init(){this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners(),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.type&&this.handleOperationComplete(t)}))}async handleOperationComplete(e){if(e.formId)try{await this.store.delete(e.formId)}catch(e){console.warn("Failed to clear form cache:",e)}const t=this.forms.get(e.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}checkPendingForms(){this.store.getAll().filter((e=>"draft"===e.status)).forEach((e=>{const t=this.forms.get(e.formId);if(t?.element){const s=t.element.querySelector(".restore-form");s&&(s.hidden=!1),new this.populateForm(t.element,e.data)}}))}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e,t){const s=document.querySelector(`[data-form-id="${e}"]`);if(!s)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore-changes" data-form-id="${e}">Restore</button>\n        <button class="discard-changes" data-form-id="${e}">Discard</button>\n    `,s.insertBefore(r,s.firstChild),r.querySelector(".restore-changes").addEventListener("click",(async()=>{await this.restorePendingForm(e,t),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(async()=>{await this.discardPendingForm(e),r.remove()}))}async restorePendingForm(e,t){const s=document.querySelector(`[data-form-id="${e}"]`);s&&(new this.populateForm(s,t),await this.store.save({formId:e,data:t,status:"restored",timestamp:Date.now()}),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){try{await this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}catch(e){console.error("Failed to discard pending form:",e)}}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const s=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=s,e.addEventListener("submit",this.submitHandler);const r={element:e,id:s,status:"",options:{autosave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e,!0)};if(this.initializeFormFields(e,r),this.forms.set(s,r),this.store&&r.options.cache){const e=this.store.get(s);e&&e.formData&&this.showPendingNotification(e)}return r}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),s=t.element,r=t.tabs,a=s.querySelectorAll(".tab-content").length,i=s.querySelector(".form-progress .fill"),n=s.querySelector(".step-text .current"),o=s.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,s)=>{const r=s+1;t.classList.remove("current","completed","pending"),r<e?t.classList.add("completed"):r===e?t.classList.add("current"):t.classList.add("pending")}))};s.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=s.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;r.switchTab(e,!0),l(i+1),s.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=s.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;r.switchTab(e,!0),l(i-1),s.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=r.switchTab.bind(r);r.switchTab=(e,t)=>{c(e,t);const r=s.querySelector(`.tab-content[data-tab="${e}"]`);if(r){const e=parseInt(r.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let s=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(s=!1)}})),s}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const s=e.querySelector(".add-repeater-row"),r=e.querySelector(".repeater-items"),a=e.querySelector("template");s&&a&&r&&(window.Sortable&&new Sortable(r,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),s.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),r.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const s=e.querySelector(".repeater-items"),r=e.querySelector("template"),a=s.children.length,i=e.dataset.field,n=r.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const s=e.nextElementSibling;s&&"LABEL"===s.tagName&&(s.htmlFor=e.id)})),s.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const s=e.closest(".repeater"),r=s.dataset.field;e.remove(),this.updateRepeaterOrder(s,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:r,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const s=e.querySelector(".repeater-items"),r=e.dataset.field;Array.from(s.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const s=e.name.split(":");if(3===s.length){const a=s[2];e.name=`${r}:${t}:${a}`,e.id=`${r}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:r,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((s=>{const r=s.dataset.dependsOn,a=s.dataset.dependsValue,i=s.dataset.dependsOperator||"==";t.dependencies.has(r)||t.dependencies.set(r,[]),t.dependencies.get(r).push({field:s,requiredValue:a,operator:i}),this.checkFieldDependency(e,s,r,a,i)}))}checkFieldDependency(e,t,s,r,a){const i=e.querySelector(`[name="${s}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,r,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,s){const r=String(e||""),a=String(t||"");switch(s){case"==":default:return r==a;case"!=":return r!=a;case">":return parseFloat(r)>parseFloat(a);case"<":return parseFloat(r)<parseFloat(a);case">=":return parseFloat(r)>=parseFloat(a);case"<=":return parseFloat(r)<=parseFloat(a);case"contains":return r.includes(a);case"empty":return""===r;case"not_empty":return""!==r}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),s=e.closest(".field");let r=s?.querySelector(".char-count");!r&&s&&(r=document.createElement("div"),r.className="char-count",r.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,s.appendChild(r));const a=()=>{const s=e.value.length;r&&(r.querySelector(".current").textContent=s,r.classList.toggle("exceeded",s>t)),s>t&&(e.value=e.value.substring(0,t),r&&(r.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const s=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const r=this.collectFormData(t);this.notify("form-submit",{formId:t.dataset.formId,fullData:r,config:s})}else;}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const r=window.getIcon?.("check-circle");r&&(r.classList.add("success-icon"),s.prepend(r))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),this.touchedFields.add(t.field),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const r=window.getIcon?.("close-circle");r&&(r.classList.add("error-icon"),s.prepend(r)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]");switch(t=t.dataset.action,t){case"clear-form":let t=e.target.closest("form");this.store.delete(t.dataset.formId),t?.reset(),e.target.closest(".restore-form").hidden=!0;break;case"dismiss-restore":e.target.closest(".restore-form").hidden=!0}}}handleNumberClick(e,t){let s=0;if(e.target.closest(".increase")?s+=1:e.target.closest(".decrease")&&(s-=1),0!==s){let r=parseFloat(t.step);r=Math.max(r,1),e.ctrlKey&&e.shiftKey?r*=50:e.ctrlKey?r*=5:e.shiftKey&&(r*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+r*s,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,s,r,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>s?(e.value=s,r.disabled=!1):r.disabled?r.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,s=t.form||t.closest("form");if(!s)return;const r=this.forms?.get(s.dataset.formId);if(r&&(r.options.autosave||this.subscribers.size>0)){const e=r.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(s,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(r,a)}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,s=t.form||t.closest("form");if(!s)return;const r=e.target.closest("input, textarea, select");if(r){const e=this.findFieldWrapper(r);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(r)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(r,e)}const a=this.forms?.get(s.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form"))return;const t=e.target.closest("input, textarea, select");if(!t)return;let s=t.closest("form");this.showFormStatus(s.dataset.formId,"pending");const r=this.findFieldWrapper(t);if(!r)return;const a=r.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,((e,t)=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with http:// or https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const r=t.dataset.min,a=t.dataset.max;return void 0!==r&&s<parseFloat(r)?`Value must be at least ${r}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,r=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(r&&e.length>parseInt(r))||`Must be no more than ${r} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const s=this.getFieldValue(e),r=t.dataset.field;if(!this.touchedFields.has(r)&&!e.required)return!0;if(!s&&!e.required)return this.clearValidation(t),!0;if(e.required&&!s)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&s){if(!new RegExp(a).test(s)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(s))return this.showError(t,e.message),!1;if(e.test){const r=e.test(s,t);if(!0!==r)return this.showError(t,r),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}showSuccess(e,t=""){if(!e)return;const s=e.querySelector(".validation-icon.success"),r=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),s&&(s.hidden=!1),r&&(r.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const s=e.querySelector(".validation-icon.success"),r=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),s&&(s.hidden=!0),r&&(r.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),s=e.querySelector(".validation-message"),r=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),r?.classList.remove("error"),t&&(t.hidden=!0),s&&(s.hidden=!0,s.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let s=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const r=e.dataset.field;r&&this.touchedFields.add(r);this.validateField(t,e)||(s=!1,!1===s&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),s}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const s=t.target.closest("input, select");if(!s)return;const r=s.name;if(!r)return;e.querySelectorAll(`[data-show-if*="${r}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const s=e.dataset.field,r=e.querySelector(".validation-message");s&&r&&(t[s]=r.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const s=`autosave_${e.id}`;this.debouncer.schedule(s,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")})).catch((t=>{console.error("Autosave failed:",t),this.showFormStatus(e.id,"error","Failed to save changes")}));const s=this.getChangedFields(e.data,t);if(0!==Object.keys(s).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,r]of Object.entries(t))"object"==typeof r&&(s[e]=r);this.notify("form-autosave",{formId:e.id,changes:s,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const s=this.collectFormData(t.element),r=this.getChangedFields(t.data,s);return Object.keys(r).length>0}showFormStatus(e,t,s=""){let r=this.forms.get(e);if(!r.options.formStatus)return;if(r.status===t)return;r.status=t;const a=r.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove();const n={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);o&&a.prepend(o),""===s&&(s=n[t]||t),i.textContent=s,a.classList.toggle("loading",["uploading","saving"].includes(t)),"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e,t=!1){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);if(e.classList.contains("table")&&"FORM"===e.tagName)return{};const s=new FormData(e);let r={};const a={},i={};for(let[t,n]of s.entries()){if(this.ignore.includes(t)||t.endsWith("_temp"))continue;this.getFieldProcessor(t)(t,n,r,a,i,e)}return window.isEmptyObject(i)?this.mergeRepeaterData(r,a):(r=this.mergeRepeaterData(r,a),this.mergePostData(r,i))}collectTimeline(e){let t={},s={},r=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)\](.+)$/);if(a){const[,t,o]=a;if(s[t]||(s[t]={id:parseInt(t)},r.push(t)),"post_thumbnail"===o)s[t].post_thumbnail=parseInt(e.querySelector(`[name="${i}"]`).closest(".item")?.dataset.id);else{this.getFieldProcessor(o)(o,n,s[t],{},{},e)}}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=r.map((e=>s[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],t}getFieldProcessor(e){return e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((s=>{const r={};Object.keys(t[s]).forEach((e=>{const a=t[s][e];Object.keys(a).length>0&&(r[e]=a)})),e[s]=Object.values(r)})),e}mergePostData(e,t){for(let[s,r]of Object.entries(t))e[s]=r;return e}processRepeaterField(e,t,s,r,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),r[n]||(r[n]={}),r[n][o]||(r[n][o]={}),c||r[n][o][l]?(r[n][o][l]?Array.isArray(r[n][o][l])||(r[n][o][l]=[r[n][o][l]]):r[n][o][l]=[],r[n][o][l].push(t)):r[n][o][l]=t}processGroupField(e,t,s,r,a,i){const n=e.split("::"),o=n[0];s[o]||(s[o]={});let l=s[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,s,r,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(s,n)||(s[n]={},Object.hasOwn(s,"sendAll")?s.sendAll.includes(n)||s.sendAll.push(n):s.sendAll=[n]),s[n][o]=t}processRegularField(e,t,s,r,a,i){s[e=e.replace("[]","")]?(Array.isArray(s[e])||(s[e]=[s[e]]),s[e].push(t)):s[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const s=this.forms.get(e);if(!s)return;const r=s.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary"),[i,n,o]=[a.querySelector("h2"),a.querySelector(".summary"),a.querySelector(".result")],l=["sendAll",...this.ignore];for(const[e,t]of Object.entries(s.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const s=this.getFieldInfo(r,e);if(!s.label)continue;const a=this.createResultElement(o,s,t,r);a&&n.appendChild(a)}o.remove(),(t="form"!==t?r.closest(t)??r:r).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let s=e.querySelector(`label[for="${t}"]`),r=null,a=null;if(r||(r=e.querySelector(`[name="${t}"]`)),r||(r=e.querySelector(`[name="${t}[]"]`)),!r){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(s=a.querySelector("legend"),r=a.querySelector("input, select, textarea"))}if(!s&&r){const e=r.closest(".field, fieldset");e&&(s=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:r&&(i="checkbox"===r.type&&r.name.endsWith("[]")?"checkbox":"checkbox"===r.type?"true_false":"SELECT"===r.tagName&&r.multiple?"select":r.type||"text"),{label:s?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:r}}createResultElement(e,t,s,r){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(s,t.type,r);return this.isHtmlContent(o)?n.innerHTML=o:n.textContent=o,a}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,s){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,s,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,s,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,s)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${s+1}:</strong><ul>`;for(const[s,r]of Object.entries(e))if(!this.isEmptyValue(r)){const e=s.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${r}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[s,r]of Object.entries(e))if(!this.isEmptyValue(r)){const e=s.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof r||Array.isArray(r)?t+=`<li><strong>${e}:</strong> ${r}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(r)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((s=>{e[s]&&t.push(e[s])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,s=null){if(0===e.length)return"<em>None selected</em>";if(t&&s&&s.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,s.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,s){if("select"===s){const s=t.querySelector(`option[value="${e}"]`);return s?.textContent||e}if("radio"===s){const s=t.querySelector(`input[type="radio"][value="${e}"]`),r=s?.nextElementSibling;return r?.textContent||e}if("checkbox"===s){const s=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(s){const e=t.querySelector(`label[for="${s.id}"]`);if(e)return e.textContent.trim();const r=s.nextElementSibling;if("LABEL"===r?.tagName)return r.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){this.config={collectFormData:!1,...e},this.isRestoring=!1;const t=window.jvbStore.register("forms",{storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4,validateData:!0,delayFetch:!0});this.store=t.forms,this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.isTimeline=window.crudManager&&window.crudManager.isTimeline,this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.blurHandler=this.handleBlur.bind(this),this.processRepeaterField=this.processRepeaterField.bind(this),this.processGroupField=this.processGroupField.bind(this),this.processLocationField=this.processLocationField.bind(this),this.processRegularField=this.processRegularField.bind(this),this.init()}async init(){this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners(),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.type&&this.handleOperationComplete(t)}))}async handleOperationComplete(e){if(e.formId)try{await this.store.delete(e.formId)}catch(e){console.warn("Failed to clear form cache:",e)}const t=this.forms.get(e.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){const e=await this.store.getAll(),t=window.location.pathname;e.filter((e=>{if("draft"!==e.status)return!1;const r=e.data?._wp_http_referer;return r===t})).forEach((e=>{const t=this.findFormElement(e);if(!t)return;let r=this.forms.get(e.formId);t.dataset.formId||(r=this.registerForm(t)),this.isRestoring=!0,new this.populateForm(t,e.data),setTimeout((()=>{this.isRestoring=!1}),0),this.showFormStatus(e.formId,"restored"),window.jvbA11y&&window.jvbA11y.announce("Your previous entry has been restored")}))}findFormElement(e){if(e.data?.form_id){const t=document.querySelector(`[name="form_id"][value="${e.data.form_id}"]`)?.closest("form");if(t)return t}if(e.data?.form_type){const t=document.querySelector(`[name="form_type"][value="${e.data.form_type}"]`)?.closest("form");if(t)return t}return document.querySelector(`[data-form-id="${e.formId}"]`)}showPendingNotification(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);if(!r)return;const s=document.createElement("div");s.className="pending-changes-notification",s.innerHTML=`\n        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore-changes" data-form-id="${e}">Restore</button>\n        <button class="discard-changes" data-form-id="${e}">Discard</button>\n    `,r.insertBefore(s,r.firstChild),s.querySelector(".restore-changes").addEventListener("click",(async()=>{await this.restorePendingForm(e,t),s.remove()})),s.querySelector(".discard-changes").addEventListener("click",(async()=>{await this.discardPendingForm(e),s.remove()}))}async restorePendingForm(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);r&&(new this.populateForm(r,t),await this.store.save({formId:e,data:t,status:"restored",timestamp:Date.now()}),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){try{await this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}catch(e){console.error("Failed to discard pending form:",e)}}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,status:"",options:{autosave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e,!0)};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.data&&this.showPendingNotification(r,e.data)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),this.initTagListFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=s.switchTab.bind(s);s.switchTab=(e,t)=>{c(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initTagListFields(e,t){e.querySelectorAll(".field.tag-list").forEach((e=>{const r=e.querySelector(".tag-input-row"),s=e.querySelector(".add-tag-item"),a=e.querySelector(".tag-items"),i=e.querySelector(".tag-template"),n=e.dataset.field,o=e.dataset.tagFormat||"first_field";if(!(r&&s&&a&&i))return;const l=()=>Array.from(r.querySelectorAll("input, select, textarea")).filter((e=>!e.closest("button"))),c=()=>{const r=l(),s={};let c=!1;if(r.forEach((e=>{const t=e.name.replace("new_",""),r=this.getFieldValue(e);r&&(c=!0),s[t]=r})),!c)return window.jvbA11y&&window.jvbA11y.announce("Please fill in at least one field","error"),void r[0].focus();const d=r.find((e=>{const t="required"in e.dataset&&"1"===e.dataset.required,r=this.getFieldValue(e);return t&&!r}));if(d){const e=d.closest(".field"),t=e?.querySelector("label")?.textContent||"This field";return this.showError(e,`${t} is required.`),void d.focus()}for(let t of r){let r=e.closest(".field");if(!this.validateField(t,r))return void t.focus()}const u=a.children.length,h=i.content.cloneNode(!0).firstElementChild;h.dataset.index=u;const m=h.querySelector(".tag-label");m&&(m.textContent=this.getTagDisplayText(s,o)),h.querySelectorAll('input[type="hidden"]').forEach((e=>{const t=e.dataset.field;e.name=`${n}:${u}:${t}`,e.value=s[t]||""})),a.appendChild(h),r.forEach((e=>{"checkbox"===e.type||"radio"===e.type?e.checked=!1:e.value="";let t=e.closest(".field");this.clearValidation(t)})),r.length>0&&r[0].focus(),t&&this.scheduleSave(t,{type:"tag_list",action:"add",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce("Item added")};s.addEventListener("click",c);const d=l();d.length>0&&(d[d.length-1].addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),c())})),d.slice(0,-1).forEach(((e,t)=>{e.addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),d[t+1].focus())}))}))),a.addEventListener("click",(e=>{if(e.target.closest(".remove-tag")){const r=e.target.closest(".tag-item"),s=r.querySelector(".tag-label")?.textContent||"Item";r.remove(),this.reindexTagList(a,n),t&&this.scheduleSave(t,{type:"tag_list",action:"remove",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce(`${s} removed`)}}))}))}reindexTagList(e,t){Array.from(e.children).forEach(((e,r)=>{e.dataset.index=r,e.querySelectorAll('input[type="hidden"]').forEach((e=>{const s=e.dataset.field;e.name=`${t}:${r}:${s}`}))}))}getTagDisplayText(e,t){const r=Object.values(e).filter((e=>e));if(0===r.length)return"New Item";switch(t){case"first_field":return r[0];case"all_fields":return r.join(", ");default:if(t.includes("{")){let r=t;for(const[t,s]of Object.entries(e))r=r.replace(`{${t}}`,s);return r}return e[t]||r[0]}}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s===a;case"!=":return s!==a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const r=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const s=this.collectFormData(t);this.notify("form-submit",{formId:t.dataset.formId,fullData:s,config:r})}else;}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const r=document.createElement("div");r.className="form-success-message success-message",r.textContent=t.message,e.insertBefore(r,e.firstChild);const s=window.getIcon?.("check-circle");s&&(s.classList.add("success-icon"),r.prepend(s))}if(t.title||t.description){const r=document.createElement("div");if(r.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,r.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,r.appendChild(t)}))}e.insertBefore(r,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const r=e.querySelector(`[data-field="${t.field}"]`);if(r){this.showError(r,t.message),this.touchedFields.add(t.field),r.scrollIntoView({behavior:"smooth",block:"center"});const e=r.querySelector("input, textarea, select");e&&e.focus()}}else{const r=document.createElement("div");r.className="form-error error-message",r.textContent=t.message;const s=window.getIcon?.("close-circle");s&&(s.classList.add("error-icon"),r.prepend(s)),e.insertBefore(r,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]"),r=t.dataset.action,s=t.closest("form");switch(r){case"clear-form":s?.dataset.formId&&(this.store.delete(s.dataset.formId),s.reset(),s.querySelector(".fstatus").hidden=!0),window.jvbA11y&&window.jvbA11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":s.querySelector(".fstatus").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(s&&(s.options.autosave||this.subscribers.size>0)){const e=s.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(s,a)}}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form")||this.isRestoring)return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,(()=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}showSuccess(e,t=""){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),r&&(r.hidden=!1),s&&(s.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let r=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const s=e.dataset.field;s&&this.touchedFields.add(s);this.validateField(t,e)||(r=!1,!1===r&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),r}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const r=t.target.closest("input, select");if(!r)return;const s=r.name;if(!s)return;e.querySelectorAll(`[data-show-if*="${s}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const r=e.dataset.field,s=e.querySelector(".validation-message");r&&s&&(t[r]=s.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")})).catch((t=>{console.error("Autosave failed:",t),this.showFormStatus(e.id,"error","Failed to save changes")}));const r=this.getChangedFields(e.data,t);if(0!==Object.keys(r).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.data,r);return Object.keys(s).length>0}showFormStatus(e,t,r=""){let s=this.forms.get(e);if(!s?.options.formStatus)return;if(s.status===t)return;s.status=t;const a=s.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove(),a.querySelector(".actions")?.remove();const n={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);if(o&&a.prepend(o),""===r&&(r=n[t]||t),i.textContent=r,a.classList.toggle("loading",["uploading","saving"].includes(t)),"restored"===t){const e=document.createElement("div");e.className="actions",e.innerHTML='\n            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>\n            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>\n        ',a.appendChild(e),setTimeout((()=>a.hidden=!0),1e4)}"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e,t=!1){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);if(e.classList.contains("table")&&"FORM"===e.tagName)return{};const r=new FormData(e);let s={};const a={},i={};for(let[t,n]of r.entries()){if(this.ignore.includes(t)||t.endsWith("_temp"))continue;this.getFieldProcessor(t)(t,n,s,a,i,e)}return 0!==Object.keys(i).length?(s=this.mergeRepeaterData(s,a),this.mergePostData(s,i)):this.mergeRepeaterData(s,a)}collectTimeline(e){let t={},r={},s=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)\](.+)$/);if(a){const[,t,o]=a;if(r[t]||(r[t]={id:parseInt(t)},s.push(t)),"post_thumbnail"===o)r[t].post_thumbnail=parseInt(e.querySelector(`[name="${i}"]`).closest(".item")?.dataset.id);else{this.getFieldProcessor(o)(o,n,r[t],{},{},e)}}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=s.map((e=>r[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],t}getFieldProcessor(e){return e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[r,s]of Object.entries(t))e[r]=s;return e}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),c||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary");if(!a)return;const i=a.querySelector(".result"),n=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(n.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;let o=i.cloneNode(!0),l=o.querySelector("h3"),c=o.querySelector("p");l.textContent=r.label;let d=this.formatFieldValue(t,r.type);this.isHtmlContent(d)?c.innerHTML=d:c.textContent=d,a.append(o)}i.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=e.querySelector(`[name=${t}]`),a=s?.closest(".field");if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
\ No newline at end of file
diff --git a/assets/js/min/gallery.min.js b/assets/js/min/gallery.min.js
index c5de416..e30cc52 100644
--- a/assets/js/min/gallery.min.js
+++ b/assets/js/min/gallery.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.index=0,this.images=[],this.zoom={scale:1,min:1,max:4,threshold:50,x:0,y:0,startX:0,startY:0,ease:.2,panning:!1},this.swipe=this.resetSwipe(),this.activePointers=new Map,this.lastTap=0,this.initElements(),this.initModal(),this.initListeners(),this.initSubscribers()}initElements(){this.elements={imageSelector:"a.open-gallery",gallery:{modal:"dialog.gallery",wrap:".wrap",nextButton:".next",prevButton:".prev",image:".image",leftImage:".image-left",rightImage:".image-right",counter:".counter"}},this.ui=window.uiFromSelectors(this.elements)}initModal(){this.modal=new window.jvbModal(this.ui.gallery.modal,{openMessage:"Opened Gallery",closeMessage:"Closed Gallery"}),this.modal.subscribe(((e,t)=>{"modal-close"===e&&this.toggleGallery(!1)}))}buildGalleryItems(e=null){let t=e?`[data-opens="${e}"]`:this.elements.imageSelector;this.items=Array.from(document.querySelectorAll(t)).map(((e,t)=>{let i=e.querySelector("img");return{id:e.dataset.id||t,small:i.dataset.small||e.src,medium:i.dataset.medium||e.src,full:i.dataset.full||e.src,alt:i.alt||"",element:i}}))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.pointerDownHandler=this.onPointerDown.bind(this),this.pointerMoveHandler=this.onPointerMove.bind(this),this.pointerUpHandler=this.onPointerUp.bind(this),this.wheelHandler=this.onWheel.bind(this),this.keyHandler=this.handleKeys.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){let t=window.targetCheck(e,this.elements.imageSelector);t&&!this.modal.isOpen?(e.preventDefault(),this.buildGalleryItems(Object.hasOwn(t.dataset,"opens")?t.dataset.opens:null),this.index=this.items.findIndex((e=>e.element===t.querySelector("img"))),this.toggleGallery(!0)):this.modal.isOpen&&(window.targetCheck(e,this.elements.gallery.nextButton)?(console.log("Next"),this.nextElement()):window.targetCheck(e,this.elements.gallery.prevButton)&&(console.log("Previous"),this.prevElement()))}handleKeys(e){if(this.modal.isOpen){switch(e.key){case"ArrowLeft":e.preventDefault(),this.prevElement();break;case"ArrowRight":e.preventDefault(),this.nextElement()}e.ctrlKey&&("+"!==e.key&&"="!==e.key||(e.preventDefault(),this.handleZoom(.2)),"-"===e.key&&(e.preventDefault(),this.handleZoom(-.2)),"0"===e.key&&(e.preventDefault(),this.resetZoom()))}}onPointerDown(e){this.swipe.startX=e.clientX,this.swipe.startY=e.clientY,this.ui.gallery.image.setPointerCapture(e.pointerId),this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=performance.now();if(t-this.lastTap<300&&1===this.activePointers.size)return this.zoom.scale>1?this.resetZoom():this.handleZoom(1,e.clientX,e.clientY),void(this.lastTap=0);if(this.lastTap=t,2===this.activePointers.size){const e=[...this.activePointers.values()];return this.pinchStartDist=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),void(this.pinchStartScale=this.zoom.scale)}this.zoom.scale>1&&(this.zoom.panning=!0,this.zoom.startX=e.clientX-this.zoom.x,this.zoom.startY=e.clientY-this.zoom.y)}onPointerMove(e){if(this.activePointers.has(e.pointerId))if(this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),2!==this.activePointers.size)this.zoom.panning&&(this.zoom.x=e.clientX-this.zoom.startX,this.zoom.y=e.clientY-this.zoom.startY,this.applyTransform());else{const e=[...this.activePointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),i=this.pinchStartScale*(t/this.pinchStartDist)-this.zoom.scale;this.handleZoom(i)}}onPointerUp(e){if(this.activePointers.delete(e.pointerId),this.activePointers.size<2&&(this.pinchStartDist=0),!this.zoom.panning&&0===this.activePointers.size){this.swipe.endX=e.clientX,this.swipe.endY=e.clientY;const t=this.swipe.endX-this.swipe.startX;this.swipe.endY,this.swipe.startY;Math.abs(t)>this.zoom.threshold&&(t>0?(console.log("Swipe right"),this.prevElement()):(console.log("Swipe left"),this.nextElement())),this.zoom.panning=!1}}onWheel(e){if(!e.ctrlKey)return;e.preventDefault();const t=e.deltaY<0?.2:-.2;this.handleZoom(t,e.clientX,e.clientY)}clampPan(){const e=this.ui.gallery.wrap;if(!e)return;const t=e.getBoundingClientRect(),i=Math.min(t.width/1920,t.height/1920),s=1920*i,n=1920*i*this.zoom.scale,o=s*this.zoom.scale,l=t.width-n-32,a=t.height-o-32;this.zoom.x=Math.min(32,Math.max(l,this.zoom.x)),this.zoom.y=Math.min(32,Math.max(a,this.zoom.y))}handleZoom(e,t=null,i=null){const s=this.zoom.scale;let n=s+e;if(n=Math.min(this.zoom.max,Math.max(this.zoom.min,n)),n===s)return;const o=n/s;let l=this.ui.gallery.image.getBoundingClientRect();null!==t&&null!==i||(t=l.left+l.width/2,i=l.top+l.height/2);const a=t-l.left,r=i-l.top;this.zoom.x=(this.zoom.x-a)*o+a,this.zoom.y=(this.zoom.y-r)*o+r,this.zoom.scale=n,this.applyTransform(),this.notify("zoom",{scale:this.zoom.scale})}applyTransform(){this.ui.gallery.image.style.transform=`translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`}resetZoom(){this.zoom.scale=1,this.zoom.x=0,this.zoom.y=0,this.zoom.startX=0,this.zoom.startY=0,this.zoom.panning=!1,this.applyTransform()}resetSwipe(){return{startX:null,startY:null,endX:null,endY:null}}toggleGallery(e,t=null){e?(this.ui.gallery.image.addEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.addEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.addEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.addEventListener("pointercancel",this.pointerUpHandler),window.addEventListener("wheel",this.wheelHandler,{passive:!1}),window.addEventListener("keydown",this.keyHandler),this.moveIntoView()):(this.ui.gallery.image.removeEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.removeEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.removeEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.removeEventListener("pointercancel",this.pointerUpHandler),window.removeEventListener("wheel",this.wheelHandler),window.removeEventListener("keydown",this.keyHandler),this.resetZoom(),this.resetSwipe(),this.activePointers.clear(),this.lastTap=0),e&&!this.modal.isOpen&&this.modal.handleOpen()}moveIntoView(e=0){let t=this.index+e;t<0?t=this.items.length-1:t>=this.items.length?t=0:t===this.items.length-3&&this.notify("load-more"),this.index=t,this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}`)}nextElement(){this.resetZoom(),this.moveIntoView(1)}prevElement(){this.resetZoom(),this.moveIntoView(-1)}updateDisplay(){const e=this.items[this.index];e&&(this.ui.gallery.image.src=e.full,this.ui.gallery.image.alt=e.alt,this.ui.gallery.counter.textContent=`${this.index+1} / ${this.items.length}`,this.ui.gallery.prevButton.disabled=this.items.length<=1,this.ui.gallery.nextButton.disabled=this.items.length<=1)}preloadAdjacent(){[-1,1].forEach((e=>{const t=this.index+e;if(t>0&&t<this.items.length){const i=this.items[t];(e<0?this.ui.gallery.leftImage:this.ui.gallery.rightImage).src=i.full}}))}initSubscribers(){this.subscribers=new Set}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.toggleGallery(!1),document.removeEventListener("click",this.clickHandler)}}document.addEventListener("DOMContentLoaded",(function(){document.querySelector("dialog.gallery")&&(window.jvbGallery=new e)}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.index=0,this.images=[],this.zoom={scale:1,min:1,max:4,threshold:50,x:0,y:0,startX:0,startY:0,ease:.2,panning:!1},this.swipe=this.resetSwipe(),this.activePointers=new Map,this.lastTap=0,this.initElements(),this.initModal(),this.initListeners(),this.initSubscribers()}initElements(){this.elements={imageSelector:"img[data-gallery]",gallery:{modal:"dialog.gallery",wrap:".wrap",nextButton:".next",prevButton:".prev",image:".image",leftImage:".image-left",rightImage:".image-right",counter:".counter"}},this.ui=window.uiFromSelectors(this.elements)}initModal(){this.modal=new window.jvbModal(this.ui.gallery.modal,{openMessage:"Opened Gallery",closeMessage:"Closed Gallery"}),this.modal.subscribe(((e,t)=>{"modal-close"===e&&this.toggleGallery(!1)}))}buildGalleryItems(e=null){let t=e?`[data-gallery="${e}"]`:this.elements.imageSelector;this.items=Array.from(document.querySelectorAll(t)).map(((e,t)=>({id:e.dataset.id||t,srcset:e.srcset||e.src,sizes:e.sizes||"100vw",src:e.currentSrc||e.src,full:e.dataset.full||e.src,alt:e.alt||"",element:e})))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.pointerDownHandler=this.onPointerDown.bind(this),this.pointerMoveHandler=this.onPointerMove.bind(this),this.pointerUpHandler=this.onPointerUp.bind(this),this.wheelHandler=this.onWheel.bind(this),this.keyHandler=this.handleKeys.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){let t=window.targetCheck(e,this.elements.imageSelector);t&&!this.modal.isOpen?(e.preventDefault(),this.buildGalleryItems(t.dataset.gallery||null),this.index=this.items.findIndex((e=>e.element===t)),this.toggleGallery(!0)):this.modal.isOpen&&(window.targetCheck(e,this.elements.gallery.nextButton)?(console.log("Next"),this.nextElement()):window.targetCheck(e,this.elements.gallery.prevButton)&&(console.log("Previous"),this.prevElement()))}handleKeys(e){if(this.modal.isOpen){switch(e.key){case"ArrowLeft":e.preventDefault(),this.prevElement();break;case"ArrowRight":e.preventDefault(),this.nextElement()}e.ctrlKey&&("+"!==e.key&&"="!==e.key||(e.preventDefault(),this.handleZoom(.2)),"-"===e.key&&(e.preventDefault(),this.handleZoom(-.2)),"0"===e.key&&(e.preventDefault(),this.resetZoom()))}}onPointerDown(e){e.preventDefault(),this.swipe.startX=e.clientX,this.swipe.startY=e.clientY,this.ui.gallery.image.setPointerCapture(e.pointerId),this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=performance.now();if(t-this.lastTap<300&&1===this.activePointers.size)return this.zoom.scale>1?this.resetZoom():this.handleZoom(1,e.clientX,e.clientY),void(this.lastTap=0);if(this.lastTap=t,2===this.activePointers.size){const e=[...this.activePointers.values()];return this.pinchStartDist=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),void(this.pinchStartScale=this.zoom.scale)}this.zoom.scale>1&&(this.zoom.panning=!0,this.zoom.startX=e.clientX-this.zoom.x,this.zoom.startY=e.clientY-this.zoom.y,this.ui.gallery.image.style.cursor="grabbing")}onPointerMove(e){if(this.activePointers.has(e.pointerId))if(this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),2!==this.activePointers.size)this.zoom.panning&&(this.zoom.x=e.clientX-this.zoom.startX,this.zoom.y=e.clientY-this.zoom.startY,this.applyTransform());else{const e=[...this.activePointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),i=this.pinchStartScale*(t/this.pinchStartDist)-this.zoom.scale;this.handleZoom(i)}}onPointerUp(e){if(this.activePointers.delete(e.pointerId),this.activePointers.size<2&&(this.pinchStartDist=0),!this.zoom.panning&&0===this.activePointers.size){this.swipe.endX=e.clientX,this.swipe.endY=e.clientY;const t=this.swipe.endX-this.swipe.startX;this.swipe.endY,this.swipe.startY;Math.abs(t)>this.zoom.threshold&&(t>0?(console.log("Swipe right"),this.prevElement()):(console.log("Swipe left"),this.nextElement()))}0===this.activePointers.size&&(this.zoom.panning=!1,this.ui.gallery.image.style.cursor=this.zoom.scale>1?"grab":"default")}onWheel(e){if(!e.ctrlKey)return;e.preventDefault();const t=e.deltaY<0?.2:-.2;this.handleZoom(t,e.clientX,e.clientY)}clampPan(){const e=this.ui.gallery.wrap;if(!e)return;const t=e.getBoundingClientRect(),i=Math.min(t.width/1920,t.height/1920),s=1920*i,n=1920*i*this.zoom.scale,o=s*this.zoom.scale,l=t.width-n-32,r=t.height-o-32;this.zoom.x=Math.min(32,Math.max(l,this.zoom.x)),this.zoom.y=Math.min(32,Math.max(r,this.zoom.y))}handleZoom(e,t=null,i=null){const s=this.zoom.scale;let n=s+e;if(n=Math.min(this.zoom.max,Math.max(this.zoom.min,n)),n===s)return;const o=n/s;let l=this.ui.gallery.image.getBoundingClientRect();null!==t&&null!==i||(t=l.left+l.width/2,i=l.top+l.height/2);const r=t-l.left,a=i-l.top;this.zoom.x=(this.zoom.x-r)*o+r,this.zoom.y=(this.zoom.y-a)*o+a,this.zoom.scale=n,this.applyTransform(),this.notify("zoom",{scale:this.zoom.scale})}applyTransform(){const e=this.ui.gallery.image;e.style.transform=`translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`,e.style.cursor=this.zoom.scale>1?"grab":"default"}resetZoom(){this.zoom.scale=1,this.zoom.x=0,this.zoom.y=0,this.zoom.startX=0,this.zoom.startY=0,this.zoom.panning=!1,this.applyTransform()}resetSwipe(){return{startX:null,startY:null,endX:null,endY:null}}toggleGallery(e,t=null){e?(this.ui.gallery.image.draggable=!1,this.ui.gallery.image.style.userSelect="none",this.ui.gallery.image.addEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.addEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.addEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.addEventListener("pointercancel",this.pointerUpHandler),window.addEventListener("wheel",this.wheelHandler,{passive:!1}),window.addEventListener("keydown",this.keyHandler),this.moveIntoView()):(this.ui.gallery.image.removeEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.removeEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.removeEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.removeEventListener("pointercancel",this.pointerUpHandler),window.removeEventListener("wheel",this.wheelHandler),window.removeEventListener("keydown",this.keyHandler),this.resetZoom(),this.resetSwipe(),this.activePointers.clear(),this.lastTap=0),e&&!this.modal.isOpen&&this.modal.handleOpen()}moveIntoView(e=0){let t=this.index+e;t<0?t=this.items.length-1:t>=this.items.length?t=0:t===this.items.length-3&&this.notify("load-more"),this.index=t,this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}`)}nextElement(){this.resetZoom(),this.moveIntoView(1)}prevElement(){this.resetZoom(),this.moveIntoView(-1)}updateDisplay(){const e=this.items[this.index];if(!e)return;const t=this.ui.gallery.image;if(e.srcset&&(t.srcset=e.srcset,t.sizes=e.sizes),t.src=e.src,t.alt=e.alt,e.full&&e.full!==e.src){const i=new Image;i.onload=()=>{this.items[this.index]===e&&(t.src=e.full,t.removeAttribute("srcset"),t.removeAttribute("sizes"))},i.src=e.full}this.ui.gallery.counter.textContent=`${this.index+1} / ${this.items.length}`,this.ui.gallery.prevButton.disabled=this.items.length<=1,this.ui.gallery.nextButton.disabled=this.items.length<=1}preloadAdjacent(){[-1,1].forEach((e=>{const t=this.index+e;if(t>0&&t<this.items.length){const i=this.items[t];(e<0?this.ui.gallery.leftImage:this.ui.gallery.rightImage).src=i.full}}))}initSubscribers(){this.subscribers=new Set}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.toggleGallery(!1),document.removeEventListener("click",this.clickHandler)}}document.addEventListener("DOMContentLoaded",(function(){document.querySelector("dialog.gallery")&&(window.jvbGallery=new e)}))})();
\ No newline at end of file
diff --git a/assets/js/min/index.php b/assets/js/min/index.php
deleted file mode 100644
index e69de29..0000000
--- a/assets/js/min/index.php
+++ /dev/null
diff --git a/assets/js/min/integrations.min.js b/assets/js/min/integrations.min.js
index ac3d085..29c5549 100644
--- a/assets/js/min/integrations.min.js
+++ b/assets/js/min/integrations.min.js
@@ -1 +1 @@
-window.jvbOAuthPopup=function(e,t){const s=(window.screen.width-600)/2,o=(window.screen.height-700)/2;e+=(e.indexOf("?")>-1?"&":"?")+"popup=1",console.log("Opening OAuth popup for",t,"with URL:",e);const n=window.open(e,t+"-oauth",`width=600,height=700,left=${s},top=${o},scrollbars=yes,resizable=yes,toolbar=no,menubar=no`);if(!n)return alert("Please allow popups for this site to complete the authorization process."),!1;window.jvbOAuthComplete=function(e,s,o){if(console.log("OAuth complete:",e,s,o),e===t)if(s){const e=document.querySelector(`.integration-card[data-service="${t}"] .setup .text`);e&&(e.textContent="Connection successful! Refreshing..."),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3)}else alert("OAuth authorization failed: "+(o||"Unknown error")),jvbRefreshIntegration(t)};const i=setInterval((()=>{try{n.closed&&(clearInterval(i),console.log("OAuth popup closed"),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3))}catch(e){}}),1e3);return!1},window.jvbRefreshIntegration=function(e){console.log("Refreshing integration:",e),fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({service:e,action:"check_oauth_status"})}).then((e=>e.json())).then((t=>{if(console.log("OAuth status check result:",t),t.success&&t.authorized){const t=document.querySelector(`.integration-card[data-service="${e}"]`);if(t){t.classList.remove("disconnected"),t.classList.add("connected");const e=t.querySelector(".setup .text");e&&(e.textContent="Connected"),setTimeout((()=>{location.reload()}),1500)}}else location.reload()})).catch((e=>{console.error("Error checking OAuth status:",e),location.reload()}))},window.integrations=new class{constructor(){this.initElements(),this.initListeners(),this.init()}initElements(){this.selectors={form:"form.integration",action:"data-action"};let e=document.querySelectorAll(this.selectors.form);this.forms=new Map,e.forEach((e=>{this.forms.set(e.dataset.service,e)}))}initListeners(){this.handleClick=this.clickHandler.bind(this),this.handleChange=this.changeHandler.bind(this),this.handleSubmit=this.submitHandler.bind(this),document.addEventListener("click",this.handleClick),document.addEventListener("change",this.handleChange),document.addEventListener("submit",this.handleSubmit)}init(){document.addEventListener("DOMContentLoaded",(()=>{this.checkForOAuthMessages()}))}checkForOAuthMessages(){const e=new URLSearchParams(window.location.search),t=e.get("success"),s=e.get("error");t?(this.showNotification(t,"success",5e3),this.cleanURL(),document.querySelectorAll("form.integration").forEach((e=>{this.updateUI(e,"connected")}))):s&&(this.showNotification(s,"error",8e3),this.cleanURL())}cleanURL(){const e=new URL(window.location);e.searchParams.delete("success"),e.searchParams.delete("error"),window.history.replaceState({},document.title,e.pathname+e.hash)}showNotification(e,t="info",s=5e3){let o=document.querySelector(".integration-status-message");if(!o){o=document.createElement("div"),o.className="integration-status-message";const e=document.querySelector(".integration-settings")||document.querySelector("main")||document.body;e.insertBefore(o,e.firstChild)}o.textContent=e,o.className=`integration-status-message ${t}`,this.notificationTimeout&&clearTimeout(this.notificationTimeout),s>0&&(this.notificationTimeout=setTimeout((()=>{o.className="integration-status-message",o.textContent=""}),s)),this.popup&&this.addPopup(e,s)}addPopup(e,t=2e3){this.popup||(this.popup=document.querySelector(".integration-popup")||this.createPopupElement()),this.popup.textContent=e,this.popup.classList.add("showing"),setTimeout((()=>{this.popup.classList.remove("showing")}),t)}createPopupElement(){const e=document.createElement("div");return e.className="integration-popup",document.body.appendChild(e),e}clickHandler(e){if(e.target.closest(this.selectors.form)&&(console.log("Clicked!"),"BUTTON"===e.target.tagName||e.target.closest("button"))){e.preventDefault();let t="BUTTON"===e.target.tagName?e.target:e.target.closest("button");this.handleAction(t)}}changeHandler(e){if(e.target.closest(this.selectors.form))if("action"in e.target.dataset)this.handleAction(e.target);else{let t=this.getFormFromTarget(e.target);if(!t)return;t.classList.add("hasChanges"),t.querySelector(".setup .text").textContent="Unsaved Changes"}}submitHandler(e){e.target.closest(this.selectors.form)&&e.preventDefault()}getFormFromTarget(e){let t=e.closest("form")?.dataset.service;return this.forms.get(t)??!1}handleOAuthClick(e){const t=e.dataset.service,s=e.href,o=(screen.width-600)/2,n=(screen.height-700)/2;this.showNotification("Opening authorization window...","info"),e.classList.add("loading"),e.setAttribute("aria-busy","true");const i=window.open(s,"oauth_"+t,`width=600,height=700,left=${o},top=${n},toolbar=no,menubar=no,location=yes,status=yes,resizable=yes`);if(!i)return this.showNotification("Popup was blocked. Please allow popups and try again.","error"),e.classList.remove("loading"),e.removeAttribute("aria-busy"),!0;i.focus(),this.showNotification("Waiting for authorization...","info");const a=setInterval((()=>{try{i.closed&&(clearInterval(a),e.classList.remove("loading"),e.removeAttribute("aria-busy"),this.showNotification("Checking authorization status...","info"),setTimeout((()=>{this.checkForOAuthMessages(),setTimeout((()=>{const e=new URLSearchParams(window.location.search);e.has("success")||e.has("error")||window.location.reload()}),500)}),500))}catch(e){}}),500);return setTimeout((()=>{clearInterval(a),e.classList.remove("loading"),e.removeAttribute("aria-busy")}),3e5),!1}async handleAction(e){const t=e.closest("form"),s=t.dataset.service,o=e.dataset.action,n="BUTTON"===e.tagName,i=n&&"save_credentials"===o;if(!("confirm"in e.dataset)||confirm(e.dataset.confirm)){this.updateUI(t,"syncing");try{this.updateUI(t,"syncing");const a={service:s,action:o,user_id:jvbSettings.currentUser,data:{}};if(n||(a.data[e.name.replace(s+"_","")]=e.value),i){const e=new FormData(t);for(let[t,o]of e.entries())["service"].includes(t)||t.includes("nonce")||(a.data[t.replace(s+"_","")]=o)}console.log("Sending Data:",a);const r=await fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(a)}),c=await r.json();if(r.ok&&c.success){let e="connected";switch(o){case"clear_credentials":e="disconnected";break;case"save_credentials":this.showNotification("Settings saved successfully","success")}console.log(c),this.updateUI(t,e),c.reload&&setTimeout((()=>{window.location.reload()}),50)}else console.log(c),this.updateUI(t,"error",c.message??""),this.showNotification(c.message||"Operation failed","error")}catch(e){this.updateUI(t,"error"),this.showNotification("Network error: "+e.message,"error"),console.error("API Error:",e)}}}updateUI(e,t,s=""){let o=["connected","disconnected","hasChanges","syncing","error"];if(!o.includes(t))return void console.log("Invalid state: ",t);s=""===s?{connected:"Set Up",disconnected:"Not Set Up",hasChanges:"Unsaved Changes",syncing:"Testing changes",error:"Something went wrong"}[t]:s,"syncing"===t?e.querySelectorAll("button").forEach((e=>{e.disabled=!0})):e.querySelectorAll("button[disabled]").forEach((e=>{e.disabled=!1})),e.classList.remove(...o),e.classList.add(t,"flash"),console.log(e);let n=e.querySelector(".setup .text");console.log(n),n.textContent=s,"syncing"===t?e.querySelectorAll("button").forEach((e=>e.disabled=!0)):e.querySelectorAll("button:disabled").forEach((e=>e.disabled=!1)),setTimeout((()=>e.classList.remove("flash")),600)}};
\ No newline at end of file
+(()=>{class e{constructor(){this.initElements(),this.initListeners(),this.init()}initElements(){this.selectors={form:"form.integration",action:"data-action"};let e=document.querySelectorAll(this.selectors.form);this.forms=new Map,e.forEach((e=>{this.forms.set(e.dataset.service,e)}))}initListeners(){this.handleClick=this.clickHandler.bind(this),this.handleChange=this.changeHandler.bind(this),this.handleSubmit=this.submitHandler.bind(this),document.addEventListener("click",this.handleClick),document.addEventListener("change",this.handleChange),document.addEventListener("submit",this.handleSubmit)}init(){document.addEventListener("DOMContentLoaded",(()=>{this.checkForOAuthMessages()}))}checkForOAuthMessages(){const e=new URLSearchParams(window.location.search),t=e.get("success"),s=e.get("error");if(t){this.showNotification(t,"success",5e3),this.cleanURL();document.querySelectorAll("form.integration").forEach((e=>{this.updateUI(e,"connected")}))}else s&&(this.showNotification(s,"error",8e3),this.cleanURL())}cleanURL(){const e=new URL(window.location);e.searchParams.delete("success"),e.searchParams.delete("error"),window.history.replaceState({},document.title,e.pathname+e.hash)}showNotification(e,t="info",s=5e3){let o=document.querySelector(".integration-status-message");if(!o){o=document.createElement("div"),o.className="integration-status-message";const e=document.querySelector(".integration-settings")||document.querySelector("main")||document.body;e.insertBefore(o,e.firstChild)}o.textContent=e,o.className=`integration-status-message ${t}`,this.notificationTimeout&&clearTimeout(this.notificationTimeout),s>0&&(this.notificationTimeout=setTimeout((()=>{o.className="integration-status-message",o.textContent=""}),s)),this.popup&&this.addPopup(e,s)}addPopup(e,t=2e3){this.popup||(this.popup=document.querySelector(".integration-popup")||this.createPopupElement()),this.popup.textContent=e,this.popup.classList.add("showing"),setTimeout((()=>{this.popup.classList.remove("showing")}),t)}createPopupElement(){const e=document.createElement("div");return e.className="integration-popup",document.body.appendChild(e),e}clickHandler(e){if(e.target.closest(this.selectors.form)&&("BUTTON"===e.target.tagName||e.target.closest("button"))){e.preventDefault();let t="BUTTON"===e.target.tagName?e.target:e.target.closest("button");this.handleAction(t)}}changeHandler(e){if(e.target.closest(this.selectors.form))if("action"in e.target.dataset)this.handleAction(e.target);else{let t=this.getFormFromTarget(e.target);if(!t)return;t.classList.add("hasChanges"),t.querySelector(".setup .text").textContent="Unsaved Changes"}}submitHandler(e){e.target.closest(this.selectors.form)&&e.preventDefault()}getFormFromTarget(e){let t=e.closest("form")?.dataset.service;return this.forms.get(t)??!1}handleOAuthClick(e){const t=e.dataset.service,s=e.href,o=(screen.width-600)/2,i=(screen.height-700)/2;this.showNotification("Opening authorization window...","info"),e.classList.add("loading"),e.setAttribute("aria-busy","true");const n=window.open(s,"oauth_"+t,`width=600,height=700,left=${o},top=${i},toolbar=no,menubar=no,location=yes,status=yes,resizable=yes`);if(!n)return this.showNotification("Popup was blocked. Please allow popups and try again.","error"),e.classList.remove("loading"),e.removeAttribute("aria-busy"),!0;n.focus(),this.showNotification("Waiting for authorization...","info");const a=setInterval((()=>{try{n.closed&&(clearInterval(a),e.classList.remove("loading"),e.removeAttribute("aria-busy"),this.showNotification("Checking authorization status...","info"),setTimeout((()=>{this.checkForOAuthMessages(),setTimeout((()=>{const e=new URLSearchParams(window.location.search);e.has("success")||e.has("error")||window.location.reload()}),500)}),500))}catch(e){}}),500);return setTimeout((()=>{clearInterval(a),e.classList.remove("loading"),e.removeAttribute("aria-busy")}),3e5),!1}async handleAction(e){const t=e.closest("form"),s=t.dataset.service,o=e.dataset.action,i="BUTTON"===e.tagName,n=i&&"save_credentials"===o;if(!("confirm"in e.dataset)||confirm(e.dataset.confirm)){this.updateUI(t,"syncing");try{this.updateUI(t,"syncing");const a={service:s,action:o,user_id:window.auth.getUser(),data:{}};if(i||(a.data[e.name.replace(s+"_","")]=e.value),n){const e=new FormData(t);for(let[t,o]of e.entries())["service"].includes(t)||t.includes("nonce")||(a.data[t.replace(s+"_","")]=o)}const r=await fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(a)}),c=await r.json();if(r.ok&&c.success){let e="connected";switch(o){case"clear_credentials":e="disconnected";break;case"save_credentials":this.showNotification("Settings saved successfully","success")}this.updateUI(t,e),c.reload&&setTimeout((()=>{window.location.reload()}),50)}else this.updateUI(t,"error",c.message??""),this.showNotification(c.message||"Operation failed","error")}catch(e){this.updateUI(t,"error"),this.showNotification("Network error: "+e.message,"error"),console.error("API Error:",e)}}}updateUI(e,t,s=""){let o=["connected","disconnected","hasChanges","syncing","error"];if(!o.includes(t))return;s=""===s?{connected:"Set Up",disconnected:"Not Set Up",hasChanges:"Unsaved Changes",syncing:"Testing changes",error:"Something went wrong"}[t]:s,"syncing"===t?e.querySelectorAll("button").forEach((e=>{e.disabled=!0})):e.querySelectorAll("button[disabled]").forEach((e=>{e.disabled=!1})),e.classList.remove(...o),e.classList.add(t,"flash"),e.querySelector(".setup .text").textContent=s,"syncing"===t?e.querySelectorAll("button").forEach((e=>e.disabled=!0)):e.querySelectorAll("button:disabled").forEach((e=>e.disabled=!1)),setTimeout((()=>e.classList.remove("flash")),600)}}window.jvbOAuthPopup=function(e,t){const s=(window.screen.width-600)/2,o=(window.screen.height-700)/2;e+=(e.indexOf("?")>-1?"&":"?")+"popup=1";const i=window.open(e,t+"-oauth",`width=600,height=700,left=${s},top=${o},scrollbars=yes,resizable=yes,toolbar=no,menubar=no`);if(!i)return alert("Please allow popups for this site to complete the authorization process."),!1;window.jvbOAuthComplete=function(e,s,o){if(e===t)if(s){const e=document.querySelector(`.integration-card[data-service="${t}"] .setup .text`);e&&(e.textContent="Connection successful! Refreshing..."),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3)}else alert("OAuth authorization failed: "+(o||"Unknown error")),jvbRefreshIntegration(t)};const n=setInterval((()=>{try{i.closed&&(clearInterval(n),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3))}catch(e){}}),1e3);return!1},window.jvbRefreshIntegration=function(e){fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({service:e,action:"check_oauth_status"})}).then((e=>e.json())).then((t=>{if(console.log("OAuth status check result:",t),t.success&&t.authorized){const t=document.querySelector(`.integration-card[data-service="${e}"]`);if(t){t.classList.remove("disconnected"),t.classList.add("connected");const e=t.querySelector(".setup .text");e&&(e.textContent="Connected"),setTimeout((()=>{location.reload()}),1500)}}else location.reload()})).catch((e=>{console.error("Error checking OAuth status:",e),location.reload()}))},document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.integrations=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/interactions.min.js b/assets/js/min/interactions.min.js
new file mode 100644
index 0000000..d7f1bed
--- /dev/null
+++ b/assets/js/min/interactions.min.js
@@ -0,0 +1 @@
+(()=>{function t(){window.auth.getUser()&&(window.jvbInteractions=new FrontendInteractions)}"requestIdleCallback"in window?requestIdleCallback((async function(){window.auth.subscribe((n=>{"auth-loaded"===n&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t())}))})):"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):setTimeout(t,1),window.toggleFavourite=function(t){window.jvbInteractions?window.jvbInteractions.toggleFavourite(t):console.warn("FrontendInteractions not initialized")},window.handleVote=function(t){window.jvbInteractions?window.jvbInteractions.handleVote(t):console.warn("FrontendInteractions not initialized")},window.isFavourited=function(t,n){return!!window.jvbInteractions&&window.jvbInteractions.isFavourited(t,n)},window.checkVoteStatus=function(t,n){return window.jvbInteractions?window.jvbInteractions.checkVoteStatus(t,n):""},window.formatVote=function(t,n){let e=window.getTemplate("voteButton");e.dataset.itemId=t.id,e.dataset.content=t.content;let o=e.querySelector("button.up"),i=e.querySelector("button.down");return"up"===n&&o.classList.add("voted"),"down"===n&&i.classList.add("voted"),t.upvotes>0&&(o.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(i.querySelector(".count").textContent="-"+t.downvotes),e},window.checkVoteStatus=function(t,n){if(!window.auth.getUser())return"";let e="";return window.userVotes&&window.userVotes[t]?.has(n)&&(e=window.userVotes[t].get(n)),e}})();
\ No newline at end of file
diff --git a/assets/js/min/loading.min.js b/assets/js/min/loading.min.js
deleted file mode 100644
index 2cd03f6..0000000
--- a/assets/js/min/loading.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{class e{constructor(){this.quips={logo:["Fetching items..."],uploading:["Sending to server..."],...JSON.parse(loadingQuips.quips)},this.isLoading=!1,this.overlay=document.querySelector(".loading-overlay"),this.overlay&&(this.overlayMessage=this.overlay.querySelector(".message"),this.overlayTitle=this.overlay.querySelector("h3"),this.overlayIcon=this.overlay.querySelector("div.icon"),this.quipInterval=null,this.activeLoads=0,this.currentQuips=this.defaultQuips)}showLoading(e="",t="Loading",s=null){this.isLoading=!0,this.activeLoads++,this.overlayTitle.textContent=t,this.currentQuips=s||this.defaultQuips,e&&(this.overlayMessage.textContent=e),this.overlay.classList.add("active"),document.body.classList.add("loading"),document.body.style.overflow="hidden",this.startQuipCycle()}hideLoading(){this.activeLoads--,this.activeLoads<=0&&(this.activeLoads=0,this.overlay.classList.remove("active"),document.body.style.overflow="",document.body.classList.remove("loading"),this.stopQuipCycle()),this.isLoading=!1}setContent(e){console.log(e);let t=Object.keys(this.quips);e=0===(e=t.map((s=>!!t.includes(e)&&e)).filter(Boolean)).length?["logo"]:e,this.currentContent=e}startQuipCycle(){this.quipInterval&&clearInterval(this.quipInterval);let e={};this.currentContent.forEach((t=>{this.quips[t].forEach((s=>{e[s]=t}))}));let t=this.shuffleArray(Object.keys(e)),s=0;this.overlayMessage.textContent=t[0],this.overlayMessage.classList.remove("changing");let o="",i="";this.quipInterval=setInterval((()=>{this.overlayMessage.classList.add("changing"),setTimeout((()=>{s=(s+1)%t.length,o=e[t[s]],o!==i&&(window.removeChildren(this.overlayIcon),this.overlayIcon.append(window.getIcon(o))),window.typeText(this.overlayMessage,t[s]),this.overlayMessage.classList.remove("changing"),i=o}),300)}),2e3)}stopQuipCycle(){this.quipInterval&&(clearInterval(this.quipInterval),this.quipInterval=null)}shuffleArray(e){for(let t=e.length-1;t>0;t--){const s=Math.floor(Math.random()*(t+1));[e[t],e[s]]=[e[s],e[t]]}return e}startAutosaving(){document.body.classList.add("autosaving")}stopAutosaving(e="Saved!"){document.body.classList.remove("autosaving");const t=document.querySelector(".save-popup");t.classList.add("show"),"Saved!"!==e&&(t.innerText=e),setTimeout((()=>{t.classList.remove("show")}),1500)}showError(e){this.overlayTitle.textContent="Error",this.overlayMessage.textContent=e,this.overlay.classList.add("active","error"),document.body.style.overflow="hidden",setTimeout((()=>{this.overlay.classList.remove("active","error"),document.body.style.overflow=""}),3e3)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbLoading=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/media.min.js b/assets/js/min/media.min.js
deleted file mode 100644
index 71ef1d6..0000000
--- a/assets/js/min/media.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{class e{constructor(){this.currentWidth=window.innerWidth,this.images=document.querySelectorAll(".wp-site-blocks img[data-small]"),0!==this.images.length&&(this.loadVisibleImages(),this.initListeners())}loadVisibleImages(){this.images.forEach(((e,t)=>{const i=e.getBoundingClientRect(),s=i.top<window.innerHeight&&i.bottom>0;(0===t||s)&&(this.loadAppropriateImage(e),e.dataset.loaded="true")}))}initListeners(){this.resizeHandler=this.handleResize.bind(this),window.addEventListener("resize",this.resizeHandler),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&(this.loadAppropriateImage(e.target),this.observer.unobserve(e.target))}))}),{rootMargin:"50px",threshold:.1}),this.images.forEach((e=>{e.dataset.loaded||this.observer.observe(e)}))}handleResize(){window.debouncer.schedule("image-resize",(()=>{const e=window.innerWidth;Math.abs(e-this.currentWidth)>100&&(this.currentWidth=e,this.updateVisibleImages())}),150)}updateVisibleImages(){this.images.forEach((e=>{const t=e.getBoundingClientRect();t.top<window.innerHeight&&t.bottom>0&&this.loadAppropriateImage(e,!0)}))}loadAppropriateImage(e,t=!1){const i=this.getTargetSize(),s=e.dataset[i];s&&(t||s!==e.currentSrc)&&(e.src=s)}getTargetSize(){return this.currentWidth<768?"medium":this.currentWidth<1200?"large":"full"}cleanup(){this.observer?.disconnect(),window.removeEventListener("resize",this.resizeHandler)}}window.isLoaded=!1,document.addEventListener("readystatechange",(()=>{!window.isLoaded&&document.querySelector(".wp-site-blocks img")&&(window.jvbMedia=new e,window.isLoaded=!0)}))})();
\ No newline at end of file
diff --git a/assets/js/min/navigation.min.js b/assets/js/min/navigation.min.js
index cd96cb0..d5ff0b8 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++}))}navIDs(){return Array.from(this.navs.keys()).map((e=>`#${e}`))}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;if(this.openNav&&!e.target.closest(this.openNav)&&this.toggleNav(!1),!e.target.closest(...this.navIDs()))return;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){console.log(e.target);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){console.log(e.target);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),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++}))}navIDs(){return Array.from(this.navs.keys()).map((e=>`#${e}`))}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
diff --git a/assets/js/min/news.min.js b/assets/js/min/news.min.js
index 272456b..7b83a86 100644
--- a/assets/js/min/news.min.js
+++ b/assets/js/min/news.min.js
@@ -1 +1 @@
-window.newsManager=class{constructor(){this.queue=window.jvbQueue,this.loading=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.activeTab="all",this.tabs=new window.jvbTabs(document.querySelector(".replace"),{news:()=>{this.activeTab="all",this.resetFilters(),this.loadItems(!0).then((()=>{}))},mine:()=>{console.log("switching to mine tab"),this.activeTab="own",this.resetFilters(),this.filters.artist=jvbSettings.currentUser,this.loadItems(!0).then((()=>{}))},watching:()=>{this.activeTab="watching",this.resetFilters(),this.filters.watched=!0,this.loadItems(!0).then((()=>{}))}}),this.isLoading=!1,this.alreadyHandling=!1,this.template=new Map,this.endpoints={news:"news",vote:"news/vote"},this.resetFilters(),this.state={hasMore:!0,pages:1,items:0},this.initElements(),this.initEvents(),this.loadItems()}resetFilters(){this.filters={page:1,order:"DESC",orderby:"date",shop:null,type:null,artist:null,watched:!1}}initElements(){this.container=document.querySelector(".replace"),this.grid=this.container.querySelector(".item-grid"),this.addButton=this.container.querySelector(".add-item-btn"),this.addModal=new window.jvbModal(this.container.querySelector(".create-modal"),{render:this.renderModal.bind(this),open:this.addButton,content:"news",openMessage:"Opened modal to create a news post.",onSave:this.saveModal.bind(this)}),this.filterForm=this.container.querySelector("form"),this.dateRangeFilter=new window.jvbModal(this.container.querySelector("dialog.date-range"),{open:!1}),this.clearFilters=this.container.querySelector(".clear-filters"),this.replyModal=new window.jvbModal(this.container.querySelector(".create-response"),{open:!1,content:"response",openMessage:"Opened Response modal",onSave:this.saveCreatedResponse.bind(this)})}initEvents(){this.filterForm.addEventListener("change",(e=>{let t=e.target.value;if(!e.target.closest(".date-range"))if("custom"===t)this.handleCustomDateRange();else{let s=e.target.name;s?this.filters[s]=t:this.resetFilters(),this.loadItems(!0)}})),document.addEventListener("click",(e=>{if(e.target===this.clearFilters&&(this.filterForm.reset(),this.resetFilters(),this.loadItems(!0)),e.target.closest("button.reply")){let t=e.target.closest("button"),s=t.closest(".item").dataset.id,n="";"news"===t.dataset.type?n=t.closest(".item").querySelector(".item-info").innerHTML:(n=t.closest(".response").querySelector(".content").innerHTML,this.replyModal.modal.dataset.parent_id=t.id.replace("reply-to","")),this.replyModal.modal.dataset.id=s,this.replyModal.modal.dataset.type=t.dataset.type,this.replyModal.modal.querySelector(".original").innerHTML="<h5>Replying to:</h5>"+n,this.replyModal.handleOpen()}}))}renderModal(){}handleCustomDateRange(){this.dateRangeFilter.handleOpen();let e=this.dateRangeFilter.modal.querySelector("input.date-start"),t=this.dateRangeFilter.modal.querySelector("input.date-end"),s=this.dateRangeFilter.modal.querySelector("select");this.dateRangeFilter.modal.querySelectorAll("input, select").forEach((n=>{n.addEventListener("change",(i=>{n===e&&""!==t.value||n===t&&""!==e.value?(this.filters.dateFrom=e.value,this.filters.dateTo=t.value,this.dateRangeFilter.handleClose(),this.loadItems(!0)):n===s&&(this.filters.customDate=s.value,this.dateRangeFilter.handleClose(),this.loadItems(!0))}))}))}async saveModal(e){const t=new FormData(this.addModal.modal.querySelector("form"));t.append("user",jvbSettings.currentUser),this.queue.addToQueue({type:"new_news",data:t})}async loadItems(e=!0){if(!this.isLoading)try{this.isLoading=!0,this.loading.show(),e&&(this.filters.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const t=this.buildFilters(),s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.endpoints.news}?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash}},{context:"news",forceRefresh:!0});return this.renderItems(s.items||[],this.filters.page>1),s.pagination&&(this.state={hasMore:s.has_more,items:s.items,pages:s.pages}),s}catch(e){throw this.handleError(e,"loading news"),e}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const e=JSON.parse(JSON.stringify(this.filters));let t={};for(var[s,n]of Object.entries(e))!1!==n&&null!==n&&(t[s]=n);return new URLSearchParams(t)}renderItems(e,t=!1){if(t||removeChildren(this.grid),0===e.length)return this.a11y.announceItems(0,t),void this.showEmptyState();const s=document.createDocumentFragment(),n=i=>{const a=Math.min(i+10,e.length);for(let t=i;t<a;t++){const n=e[t],i=this.createItemElement(n);s.appendChild(i)}a<e.length?requestAnimationFrame((()=>{n(a)})):(this.grid.appendChild(s),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.state.hasMore))};e.length>0?n(0):this.a11y.announceItems(0,t)}createItemElement(e){const t=window.getTemplate(`template-${this.activeTab}`);t.id=`news-${e.id}`,t.dataset.id=e.id;const[s]=t.getElementsByTagName("h3"),[n]=t.getElementsByClassName("published"),[i]=t.getElementsByClassName("artist"),[a]=t.getElementsByClassName("shop"),[r]=t.getElementsByClassName("tldr"),[o]=t.getElementsByClassName("item-info"),[l]=t.getElementsByClassName("image");[s.textContent,n.textContent,i.href,i.textContent,r.textContent,o.innerHTML]=[e.title,formatTimeAgo(e.date),e.artist.url,e.artist.name,e.tldr,e.post_content],e.shop?[a.href,a.innerHTML]=[e.shop.url,jvbSettings.icons.shop+e.shop.name]:a.hidden=!0;const[d]=t.getElementsByClassName("favourite-button");if("own"!==this.activeTab)[d.dataset.id,d.dataset.artist]=[e.id,e.artist.id],window.userFavourites.news?.includes(parseInt(e.id))?(removeChildren(d),d.append(getIcon("star-fi"))):(removeChildren(d),d.append(getIcon("star")));else{d.hidden=!0;const[s]=t.getElementsByClassName("select-checkbox"),[n]=t.getElementsByTagName("label");[s.id,s.value,n.for]=[`item-${e.id}`,e.id,`item-${e.id}`]}let h="";window.userVotes?.news?.has(e.id)&&(h=window.userVotes.news.get(e.id)),console.log(e),t.querySelector(".summary").appendChild(formatVote(e,h));let c=window.getTemplate("commentsButton");c.href=`#responses-to-${e.id}`,c.querySelector(".count").textContent=e.comments.items.length;let m=window.getTemplate("responses");m.id=`responses-to-${e.id}`,m.querySelector("summary").textContent+=" { "+e.comments.items.length+" }";let p=window.getTemplate("replyButton");return p.id="reply-to-"+e.id,p.dataset.type="news",p.dataset.action="reply",t.appendChild(p),e.comments.items.length>0&&e.comments.items.forEach((e=>{m.appendChild(this.formatComment(e))})),t.appendChild(m),t.querySelector(".vote").prepend(c,t.querySelector(".vote button")),e.image&&e.image.replace(/src="([^"]+)"/,'data-src="$1"'),t}formatComment(e,t=null){let s=window.getTemplate("response");s.id="response-"+e.id;let n=s.querySelector("summary");n.querySelector(".content").innerHTML=e.response,n.querySelector(".created").textContent=formatTimeAgo(e.created_at);let i=checkVoteStatus("response",e.id);e.content="response",s.querySelector(".footer").appendChild(formatVote(e,i)),console.log(e);let a=window.getTemplate("replyButton");a.id="reply-to-"+e.id,t&&(a.dataset.parent_id=t),a.dataset.action="reply",a.dataset.type=e.content,n.querySelector(".vote").prepend(a,n.querySelector(".vote").firstElementChild);let r=n.querySelector(".artist"),o=n.querySelector(".shop");if(console.log(e),e.artist?(e.artist.shop||o.remove(),[r.href,r.textContent,o.href,o.textContent]=[e.artist.url,e.artist.name,e.artist.shop.url,e.artist.shop.name]):(r.remove(),o.remove()),e.children.items.length>0){let t=window.getTemplate("responses");t.id="replies-to-"+e.id,t.querySelector("summary").textContent="See Responses {"+e.children.items.length+"}",e.children.items.forEach((s=>{t.appendChild(this.formatComment(s,e.id))})),s.appendChild(t)}return s}renderResponseCreate(){}saveCreatedResponse(){console.log("Saving create response"),console.log(this.replyModal.modal.id);const e=this.replyModal.modal;let t={user:jvbSettings.currentUser,item_id:e.dataset.id,response:e.querySelector(".ql-editor").innerHTML,content:e.dataset.type,action:"create"};e.dataset.parent_id&&(t.parent_id=e.dataset.parent_id),console.log(t),this.queue.addToQueue({type:"new_response",data:t})}showEmptyState(){const e=document.createElement("div");e.className="no-news",e.innerHTML="\n            <h3>Nothing here</h3>\n            <p>No updates here.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.appendChild(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){let e=this.grid.querySelector(".no-news");e&&e.remove()}handleError(e,t){console.error(`News error (${t}):`,e),window.jvbError&&window.jvbError.log(e,{component:"NewsManager",action:t}),window.jvbA11y&&window.jvbA11y.announce(`Error ${t}. ${e.message||"Please try again."}`)}};
\ No newline at end of file
+window.newsManager=class{constructor(){this.queue=window.jvbQueue,this.loading=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.activeTab="all",this.tabs=new window.jvbTabs(document.querySelector(".replace"),{news:()=>{this.activeTab="all",this.resetFilters(),this.loadItems(!0).then((()=>{}))},mine:()=>{console.log("switching to mine tab"),this.activeTab="own",this.resetFilters(),this.filters.artist=window.auth.getUser(),this.loadItems(!0).then((()=>{}))},watching:()=>{this.activeTab="watching",this.resetFilters(),this.filters.watched=!0,this.loadItems(!0).then((()=>{}))}}),this.isLoading=!1,this.alreadyHandling=!1,this.template=new Map,this.endpoints={news:"news",vote:"news/vote"},this.resetFilters(),this.state={hasMore:!0,pages:1,items:0},this.initElements(),this.initEvents(),this.loadItems()}resetFilters(){this.filters={page:1,order:"DESC",orderby:"date",shop:null,type:null,artist:null,watched:!1}}initElements(){this.container=document.querySelector(".replace"),this.grid=this.container.querySelector(".item-grid"),this.addButton=this.container.querySelector(".add-item-btn"),this.addModal=new window.jvbModal(this.container.querySelector(".create-modal"),{render:this.renderModal.bind(this),open:this.addButton,content:"news",openMessage:"Opened modal to create a news post.",onSave:this.saveModal.bind(this)}),this.filterForm=this.container.querySelector("form"),this.dateRangeFilter=new window.jvbModal(this.container.querySelector("dialog.date-range"),{open:!1}),this.clearFilters=this.container.querySelector(".clear-filters"),this.replyModal=new window.jvbModal(this.container.querySelector(".create-response"),{open:!1,content:"response",openMessage:"Opened Response modal",onSave:this.saveCreatedResponse.bind(this)})}initEvents(){this.filterForm.addEventListener("change",(e=>{let t=e.target.value;if(!e.target.closest(".date-range"))if("custom"===t)this.handleCustomDateRange();else{let s=e.target.name;s?this.filters[s]=t:this.resetFilters(),this.loadItems(!0)}})),document.addEventListener("click",(e=>{if(e.target===this.clearFilters&&(this.filterForm.reset(),this.resetFilters(),this.loadItems(!0)),e.target.closest("button.reply")){let t=e.target.closest("button"),s=t.closest(".item").dataset.id,n="";"news"===t.dataset.type?n=t.closest(".item").querySelector(".item-info").innerHTML:(n=t.closest(".response").querySelector(".content").innerHTML,this.replyModal.modal.dataset.parent_id=t.id.replace("reply-to","")),this.replyModal.modal.dataset.id=s,this.replyModal.modal.dataset.type=t.dataset.type,this.replyModal.modal.querySelector(".original").innerHTML="<h5>Replying to:</h5>"+n,this.replyModal.handleOpen()}}))}renderModal(){}handleCustomDateRange(){this.dateRangeFilter.handleOpen();let e=this.dateRangeFilter.modal.querySelector("input.date-start"),t=this.dateRangeFilter.modal.querySelector("input.date-end"),s=this.dateRangeFilter.modal.querySelector("select");this.dateRangeFilter.modal.querySelectorAll("input, select").forEach((n=>{n.addEventListener("change",(i=>{n===e&&""!==t.value||n===t&&""!==e.value?(this.filters.dateFrom=e.value,this.filters.dateTo=t.value,this.dateRangeFilter.handleClose(),this.loadItems(!0)):n===s&&(this.filters.customDate=s.value,this.dateRangeFilter.handleClose(),this.loadItems(!0))}))}))}async saveModal(e){const t=new FormData(this.addModal.modal.querySelector("form"));t.append("user",window.auth.getUser()),this.queue.addToQueue({type:"new_news",data:t})}async loadItems(e=!0){if(!this.isLoading)try{this.isLoading=!0,this.loading.show(),e&&(this.filters.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const t=this.buildFilters(),s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.endpoints.news}?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("dash")}},{context:"news",forceRefresh:!0});return this.renderItems(s.items||[],this.filters.page>1),s.pagination&&(this.state={hasMore:s.has_more,items:s.items,pages:s.pages}),s}catch(e){throw this.handleError(e,"loading news"),e}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const e=JSON.parse(JSON.stringify(this.filters));let t={};for(var[s,n]of Object.entries(e))!1!==n&&null!==n&&(t[s]=n);return new URLSearchParams(t)}renderItems(e,t=!1){if(t||removeChildren(this.grid),0===e.length)return this.a11y.announceItems(0,t),void this.showEmptyState();const s=document.createDocumentFragment(),n=i=>{const a=Math.min(i+10,e.length);for(let t=i;t<a;t++){const n=e[t],i=this.createItemElement(n);s.appendChild(i)}a<e.length?requestAnimationFrame((()=>{n(a)})):(this.grid.appendChild(s),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.state.hasMore))};e.length>0?n(0):this.a11y.announceItems(0,t)}createItemElement(e){const t=window.getTemplate(`template-${this.activeTab}`);t.id=`news-${e.id}`,t.dataset.id=e.id;const[s]=t.getElementsByTagName("h3"),[n]=t.getElementsByClassName("published"),[i]=t.getElementsByClassName("artist"),[a]=t.getElementsByClassName("shop"),[o]=t.getElementsByClassName("tldr"),[r]=t.getElementsByClassName("item-info"),[l]=t.getElementsByClassName("image");[s.textContent,n.textContent,i.href,i.textContent,o.textContent,r.innerHTML]=[e.title,formatTimeAgo(e.date),e.artist.url,e.artist.name,e.tldr,e.post_content],e.shop?[a.href,a.innerHTML]=[e.shop.url,jvbSettings.icons.shop+e.shop.name]:a.hidden=!0;const[d]=t.getElementsByClassName("favourite-button");if("own"!==this.activeTab)[d.dataset.id,d.dataset.artist]=[e.id,e.artist.id],window.userFavourites.news?.includes(parseInt(e.id))?(removeChildren(d),d.append(getIcon("star-fi"))):(removeChildren(d),d.append(getIcon("star")));else{d.hidden=!0;const[s]=t.getElementsByClassName("select-checkbox"),[n]=t.getElementsByTagName("label");[s.id,s.value,n.for]=[`item-${e.id}`,e.id,`item-${e.id}`]}let h="";window.userVotes?.news?.has(e.id)&&(h=window.userVotes.news.get(e.id)),console.log(e),t.querySelector(".summary").appendChild(formatVote(e,h));let c=window.getTemplate("commentsButton");c.href=`#responses-to-${e.id}`,c.querySelector(".count").textContent=e.comments.items.length;let m=window.getTemplate("responses");m.id=`responses-to-${e.id}`,m.querySelector("summary").textContent+=" { "+e.comments.items.length+" }";let p=window.getTemplate("replyButton");return p.id="reply-to-"+e.id,p.dataset.type="news",p.dataset.action="reply",t.appendChild(p),e.comments.items.length>0&&e.comments.items.forEach((e=>{m.appendChild(this.formatComment(e))})),t.appendChild(m),t.querySelector(".vote").prepend(c,t.querySelector(".vote button")),e.image&&e.image.replace(/src="([^"]+)"/,'data-src="$1"'),t}formatComment(e,t=null){let s=window.getTemplate("response");s.id="response-"+e.id;let n=s.querySelector("summary");n.querySelector(".content").innerHTML=e.response,n.querySelector(".created").textContent=formatTimeAgo(e.created_at);let i=checkVoteStatus("response",e.id);e.content="response",s.querySelector(".footer").appendChild(formatVote(e,i)),console.log(e);let a=window.getTemplate("replyButton");a.id="reply-to-"+e.id,t&&(a.dataset.parent_id=t),a.dataset.action="reply",a.dataset.type=e.content,n.querySelector(".vote").prepend(a,n.querySelector(".vote").firstElementChild);let o=n.querySelector(".artist"),r=n.querySelector(".shop");if(console.log(e),e.artist?(e.artist.shop||r.remove(),[o.href,o.textContent,r.href,r.textContent]=[e.artist.url,e.artist.name,e.artist.shop.url,e.artist.shop.name]):(o.remove(),r.remove()),e.children.items.length>0){let t=window.getTemplate("responses");t.id="replies-to-"+e.id,t.querySelector("summary").textContent="See Responses {"+e.children.items.length+"}",e.children.items.forEach((s=>{t.appendChild(this.formatComment(s,e.id))})),s.appendChild(t)}return s}renderResponseCreate(){}saveCreatedResponse(){console.log("Saving create response"),console.log(this.replyModal.modal.id);const e=this.replyModal.modal;let t={user:window.auth.getUser(),item_id:e.dataset.id,response:e.querySelector(".ql-editor").innerHTML,content:e.dataset.type,action:"create"};e.dataset.parent_id&&(t.parent_id=e.dataset.parent_id),console.log(t),this.queue.addToQueue({type:"new_response",data:t})}showEmptyState(){const e=document.createElement("div");e.className="no-news",e.innerHTML="\n            <h3>Nothing here</h3>\n            <p>No updates here.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.appendChild(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){let e=this.grid.querySelector(".no-news");e&&e.remove()}handleError(e,t){console.error(`News error (${t}):`,e),window.jvbError&&window.jvbError.log(e,{component:"NewsManager",action:t}),window.jvbA11y&&window.jvbA11y.announce(`Error ${t}. ${e.message||"Please try again."}`)}};
\ No newline at end of file
diff --git a/assets/js/min/notificationManager.min.js b/assets/js/min/notificationManager.min.js
index 86c2a44..1c45d3a 100644
--- a/assets/js/min/notificationManager.min.js
+++ b/assets/js/min/notificationManager.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(){this.resetFilters(),this.activeTab="all",this.isLoading=!1,this.loading=window.jvbLoading,this.container=document.querySelector(".container"),this.grid=this.container.querySelector(".notifications-list"),this.tabs=new window.jvbTabs(this.container,{all:()=>{this.resetFilters(),this.activeTab="all",this.loadNotifications()},favourite:()=>{this.resetFilters(),this.activeTab="favourite",this.filters.content="favourite",this.loadNotifications()},artist:()=>{this.resetFilters(),this.activeTab="artist",this.filters.content="artist",this.loadNotifications()},shop:()=>{this.resetFilters(),this.activeTab="shop",this.filters.content="shop",this.loadNotifications()},event:()=>{this.resetFilters(),this.activeTab="favourite",this.filters.content="favourite",this.loadNotifications()},news:()=>{this.resetFilters(),this.activeTab="news",this.filters.content="news",this.loadNotifications()},system:()=>{this.resetFilters(),this.activeTab="system",this.filters.content="system",this.loadNotifications()}}),this.loadNotifications()}resetFilters(){this.filters={content:"all",date:""},this.hasMore=!0}async loadNotifications(t=!0){if(!this.isLoading&&this.hasMore)try{this.isLoading=!0,this.loading.show(),t&&(this.filters.page=1,this.grid.classList.remove("empty"));const i=this.buildFilters();console.log(this.filters),console.log("Reset? ",this.reset);const s=await this.cache.fetchWithCache(`${jvbSettings.api}notifications?${i.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbAdmin.nonce}},{context:"admin",forceRefresh:!0});return console.log(s),s}catch(t){throw this.handleError(t,"loading notifications"),t}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const t=JSON.parse(JSON.stringify(this.filters));let i={};for(var[s,e]of Object.entries(t))!1!==e&&null!==e&&(i[s]=e);return i.context="admin",i.user=jvbSettings.currentUser,new URLSearchParams(i)}}document.addEventListener("DOMContentLoaded",(()=>{window.notificationsDash=new t,console.log(jvbSettings)}))})();
\ No newline at end of file
+(()=>{class t{constructor(){this.resetFilters(),this.activeTab="all",this.isLoading=!1,this.loading=window.jvbLoading,this.container=document.querySelector(".container"),this.grid=this.container.querySelector(".notifications-list"),this.tabs=new window.jvbTabs(this.container,{all:()=>{this.resetFilters(),this.activeTab="all",this.loadNotifications()},favourite:()=>{this.resetFilters(),this.activeTab="favourite",this.filters.content="favourite",this.loadNotifications()},artist:()=>{this.resetFilters(),this.activeTab="artist",this.filters.content="artist",this.loadNotifications()},shop:()=>{this.resetFilters(),this.activeTab="shop",this.filters.content="shop",this.loadNotifications()},event:()=>{this.resetFilters(),this.activeTab="favourite",this.filters.content="favourite",this.loadNotifications()},news:()=>{this.resetFilters(),this.activeTab="news",this.filters.content="news",this.loadNotifications()},system:()=>{this.resetFilters(),this.activeTab="system",this.filters.content="system",this.loadNotifications()}}),this.loadNotifications()}resetFilters(){this.filters={content:"all",date:""},this.hasMore=!0}async loadNotifications(t=!0){if(!this.isLoading&&this.hasMore)try{this.isLoading=!0,this.loading.show(),t&&(this.filters.page=1,this.grid.classList.remove("empty"));const i=this.buildFilters();console.log(this.filters),console.log("Reset? ",this.reset);const s=await this.cache.fetchWithCache(`${jvbSettings.api}notifications?${i.toString()}`,{method:"GET",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:jvbAdmin.nonce}},{context:"admin",forceRefresh:!0});return console.log(s),s}catch(t){throw this.handleError(t,"loading notifications"),t}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const t=JSON.parse(JSON.stringify(this.filters));let i={};for(var[s,e]of Object.entries(t))!1!==e&&null!==e&&(i[s]=e);return i.context="admin",i.user=window.auth.getUser(),new URLSearchParams(i)}}document.addEventListener("DOMContentLoaded",(()=>{window.notificationsDash=new t,console.log(jvbSettings)}))})();
\ No newline at end of file
diff --git a/assets/js/min/notifications.min.js b/assets/js/min/notifications.min.js
index b13388d..5ee98e4 100644
--- a/assets/js/min/notifications.min.js
+++ b/assets/js/min/notifications.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(t={}){this.popupQueue=[],this.isLoading=!1,this.cache=window.jvbCache,this.isProcessingQueue=!1,this.options={maxVisibleNotifications:5,displayDuration:{high:7e3,medium:5e3,low:3e3},position:"bottom-right",pollingInterval:6e4,...t},this.button=document.querySelector(".toggle.notifications"),this.submenu=document.querySelector(".notifications-preview"),this.toasts=document.querySelector(".toasts"),this.notificationsLoaded=!1,this.pollTimer=null,this.lastCheck=null,this.button&&this.submenu&&this.init(),this.clickListeners=this.checkClicks.bind(this),this.updateListeners()}init(){this.submenu.addEventListener("click",(t=>{const e=t.target.closest(".mark-read");if(e){const t=e.closest(".notification-preview");t&&this.markAsRead(t.dataset.id)}})),this.loadNotifications(),this.initializePolling()}checkClicks(t){if(t.target.closest(".close-toast")){let e=t.target.closest(".toast");e.classList.add("hiding"),setTimeout((()=>{e.remove(),this.updateListeners()}),300)}}updateListeners(){this.toasts.addEventListener("click",this.clickListeners)}toggleDropdown(){this.notificationsLoaded||this.loadNotifications()}async loadNotifications(t=!1){if(!this.isLoading)try{this.isLoading=!0;const t=new URLSearchParams({user:jvbSettings.currentUser,status:"unread",limit:5}),e=await this.cache.fetchWithCache(`${jvbSettings.api}notifications?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.notifications}},{context:"notifications",forceRefresh:!0});console.log(e),this.renderPreviewNotifications(e.notifications),this.updateUnreadCount(e.total),this.notificationsLoaded=!0,this.lastCheck=(new Date).toUTCString()}catch(t){console.error("Error loading notifications:",t),this.renderErrorState(t.message)}}renderErrorState(t){const e=this.submenu.querySelector("#view-all");this.submenu.querySelectorAll("li:not(#view-all)").forEach((t=>t.remove()));const i=document.createElement("li");i.className="error-state",i.innerHTML=`\n        <p>${t}</p>\n        <button onclick="window.jvbNotifications.loadNotifications()">\n            Try Again\n        </button>\n    `,this.submenu.insertBefore(i,e)}renderPreviewNotifications(t){this.submenu.querySelector("#view-all");this.submenu.querySelectorAll("li:not(#view-all)").forEach((t=>t.remove())),t.forEach((t=>{let e=window.getTemplate("notificationItem");e.classList.add(t.status,`priority-${t.priority}`),e.dataset.id=t.id,e.prepend(getIcon(t.icon));let i=e.querySelector("p"),s=e.querySelector("time");[i.textContent,s.datetime,s.textContent]=[t.message,new Date(t.created_at).toISOString(),formatTimeAgo(t.created_at)];let o=window.getTemplate("notificationActions"),n=o.querySelector("button");t.actions.length>0&&(t.actions.forEach((e=>{let i=n.cloneNode(!0);e.primary&&i.classList.add("primary"),[i.dataset.id,i.dataset.action,i.textContent]=[t.id,e.label.toLowerCase(),e.label],o.append(i)})),n.remove()),e.append(o),this.submenu.prepend(e)})),0===t.length&&this.submenu.prepend(window.getTemplate("emptyNotification"))}queuePopupNotification(t){this.popupQueue.push(t),this.processPopupQueue()}async processPopupQueue(){if(!this.isProcessingQueue&&0!==this.popupQueue.length){for(this.isProcessingQueue=!0;this.popupQueue.length>0;){const t=this.popupQueue.shift();await this.showToast(t.message,t.type,t.actions),this.popupQueue.length>0&&await new Promise((t=>setTimeout(t,300)))}this.isProcessingQueue=!1}}showToast(t,e="success",i={}){let s=window.getTemplate("notificationPopup");if(s.classList.add(e),s.querySelector("p").textContent=t,Object.entries(i).length>0){let t=window.getTemplate("notificationActions"),e=i.querySelector("button");notification.actions.forEach((i=>{let s=e.cloneNode(!0);i.primary&&s.classList.add("primary"),[s.dataset.action,s.textContent]=[i.label.toLowerCase(),i.label],t.prepend(s)}))}this.toasts.append(s),setTimeout((()=>{s.classList.add("show")}),10),setTimeout((()=>{s.classList.add("hiding"),setTimeout((()=>{s.remove()}),300)}),3e3)}createNotificationElement(t){this.showToast(t.message),this.renderPreviewNotifications([t])}removePopupNotification(t){t.classList.remove("show"),setTimeout((()=>{t.remove()}),300)}updateUnreadCount(t){let e=this.button.querySelector("span");this.button.classList.remove("has"),[e.textContent,e.ariaLabel]=["","Notifications"],t&&!isNaN(t)&&(t=parseInt(t,10))>0&&(this.button.classList.add("has"),[e.textContent,e.ariaLabel]=[t,t+" unread notification"+(t>1?"s":"")])}async markAsRead(t){try{const e=this.submenu.querySelector(`[data-id="${t}"]`);if(!e)return;e.classList.add("slide-out");const i=await fetch(`${jvbSettings.api}notifications`,{method:"POST",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash},body:{notification:t,user:jvbSettings.currentUser}});if(!i.ok)throw new Error(notificationSettings.strings.error);const s=await i.json();s.success&&(setTimeout((()=>{e.remove();if(0===this.submenu.querySelectorAll(".notification-preview").length){const t=this.submenu.querySelector("#view-all"),e=document.createElement("li");e.className="empty-state fade-in",e.textContent=notificationSettings.strings.noNotifications,this.submenu.insertBefore(e,t),requestAnimationFrame((()=>{e.classList.remove("fade-in")}))}}),300),this.updateUnreadCount(s.total))}catch(t){console.error("Error marking notification as read:",t)}}initializePolling(){this.pollTimer=setInterval((()=>{this.checkNotifications()}),this.options.pollingInterval),document.addEventListener("visibilitychange",(()=>{document.hidden||this.checkNotifications()}))}async checkNotifications(){try{const t=new URLSearchParams({user:jvbSettings.currentUser,status:"unread"}),e=await fetch(`${jvbSettings.api}notifications?${t.toString()}`,{headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash,"If-Modified-Since":this.lastCheck}});if(!e.ok)return;(await e.json()).has_new&&await this.loadNotifications(!0)}catch(t){console.error("Check notifications error:",t)}}destroy(){this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbNotifications=new t({position:"bottom-right",maxVisibleNotifications:5,displayDuration:5e3})})),window.addNotification=function(t,e="info"){window.jvbNotifications.showToast(t,e)}})();
\ No newline at end of file
+(()=>{class t{constructor(t={}){this.popupQueue=[],this.isLoading=!1,this.cache=window.jvbCache,this.isProcessingQueue=!1,this.options={maxVisibleNotifications:5,displayDuration:{high:7e3,medium:5e3,low:3e3},position:"bottom-right",pollingInterval:6e4,...t},this.button=document.querySelector(".toggle.notifications"),this.submenu=document.querySelector(".notifications-preview"),this.toasts=document.querySelector(".toasts"),this.notificationsLoaded=!1,this.pollTimer=null,this.lastCheck=null,this.button&&this.submenu&&this.init(),this.clickListeners=this.checkClicks.bind(this),this.updateListeners()}init(){this.submenu.addEventListener("click",(t=>{const e=t.target.closest(".mark-read");if(e){const t=e.closest(".notification-preview");t&&this.markAsRead(t.dataset.id)}})),this.loadNotifications(),this.initializePolling()}checkClicks(t){if(t.target.closest(".close-toast")){let e=t.target.closest(".toast");e.classList.add("hiding"),setTimeout((()=>{e.remove(),this.updateListeners()}),300)}}updateListeners(){this.toasts.addEventListener("click",this.clickListeners)}toggleDropdown(){this.notificationsLoaded||this.loadNotifications()}async loadNotifications(t=!1){if(!this.isLoading)try{this.isLoading=!0;const t=new URLSearchParams({user:window.auth.getUser(),status:"unread",limit:5}),e=await this.cache.fetchWithCache(`${jvbSettings.api}notifications?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("notifications")}},{context:"notifications",forceRefresh:!0});this.renderPreviewNotifications(e.notifications),this.updateUnreadCount(e.total),this.notificationsLoaded=!0,this.lastCheck=(new Date).toUTCString()}catch(t){console.error("Error loading notifications:",t),this.renderErrorState(t.message)}}renderErrorState(t){const e=this.submenu.querySelector("#view-all");this.submenu.querySelectorAll("li:not(#view-all)").forEach((t=>t.remove()));const i=document.createElement("li");i.className="error-state",i.innerHTML=`\n        <p>${t}</p>\n        <button onclick="window.jvbNotifications.loadNotifications()">\n            Try Again\n        </button>\n    `,this.submenu.insertBefore(i,e)}renderPreviewNotifications(t){this.submenu.querySelector("#view-all");this.submenu.querySelectorAll("li:not(#view-all)").forEach((t=>t.remove())),t.forEach((t=>{let e=window.getTemplate("notificationItem");e.classList.add(t.status,`priority-${t.priority}`),e.dataset.id=t.id,e.prepend(getIcon(t.icon));let i=e.querySelector("p"),o=e.querySelector("time");[i.textContent,o.datetime,o.textContent]=[t.message,new Date(t.created_at).toISOString(),formatTimeAgo(t.created_at)];let s=window.getTemplate("notificationActions"),n=s.querySelector("button");t.actions.length>0&&(t.actions.forEach((e=>{let i=n.cloneNode(!0);e.primary&&i.classList.add("primary"),[i.dataset.id,i.dataset.action,i.textContent]=[t.id,e.label.toLowerCase(),e.label],s.append(i)})),n.remove()),e.append(s),this.submenu.prepend(e)})),0===t.length&&this.submenu.prepend(window.getTemplate("emptyNotification"))}queuePopupNotification(t){this.popupQueue.push(t),this.processPopupQueue()}async processPopupQueue(){if(!this.isProcessingQueue&&0!==this.popupQueue.length){for(this.isProcessingQueue=!0;this.popupQueue.length>0;){const t=this.popupQueue.shift();await this.showToast(t.message,t.type,t.actions),this.popupQueue.length>0&&await new Promise((t=>setTimeout(t,300)))}this.isProcessingQueue=!1}}showToast(t,e="success",i={}){let o=window.getTemplate("notificationPopup");if(o.classList.add(e),o.querySelector("p").textContent=t,Object.entries(i).length>0){let t=window.getTemplate("notificationActions"),e=i.querySelector("button");notification.actions.forEach((i=>{let o=e.cloneNode(!0);i.primary&&o.classList.add("primary"),[o.dataset.action,o.textContent]=[i.label.toLowerCase(),i.label],t.prepend(o)}))}this.toasts.append(o),setTimeout((()=>{o.classList.add("show")}),10),setTimeout((()=>{o.classList.add("hiding"),setTimeout((()=>{o.remove()}),300)}),3e3)}createNotificationElement(t){this.showToast(t.message),this.renderPreviewNotifications([t])}removePopupNotification(t){t.classList.remove("show"),setTimeout((()=>{t.remove()}),300)}updateUnreadCount(t){let e=this.button.querySelector("span");this.button.classList.remove("has"),[e.textContent,e.ariaLabel]=["","Notifications"],t&&!isNaN(t)&&(t=parseInt(t,10))>0&&(this.button.classList.add("has"),[e.textContent,e.ariaLabel]=[t,t+" unread notification"+(t>1?"s":"")])}async markAsRead(t){try{const e=this.submenu.querySelector(`[data-id="${t}"]`);if(!e)return;e.classList.add("slide-out");const i=await fetch(`${jvbSettings.api}notifications`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("dash")},body:{notification:t,user:window.auth.getUser()}});if(!i.ok)throw new Error(notificationSettings.strings.error);const o=await i.json();o.success&&(setTimeout((()=>{e.remove();if(0===this.submenu.querySelectorAll(".notification-preview").length){const t=this.submenu.querySelector("#view-all"),e=document.createElement("li");e.className="empty-state fade-in",e.textContent=notificationSettings.strings.noNotifications,this.submenu.insertBefore(e,t),requestAnimationFrame((()=>{e.classList.remove("fade-in")}))}}),300),this.updateUnreadCount(o.total))}catch(t){console.error("Error marking notification as read:",t)}}initializePolling(){this.pollTimer=setInterval((()=>{this.checkNotifications()}),this.options.pollingInterval),document.addEventListener("visibilitychange",(()=>{document.hidden||this.checkNotifications()}))}async checkNotifications(){try{const t=new URLSearchParams({user:window.auth.getUser(),status:"unread"}),e=await fetch(`${jvbSettings.api}notifications?${t.toString()}`,{headers:{"X-WP-Nonce":window.auth.getNonce(),action_nonce:window.auth.getNonce("dash"),"If-Modified-Since":this.lastCheck}});if(!e.ok)return;(await e.json()).has_new&&await this.loadNotifications(!0)}catch(t){console.error("Check notifications error:",t)}}destroy(){this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbNotifications=new t({position:"bottom-right",maxVisibleNotifications:5,displayDuration:5e3}))}))})),window.addNotification=function(t,e="info"){window.jvbNotifications.showToast(t,e)}})();
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index 283d70d..c49e5dd 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(t={}){this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...t},this.user=jvbSettings.currentUser,this.headers={"X-WP-Nonce":jvbSettings.nonce,...t.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const e=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=e.queue,this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const t=this.getOperationsByStatus(["completed","failed_permanent"],!1);t.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((t,e)=>{switch(t){case"data-loaded":case"items-saved":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":if(e.item){const t=this.store.data.get(e.item.id);t&&t.status!==e.item.status&&this.handleOperationStatusChange(e.item,t.status)}this.hasQueuedOperations()&&this.startPolling();break;default:this.updateUI()}})),this.notify("queue-initialized",{operations:t})}handleOperationStatusChange(t,e){if(t&&e!==t.status)switch(t.status){case"completed":this.notify("operation-completed",t);break;case"failed":this.notify("operation-failed",t);break;case"failed_permanent":this.notify("operation-failed-permanent",t)}}addToQueue(t){const e={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...t};if(e.headers={...this.headers,...e.headers},!e.endpoint||!e.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((t=>"queued"===t.status&&t.endpoint===e.endpoint&&t.canMerge));if(s.length>0){const t=s[0];return t.data=window.deepMerge(t.data,e.data),t.timestamp=Date.now(),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}return console.log("Added to Queue: ",e),this.store.clearCache(),this.setQueue(e),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}setQueue(t){this.store.save(t)}updateOperationStatus(t,e){let s=this.store.get(t);s&&(s.status=e,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(t){return this.store.get(t)}clearQueue(t){this.store.delete(t)}startActivityTracking(){if(!this.activityListeners){const t=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=t.map((t=>{const e=()=>this.resetActivityTimer();return document.addEventListener(t,e,{passive:!0}),{event:t,handler:e}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:t,handler:e})=>{document.removeEventListener(t,e)})),this.activityListeners=null)}setProcessing(t){this.isProcessing=t,this.ui.toggle.classList.toggle("saving",t)}async processQueue(){if(this.isProcessing)return;const t=this.getOperationsByStatus("queued");if(0===t.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const e of t)await this.processOperation(e);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(t){try{this.updateOperationStatus(t.id,"uploading"),t.data?._isFormData&&(t.data=await this.store.objectToFormData(t.data));const e=`${this.config.apiBase}${t.endpoint}`;let s;t.data instanceof FormData?(t.data.append("id",t.id),t.data.append("user",this.user),s=t.data):(s=JSON.stringify({...t.data,id:t.id,user:this.user}),t.headers["Content-Type"]="application/json");const i=await fetch(e,{method:t.method,headers:t.headers,body:s}),a=await i.json();if(!i.ok||!1===a.success)throw new Error(a.message||`HTTP ${i.status}`);if(a.id&&t.id!==a.id){const e=this.getQueue(a.id);e?(e.data=window.deepMerge(e.data,t.data),e.status="pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e),this.removeOperationFromUI(t.id),t=e):(this.clearQueue(t.id),t.id=a.id,t.status="pending",t.serverData=a,this.updateOperationStatus(t.id,t.status),this.setQueue(t))}else t.status="pending",t.serverData=a,this.updateOperationStatus(t.id,"pending"),this.setQueue(t);this.a11y.announce(`${t.title} sent to server for processing.`)}catch(e){console.error("Operation failed:",e),t.retries++,t.lastError=e.message,t.retries>=this.config.maxRetries?t.status="failed_permanent":(t.status="failed",t.nextRetry=Date.now()+1e3*Math.pow(2,t.retries)),this.updateOperationStatus(t.id,t.status),this.setQueue(t)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(t){console.error("Polling error:",t)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(t,e){if(0!==(t=(t=Array.isArray(t)?t:t.includes(",")?t.split(","):[t]).filter((t=>{let s=this.getQueue(t);return this.getAllowedActions(s.status).includes(e)}))).length){["cancel","dismiss"].includes(e)&&t.forEach((t=>{this.removeOperationFromUI(t)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:t,action:e,user:jvbSettings.currentUser})});if(!i.ok){const t=await i.json().catch((()=>{}));throw new Error(t.message||`${e} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${e} operation failed`);return["cancel","dismiss"].includes(e)?t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),this.clearQueue(t)})):(t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),a}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:e,operationIds:t,itemCount:t.length},(()=>this.updateServerOperations(t,e)));if(i.retried)return i;throw s}}}getAllowedActions(t){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[t]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=t=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return t.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(t){if(t.target.closest(this.selectors.panel,this.selectors.toggle))if(t.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(t.target.closest(this.selectors.clearButton)){const t=this.getOperationsByStatus("completed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"dismiss")}}else if(t.target.closest(this.selectors.retryButton)){const t=this.getOperationsByStatus("failed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"retry")}}else if(t.target.closest("[data-action]")){const e=t.target.closest("[data-action]"),s=e.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,e.dataset.action)}else if(t.target.closest(".filters [data-filter]")){const e=t.target.closest("[data-filter]").dataset.filter;this.setFilter(e)}}handleChange(t){}initUI(){if(this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[t,e]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(t))if("object"==typeof e){this.ui[t]={};for(let[s,i]of Object.entries(e))this.ui[t][s]=this.ui.panel.querySelector(i)}else this.ui[t]=this.ui.panel.querySelector(e)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const t=Array.from(this.store.data.values()),e=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=t.length-e.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>0?"":"none"}if(this.ui.indicator){const t=e.queued>0||e.uploading>0||e.pending>0||e.processing>0;this.ui.indicator.classList.toggle("active",t)}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?t.length:e[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(t){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[t]||t}getItemMessage(t){if(t.message)return t.message;if(t.error_message)return t.error_message;switch(t.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return t.position?`Position ${t.position} in queue`:"In server queue";case"processing":return t.progress?`${t.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${t.lastError||"Unknown error"} (Retry ${t.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${t.lastError||"Unknown error"}`;default:return""}}calculateProgress(t){if(t.progress)return t.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[t.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const t=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===t.length){let t=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(t),this.a11y.announce("Nothing queued.")}else t.forEach((t=>{const e=this.createOperationUI(t);this.ui.itemsContainer.append(e)}))}createOperationUI(t){const e=window.getTemplate("queueItem");return e.dataset.id=t.id,this.updateOperationUI(t,e),e}updateOperationUI(t,e=null){e||(e=this.ui.itemsContainer?.querySelector(`[data-id="${t.id}"]`)),e||(e=this.createOperationUI(t)),this.statuses.forEach((t=>e.classList.remove(t))),e.classList.add(t.status);let s="";t.updated_at?s=window.formatTimeAgo(new Date(t.updated_at)):t.created_at&&(s=window.formatTimeAgo(new Date(t.created_at)));const i=this.calculateProgress(t),a=e.querySelector(".type"),n=e.querySelector(".status"),r=e.querySelector(".info .details"),o=e.querySelector(".info .time"),l=e.querySelector(".progress .fill");if(a&&(a.textContent=t.title),n){n.querySelector(".icon")?.remove();let e=this.getStatusLabel(t.status);n.title=e,n.prepend(window.getIcon(this.icons[t.status])),n.querySelector("span").textContent=e}r&&(r.textContent=this.getItemMessage(t)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const d=e.querySelector(".actions");d&&this.updateActionButtons(t,d)}updateActionButtons(t,e){switch(window.removeChildren(e),t.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",e.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=t.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",e.appendChild(i),e.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",e.appendChild(n)}}removeOperationFromUI(t){const e=this.ui.itemsContainer?.querySelector(`[data-id="${t}"]`);e&&(e.style.opacity="0",e.style.transform="scale(0.9)",setTimeout((()=>e.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let t=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{t--,this.ui.countdown.textContent=t,t<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(t){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(t)&&this.ui.panel?.classList.add(t)}setFilter(t){Object.values(this.ui.filters).forEach((e=>{e&&e.classList.toggle("active",e.dataset.filter===t)})),"all"===t?this.store.clearFilters():this.store.setFilter("status",t)}showPopup(t,e="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=t),this.ui.popup.className=`popup ${e} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(t,e=!0){return Array.isArray(t)||"string"!=typeof t||(t=[t]),e?Array.from(this.store.data.values()).filter((e=>t.includes(e.status))):Array.from(this.store.data.values()).filter((e=>!t.includes(e.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((s=>s(t,e)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new t}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){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(){const e=this.getOperationsByStatus(["completed","failed_permanent"],!1);e.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":if(t.item){const e=this.store.data.get(t.item.id);e&&e.status!==t.item.status&&this.handleOperationStatusChange(t.item,e.status)}this.hasQueuedOperations()&&this.startPolling();break;default:this.updateUI()}})),this.notify("queue-initialized",{operations:e})}handleOperationStatusChange(e,t){if(e&&t!==e.status)switch(e.status){case"completed":this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.get(e);this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}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)return void this.stopActivityTracking();this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0?(this.startPolling(),this.showQueue()):this.hideQueue()}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}`);if(a.id&&e.id!==a.id){const t=this.getQueue(a.id);t?(t.data=window.deepMerge(t.data,e.data),t.status=a.status||"pending",t.serverData=a,this.updateOperationStatus(t.id,t.status),this.setQueue(t),this.removeOperationFromUI(e.id),e=t):(this.clearQueue(e.id),e.id=a.id,e.status=a.status||"pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e))}else e.status=a.status||"pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e);this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(e,t){if(0===(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length)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.getOperationsByStatus(["queued","uploading"]).length>0)return e.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationsByStatus("completed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"dismiss")}}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationsByStatus("failed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"retry")}}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}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/quill.min.js b/assets/js/min/quill.min.js
index e1b1aca..d36eccd 100644
--- a/assets/js/min/quill.min.js
+++ b/assets/js/min/quill.min.js
@@ -1 +1 @@
-window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const o=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        <i class="icon icon-paragraph"></i>\n                    </button>\n                    <button type="button" class="ql-h1">\n                        <i class="icon icon-text-h-one"></i>\n                    </button>\n                    <button type="button" class="ql-h2">\n                        <i class="icon icon-text-h-two"></i>\n                    </button>\n                    <button type="button" class="ql-h3">\n                        <i class="icon icon-text-h-three"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        <i class="icon icon-text-b-fi"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        <i class="icon icon-text-italic"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        <i class="icon icon-text-underline"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        <i class="icon icon-text-strikethrough"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        <i class="icon icon-list-dashes"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        <i class="icon icon-list-numbers"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        <i class="icon icon-text-align-left"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        <i class="icon icon-text-align-center"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        <i class="icon icon-text-align-right"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        <i class="icon icon-link"></i>\n                    </button>\n                    ${o}\n                </span>\n            `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const o=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;this.quill.getText(t.index,t.length);const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const o=e.querySelector(".remove");o&&o.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),o=new FormData;o.append("image",e),objectID&&o.append("post_id",objectID),window.jvbLoading&&window.jvbLoading.showLoading("Uploading image...","Processing Upload");try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":jvbSettings.nonce},body:o});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{window.jvbLoading&&window.jvbLoading.hide(),t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});o.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),o.on("text-change",(()=>{t.value=o.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))};
\ No newline at end of file
+window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const o=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        <i class="icon icon-paragraph"></i>\n                    </button>\n                    <button type="button" class="ql-h1">\n                        <i class="icon icon-text-h-one"></i>\n                    </button>\n                    <button type="button" class="ql-h2">\n                        <i class="icon icon-text-h-two"></i>\n                    </button>\n                    <button type="button" class="ql-h3">\n                        <i class="icon icon-text-h-three"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        <i class="icon icon-text-b-fi"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        <i class="icon icon-text-italic"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        <i class="icon icon-text-underline"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        <i class="icon icon-text-strikethrough"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        <i class="icon icon-list-dashes"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        <i class="icon icon-list-numbers"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        <i class="icon icon-text-align-left"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        <i class="icon icon-text-align-center"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        <i class="icon icon-text-align-right"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        <i class="icon icon-link"></i>\n                    </button>\n                    ${o}\n                </span>\n            `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const o=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;this.quill.getText(t.index,t.length);const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const o=e.querySelector(".remove");o&&o.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),o=new FormData;o.append("image",e),objectID&&o.append("post_id",objectID),window.jvbLoading&&window.jvbLoading.showLoading("Uploading image...","Processing Upload");try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:o});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{window.jvbLoading&&window.jvbLoading.hide(),t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});o.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),o.on("text-change",(()=>{t.value=o.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))};
\ No newline at end of file
diff --git a/assets/js/min/referral.min.js b/assets/js/min/referral.min.js
index f29d3f5..ff24a90 100644
--- a/assets/js/min/referral.min.js
+++ b/assets/js/min/referral.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector(".jvb-referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.initElements(),this.initListeners(),this.checkForReferral(),this.isLoggedIn()&&(this.loadStats(),this.loadRecentReferrals()))}initElements(){this.selectors={copyBtn:".copy-btn",checkCode:".check-code-btn",submit:"[type=submit]"},this.forms=this.container.querySelectorAll("form"),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.bindEventListeners(!0)},onClose:()=>{this.bindEventListeners(!1)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this)}bindEventListeners(e){const t=e?"addEventListener":"removeEventListener";this.forms.forEach((e=>{e[t]("submit",this.submitHandler)})),this.container[t]("click",this.clickHandler),this.container[t]("input",this.inputHandler)}isLoggedIn(){return Boolean(jvbSettings.currentUser)}handleClick(e){const t=e.target.closest(".copy-btn, .check-code-btn, .attn");t&&(t.classList.contains("copy-btn")?this.handleCopyClick(t):t.classList.contains("check-code-btn")?this.handleCheckCode(e):t.classList.contains("attn")&&t.classList.remove("attn"))}handleCopyClick(e){const t=e.dataset.target,s=this.container.querySelector(`#${t}`);if(!s)return;const r=s.textContent.trim();navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(r).then((()=>{this.showCopySuccess(e)})).catch((()=>{this.selectText(s),this.showCopyFallback(e)})):(this.selectText(s),this.showCopyFallback(e))}selectText(e){if(window.getSelection&&document.createRange){const t=window.getSelection(),s=document.createRange();s.selectNodeContents(e),t.removeAllRanges(),t.addRange(s)}else if(document.body.createTextRange){const t=document.body.createTextRange();t.moveToElementText(e),t.select()}}showCopySuccess(e){const t=e.innerHTML;e.innerHTML=window.jvbIcon("check",{size:16})+" Copied!",e.classList.add("success"),setTimeout((()=>{e.innerHTML=t,e.classList.remove("success")}),2e3)}showCopyFallback(e){const t=e.innerHTML;e.innerHTML="✓ Selected - Press Ctrl+C",e.classList.add("selected"),setTimeout((()=>{e.innerHTML=t,e.classList.remove("selected")}),3e3)}handleInput(e){"referral_code"!==e.target.id&&"referral_code"!==e.target.name||(e.target.value=e.target.value.toUpperCase())}async handleCheckCode(e){e.preventDefault();const t=e.target.closest("form"),s=t.querySelector('[name="referral_code"]'),r=t.querySelector(".code-status");if(!s||!r)return;const n=s.value.trim();if(n){r.hidden=!1,r.className="code-status loading",r.innerHTML='<span class="spinner"></span> Checking...';try{const e=await this.validateCodeOnly(n);e.success?this.showCodeStatus(r,`✓ Valid! Referred by ${e.referrer_name}`,"success"):this.showCodeStatus(r,e.message||"Invalid code","error")}catch(e){console.error("Error checking code:",e),this.showCodeStatus(r,"Error checking code","error")}}else this.showCodeStatus(r,"Please enter a code","error")}showCodeStatus(e,t,s){e.hidden=!1,e.className=`code-status ${s}`,e.textContent=t,"error"===s&&setTimeout((()=>{e.hidden=!0}),5e3)}async checkForReferral(){const e=this.getUrlParameter("seeReferral"),t=this.getUrlParameter("ref");if(!e&&!t)return;if(!t)return void this.popup.openPopup();const s=this.container.querySelector('[name="referral_code"]');if(!s)return;const r=t.toUpperCase();s.value=r,s.readOnly=!0,this.popup.togglePopup();try{const e=await this.validateCodeOnly(r);if(e.success){const t=s.closest("form").querySelector(".code-status");t&&this.showCodeStatus(t,`✓ ${e.referrer_name} invited you!`,"success");const r=this.container.querySelector('[name="referral_name"]');r&&r.focus()}else s.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),s.readOnly=!1}this.removeUrlParameter("ref")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}referrals/check-code`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({code:e})});return await t.json()}async loadStats(){if(this.container.querySelector(".stats-summary"))try{const e=await fetch(`${jvbSettings.api}referrals/my-stats?user=${jvbSettings.currentUser}`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}async loadRecentReferrals(){const e=this.container.querySelector(".recent-referrals-list");if(e)try{const t=await fetch(`${jvbSettings.api}referrals/my-referrals?limit=5&user=${jvbSettings.currentUser}`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),s=await t.json();s.success&&s.referrals?this.renderRecentReferrals(e,s.referrals):e.innerHTML='<p class="no-referrals">No referrals yet</p>'}catch(t){console.error("Error loading referrals:",t),e.innerHTML='<p class="error">Failed to load referrals</p>'}}renderRecentReferrals(e,t){if(!t||0===t.length)return void(e.innerHTML='<p class="no-referrals">Share your code to get started!</p>');const s=t.map((e=>`\n\t\t\t<div class="referral-item">\n\t\t\t\t<div class="referral-info">\n\t\t\t\t\t<strong>${window.escapeHtml(e.referee_name)}</strong>\n\t\t\t\t\t<span class="status-badge ${e.status}">${e.status}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class="referral-date">${this.formatDate(e.referred_at)}</div>\n\t\t\t</div>\n\t\t`)).join("");e.innerHTML=s}formatDate(e){const t=new Date(e),s=new Date,r=Math.abs(s-t),n=Math.floor(r/864e5);return 0===n?"Today":1===n?"Yesterday":n<7?`${n} days ago`:t.toLocaleDateString("en-US",{month:"short",day:"numeric"})}async handleFormSubmit(e){e.preventDefault();const t=e.target,s=new FormData(t);this.setFormLoading(!0,t);try{let e={success:!1,message:""};if("referral-code-form"===t.id){const t={name:s.get("referral_name"),email:s.get("referral_email"),code:s.get("referral_code")};t.name&&t.email&&t.code?e=await this.makeRequest("referrals/register",t):e.message="Please fill in all fields"}else if("login-form"===t.id){const t={type:"login",email:s.get("login_email"),context:{redirect_to:window.location.href+"?seeReferral=1"}};e=await this.makeRequest("magic",t)}e.success?this.handleSuccess(t,e):this.showFormMessage(t,e.message||"Something went wrong. Please try again.","error")}catch(e){console.error("Error submitting form:",e),this.showFormMessage(t,"Something went wrong. Please try again.","error")}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic","referrals/register","referrals/check-code"].includes(e))return{success:!1,message:"Invalid endpoint"};const s=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(t)});return await s.json()}handleSuccess(e,t){e.style.display="none";const s=e.nextElementSibling;s&&s.classList.contains("success-content")&&(s.hidden=!1,s.scrollIntoView({behavior:"smooth",block:"center"})),this.dispatchEvent("emailSent",{email:t.email})}showFormMessage(e,t,s="error"){const r=e.querySelector(".status");if(!r)return;const n=r.querySelector(".message");n&&(n.textContent=t),r.hidden=!1,r.className=`status ${s}`,"error"===s&&setTimeout((()=>{r.hidden=!0}),5e3)}setFormLoading(e,t){t.querySelectorAll("input, button").forEach((t=>t.disabled=e));const s=t.querySelector(".status");if(s&&(s.classList.toggle("loading",e),e)){s.hidden=!1;const e=s.querySelector(".message");e&&(e.textContent="Sending...")}}dispatchEvent(e,t){const s=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(s)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbReferral=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("aside.referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.hasCopy=navigator.clipboard&&navigator.clipboard.writeText,this.initElements(),this.storesInited=!1,this.initStore(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copyBtn:".copy-btn",checkCode:".check-code-btn",submit:"[type=submit]",recentList:".recent-referrals-list",invite:"form.invite",adminList:".items-list.referral",dash:".replace .referral-dashboard",stats:{codeUsed:'[data-stat="code_used"]',consultations:'[data-stat="consultations"]',treatments:'[data-stat="treatments"]',rewards:'[data-stat="total_rewards"]'},list:".referrals-list"},this.forms=this.container.querySelectorAll("form"),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.bindEventListeners(!0)},onClose:()=>{this.bindEventListeners(!1)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors),this.dashTabs=null,this.ui.dash&&(this.dashTabs=new window.jvbTabs(this.ui.dash)),this.hasCopy||document.querySelectorAll(this.selectors.copyBtn).forEach((e=>{e.remove()})),this.formController=null,this.ui.invite&&(this.formController=new window.jvbForm,this.formController.registerForm(this.ui.invite,{autosave:!0,endpoint:"referrals",formStatus:!1}),this.formController.subscribe(((e,t)=>{"form-submit"===e&&((t=t.fullData).action="invite",window.jvbQueue.addToQueue({endpoint:"referrals",data:t,title:"Submitting invitations"}))})))}initStore(){if(!this.isLoggedIn())return;const e=window.jvbStore.register("referrals",[{storeName:"stats",keyPath:"user_id",endpoint:"referrals/stats",TTL:3e5,showLoading:!1,delayFetch:!1,filters:{type:"dashboard",user:window.auth.getUser()}},{storeName:"list",keyPath:"id",endpoint:"referrals",TTL:6e5,showLoading:!1,delayFetch:!1,filters:{user:window.auth.getUser(),status:"all",limit:50,offset:0}}]);this.statsStore=e.stats,this.listStore=e.list,this.statsStore&&this.statsStore.subscribe(this.handleStatsEvent.bind(this)),this.listStore&&this.listStore.subscribe(this.handleListEvent.bind(this)),this.ui.dash&&this.initViewController()}initViewController(){this.listStore&&this.ui.adminList&&(this.view=new window.jvbViews(this.ui.adminList,this.listStore),this.view.subscribe(((e,t)=>{switch(e){case"item-action":this.handleItemAction(t);break;case"bulk-action":this.handleBulkAction(t)}})))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this)}bindEventListeners(e){const t=e?"addEventListener":"removeEventListener";this.forms.forEach((e=>{e[t]("submit",this.submitHandler)})),this.container[t]("click",this.clickHandler),this.container[t]("input",this.inputHandler)}isLoggedIn(){return Boolean(window.auth.getUser())}handleStatsEvent(e,t){switch(e){case"data-loaded":t.items&&t.items.length>0&&this.updateStatsDisplay();break;case"fetch-error":console.error("Error loading stats:",t.error)}}handleListEvent(e,t){switch(e){case"data-loaded":this.ui.recentList&&this.renderRecentReferrals();break;case"fetch-error":console.error("Error loading referrals:",t.error)}}updateStatsDisplay(){if(0===!this.statsStore.data.size)return;let e=this.statsStore.data.get(parseInt(window.auth.getUser()));const t={total:e.code_used||0,treated:e.treatments||0,pending:e.pending||0,rewards:"$"+parseFloat(e.total_rewards||0).toFixed(2)};Object.entries(t).forEach((([e,t])=>{const s=this.container.querySelector(`[data-stat="${e}"]`);s&&(s.textContent=t)}));const s=this.container.querySelectorAll(".stats .card");s.length>=4&&(s[0].querySelector(".stat-number").textContent=t.code_used,s[1].querySelector(".stat-number").textContent=t.consultations,s[2].querySelector(".stat-number").textContent=t.treatments,s[3].querySelector(".stat-number").textContent=t.total_rewards)}handleItemAction(e){const{action:t,itemId:s}=e;switch(t){case"remove":this.removeReferral(s);break;case"resend":this.resendInvite(s)}}async removeReferral(e){if(confirm("Remove this referral from your list?"))try{const t=await fetch(`${jvbSettings.api}referrals`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({action:"remove",referral_id:e})});(await t.json()).success&&(this.listStore&&this.listStore.fetch(),this.statsStore&&this.statsStore.fetch(),this.a11y?.announce("Referral removed"))}catch(e){console.error("Error removing referral:",e)}}async resendInvite(e){try{const t=await fetch(`${jvbSettings.api}referrals`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({action:"resend",referral_id:e})}),s=await t.json();s.success?this.a11y?.announce("Invitation resent"):alert(s.message||"Cannot resend yet. Wait 7 days between invites.")}catch(e){console.error("Error resending invite:",e)}}handleClick(e){const t=e.target.closest(".copy-btn, .check-code-btn, .attn");t&&(t.classList.contains("copy-btn")?this.handleCopyClick(t):t.classList.contains("check-code-btn")?this.handleCheckCode(e):t.classList.contains("attn")&&t.classList.remove("attn"))}handleCopyClick(e){const t=e.dataset.target,s=this.container.querySelector(`#${t}`);if(!s)return;const r=s.textContent.trim();this.hasCopy&&navigator.clipboard.writeText(r).then((()=>{this.showCopySuccess(e)})).catch((()=>{this.selectText(s),this.showCopyFallback(e)}))}selectText(e){if(window.getSelection&&document.createRange){const t=window.getSelection(),s=document.createRange();s.selectNodeContents(e),t.removeAllRanges(),t.addRange(s)}else if(document.body.createTextRange){const t=document.body.createTextRange();t.moveToElementText(e),t.select()}}showCopySuccess(e){const t=e.innerHTML;e.innerHTML=window.jvbIcon("check",{size:16})+" Copied!",e.classList.add("success"),setTimeout((()=>{e.innerHTML=t,e.classList.remove("success")}),2e3)}showCopyFallback(e){const t=e.innerHTML;e.innerHTML="✓ Selected - Press Ctrl+C",e.classList.add("selected"),setTimeout((()=>{e.innerHTML=t,e.classList.remove("selected")}),3e3)}handleInput(e){"referral_code"!==e.target.id&&"referral_code"!==e.target.name||(e.target.value=e.target.value.toUpperCase())}async handleCheckCode(e){e.preventDefault();const t=e.target.closest("form"),s=t.querySelector('[name="referral_code"]'),r=t.querySelector(".code-status");if(!s||!r)return;const a=s.value.trim();if(a){r.hidden=!1,r.className="code-status loading",r.innerHTML='<span class="spinner"></span> Checking...';try{const e=await this.validateCodeOnly(a);e.success?this.showCodeStatus(r,`✓ Valid! Referred by ${e.referrer_name}`,"success"):this.showCodeStatus(r,e.message||"Invalid code","error")}catch(e){console.error("Error checking code:",e),this.showCodeStatus(r,"Error checking code","error")}}else this.showCodeStatus(r,"Please enter a code","error")}showCodeStatus(e,t,s){e.hidden=!1,e.className=`code-status ${s}`,e.textContent=t,"error"===s&&setTimeout((()=>{e.hidden=!0}),5e3)}async checkForReferral(){const e=this.getUrlParameter("ref"),t=this.getUrlParameter("rname"),s=this.getUrlParameter("remail"),r=this.getUrlParameter("seeReferral");if(!e&&!r)return;if(r&&!e)return this.popup.openPopup(),void this.removeUrlParameter("seeReferral");const a=this.container.querySelector('[name="referral_code"]');if(!a)return;const n=e.toUpperCase();if(a.value=n,a.readOnly=!0,t||s){const e=this.container.querySelector('[name="referral_name"]');e&&(e.value=t);const r=this.container.querySelector('[name="referral_email"]');r&&(r.value=s)}this.popup.openPopup();try{const e=await this.validateCodeOnly(n);if(e.success){const t=a.closest("form").querySelector(".code-status");t&&this.showCodeStatus(t,`✓ ${e.referrer_name} invited you!`,"success");const s=this.container.querySelector('[name="referral_name"]');s&&!s.value&&s.focus()}else a.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),a.readOnly=!1}this.removeUrlParameter("ref"),this.removeUrlParameter("rname"),this.removeUrlParameter("remail")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}referrals/code`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({code:e})});return await t.json()}async loadStats(){if(this.container.querySelector(".stats-summary"))try{const e=await fetch(`${jvbSettings.api}referrals/my-stats?user=${window.auth.getUser()}`,{headers:{"X-WP-Nonce":window.auth.getNonce()}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}async loadSidebarStats(){try{const e=await fetch(`${jvbSettings.api}referrals/stats?user=${window.auth.getUser()}&type=quick`,{headers:{"X-WP-Nonce":window.auth.getNonce()}}),t=await e.json();t.success&&t.stats&&this.updateSidebarStats(t.stats)}catch(e){console.error("Error loading sidebar stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}renderRecentReferrals(){let e=this.ui.recentList,t=Array.from(this.listStore.data.values());t&&0!==t.length?e.innerHTML=t.map((e=>`\n\t\t\t<div class="referral-item">\n\t\t\t\t<div class="referral-info">\n\t\t\t\t\t<strong>${window.escapeHtml(e.referee_name)}</strong>\n\t\t\t\t\t<span class="status-badge">${e.referral_status}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class="referral-date">${window.formatTimeAgo(e.referred_at)}</div>\n\t\t\t</div>\n\t\t`)).join(""):e.innerHTML='<p class="no-referrals">Share your code to get started!</p>'}formatDate(e){const t=new Date(e),s=new Date,r=Math.abs(s-t),a=Math.floor(r/864e5);return 0===a?"Today":1===a?"Yesterday":a<7?`${a} days ago`:t.toLocaleDateString("en-US",{month:"short",day:"numeric"})}async handleFormSubmit(e){e.preventDefault();const t=e.target,s=new FormData(t);this.setFormLoading(!0,t);try{let e={success:!1,message:""};if("referral-code-form"===t.id){const t={name:s.get("referral_name"),email:s.get("referral_email"),referral_code:s.get("referral_code")};t.name&&t.email&&t.referral_code?e=await this.makeRequest("auth/register",t):e.message="Please fill in all fields"}else if("login-form"===t.id){const t={type:"login",email:s.get("login_email"),context:{redirect_to:window.location.href+"?seeReferral=1"}};e=await this.makeRequest("magic",t)}e.success?this.handleSuccess(t,e):this.showFormMessage(t,e.message||"Something went wrong. Please try again.","error")}catch(e){console.error("Error submitting form:",e),this.showFormMessage(t,"Something went wrong. Please try again.","error")}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic","auth/register"].includes(e))return{success:!1,message:"Invalid endpoint"};const s=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(t)});if(!s.ok){const e=await s.text();console.error("Error response:",s.status,e);try{return JSON.parse(e)}catch{return{success:!1,message:"Server error"}}}return await s.json()}handleSuccess(e,t){e.style.display="none";const s=e.nextElementSibling;s&&s.classList.contains("success-content")&&(s.hidden=!1,s.scrollIntoView({behavior:"smooth",block:"center"})),this.dispatchEvent("emailSent",{email:t.email})}showFormMessage(e,t,s="error"){const r=e.querySelector(".status");if(!r)return;const a=r.querySelector(".message");a&&(a.textContent=t),r.hidden=!1,r.className=`status ${s}`,"error"===s&&setTimeout((()=>{r.hidden=!0}),5e3)}setFormLoading(e,t){t.querySelectorAll("input, button").forEach((t=>t.disabled=e));const s=t.querySelector(".status");if(s&&(s.classList.toggle("loading",e),e)){s.hidden=!1;const e=s.querySelector(".message");e&&(e.textContent="Sending...")}}dispatchEvent(e,t){const s=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(s)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbReferral=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/schema.min.js b/assets/js/min/schema.min.js
new file mode 100644
index 0000000..af3dd28
--- /dev/null
+++ b/assets/js/min/schema.min.js
@@ -0,0 +1 @@
+(()=>{class e{constructor(){this.formController=null,this.tabsInstance=null,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.init()}init(){if(window.jvbForm&&!window.formController?(this.formController=new window.jvbForm,window.formController=this.formController):window.formController&&(this.formController=window.formController),window.jvbTabs){const e=document.querySelector(".jvb-seo-admin");e&&(this.tabsInstance=new window.jvbTabs(e))}this.formController&&this.formController.subscribe(((e,t)=>{"form-submit"===e&&this.handleFormSubmit(t)})),this.queue&&this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"seo"===t.endpoint&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initializeForms(),this.addPreservedFieldStyles()}initializeForms(){document.querySelectorAll('form[data-save="seo"]').forEach((e=>{this.formController&&this.formController.registerForm(e,{endpoint:"seo",autosave:!1,formStatus:!1}),this.initializeTypeSwitch(e);const t=e.querySelector('[data-action="reset"]');t&&t.addEventListener("click",(()=>this.handleReset(e)))}))}handleFormSubmit(e){const t=e.config.element.dataset.content,n=e.fullData,o={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"save",...n},popup:"Saving SEO configuration",title:`Saving ${t} settings`};this.queue.addToQueue(o)}async handleReset(e){const t=e.dataset.content;if(!confirm("Reset to default settings? This cannot be undone."))return;const n={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"reset"},popup:"Resetting configuration",title:`Resetting ${t} to defaults`};this.queue.addToQueue(n)}handleQueueSuccess(e,t){console.log("SEO save successful:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Configuration saved successfully"),"reset"===t.operation?.data?.action&&t.response?.schema&&this.reloadFormData(t.operation.data.context,t.response)}handleQueueFailure(e,t){console.error("SEO operation failed permanently:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(`Error: ${t.error_message||"Operation failed"}`)}reloadFormData(e,t){const n=document.querySelector(`form[data-content="${e}"]`);if(!n)return;const o=t.schema||{};Object.keys(o).forEach((e=>{const t=n.querySelector(`[name="${e}"]`);t&&("checkbox"===t.type?t.checked=!!o[e]:t.value=o[e]||"")})),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Form reset to defaults")}initializeTypeSwitch(e){const t=e.querySelector('select[name="type"]');t&&(t.addEventListener("change",(n=>{const o=e.dataset.currentType||t.dataset.initialValue,a=n.target.value;o!==a&&this.confirmTypeChange(e,t,o,a)})),t.dataset.initialValue=t.value,e.dataset.currentType=t.value)}confirmTypeChange(e,t,n,o){const a={},s=new FormData(e);for(let[e,t]of s.entries())"type"!==e&&t&&""!==t&&(a[e]=t);const r=window.getTemplate(`seo-${o}`);if(!r)return console.error("No template found for type:",o),void(t.value=n);const i=e=>e.split(":")[0],l=new Set(Object.keys(a).map(i)),c=r.querySelectorAll("[data-field]"),u=new Set(Array.from(c).map((e=>e.dataset.field)));if(0===u.size){const e=r.querySelectorAll("[name]");Array.from(e).forEach((e=>{u.add(i(e.getAttribute("name")))}))}const d=[...l].filter((e=>u.has(e))),h=[...l].filter((e=>!u.has(e)));let p=`Change schema type from ${n} to ${o}?\n\n`;d.length>0&&(p+=`✓ ${d.length} field value(s) will be preserved:\n`,p+=d.map((e=>`  • ${e}`)).join("\n"),p+="\n\n"),h.length>0&&(p+=`⚠ ${h.length} field value(s) will be lost:\n`,p+=h.map((e=>`  • ${e}`)).join("\n")),confirm(p)?this.handleTypeChange(e,t,o):(t.value=n,this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Type change cancelled"))}handleTypeChange(e,t,n){const o=e.dataset.currentType||t.dataset.initialValue,a=this.collectFormData(e),s=window.getTemplate(`seo-${n}`);if(!s)return void console.error("No template found for type:",n);const r=e.querySelector(".seo-"+o);if(r&&(r.parentNode.insertBefore(s,r),r.remove()),e.dataset.currentType=n,window.jvbPopulateForm){const t=new window.jvbPopulateForm,o=[];if(Object.keys(a).forEach((n=>{const s=e.querySelector(`[data-field="${n}"]`);if(s){const e=this.getFieldType(s),r=a[n];if("repeater"===e&&Array.isArray(r))t.populateRepeaterField(s,n,r),o.push(n);else if(null!=r&&""!==r){const e=s.querySelector(`[name="${n}"]`)||s.querySelector(`[name^="${n}"]`);e&&(this.populateSimpleField(e,r),o.push(n))}}})),o.length>0){const e=`Schema type changed to ${n}. Preserved ${o.length} field value(s).`;console.log(e),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(e)}else{const e=`Schema type changed to ${n}.`;this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(e)}}}collectFormData(e){const t={},n=new FormData(e);for(let[e,o]of n.entries())if("type"!==e&&"context"!==e)if(e.includes(":")){const n=e.split(":"),a=n[0],s=parseInt(n[1]),r=n[2];t[a]||(t[a]=[]),t[a][s]||(t[a][s]={}),t[a][s][r]=o}else t[e]=o;return t}getFieldType(e){return e.classList.contains("repeater")?"repeater":"text"}populateSimpleField(e,t){"checkbox"===e.type?e.checked="1"===t||"true"===t||!0===t:"SELECT"===e.tagName?setTimeout((()=>{e.value=t}),10):e.value=t,e.classList.add("value-preserved"),setTimeout((()=>e.classList.remove("value-preserved")),2e3)}addPreservedFieldStyles(){const e=document.createElement("style");e.textContent="\n            .value-preserved {\n                background-color: #e7f5e7 !important;\n                transition: background-color 0.3s ease;\n            }\n        ",document.head.appendChild(e)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSchema=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/selector.min.js b/assets/js/min/selector.min.js
index a54c866..f361149 100644
--- a/assets/js/min/selector.min.js
+++ b/assets/js/min/selector.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.hasAutocomplete=!1,this.isInitializing=!0,this.taxonomiesToFetch=new Set,this.triggers=new Set([".taxonomy-toggle"]),this.subscribers=new Set;const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.hasAutocomplete&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":const e=this.store.filters.taxonomy;if(e?.includes(",")&&this.handleBatchDataLoaded(e,t),e){(e.includes(",")?e.split(",").map((e=>e.trim())):[e]).forEach((e=>{this.updateFieldsForTaxonomy(e)}))}if(this.modal?.open&&this.handleTermsLoaded(t),this.isAutocompleteActive&&this.activeField){const e=this.fields.get(this.activeField),i=t.data?.items||[],s=t.filters?.search||"";this.showAutocompleteResults(e,i,s),this.isAutocompleteActive=!1}break;case"filters-changed":this.modal?.open&&this.showLoading();break;case"fetch-error":this.isAutocompleteActive&&this.activeField&&(this.showAutocompleteError(this.activeField),this.isAutocompleteActive=!1),this.handleFetchError(t.error)}}handleTermsLoaded(e){this.hideLoading();const t=this.store.getFiltered(),i=this.store.lastResponse?.page||{},s=e.filters?.search&&e.filters.search.length>0,o=i.page>1;this.notify("terms-loaded",{terms:t,filters:e.filters}),0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,o)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldButtonState(e){const t=this.fields.get(e);if(!t)return;const i=Array.from(this.store.data.values()).some((e=>e.taxonomy===t.taxonomy));t.toggle&&(t.toggle.disabled=!i&&!t.canCreate,t.toggle.title=i?`Select ${this.getPlural(t.taxonomy)}`:`No ${this.getSingular(t.taxonomy)} available`)}updateFieldsForTaxonomy(e){this.getFieldsForTaxonomy(e).forEach((e=>{this.updateFieldButtonState(e.id)}))}getFieldsForTaxonomy(e){return Array.from(this.fields.values()).filter((t=>t.taxonomy===e))}scanExistingFields(e=null){e||(e=document.body);e.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return!1;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=Object.hasOwn(t,"button")?t.button:e.querySelector("button.taxonomy-toggle");Object.hasOwn(t,"buttonSelector")&&this.triggers.add(t.buttonSelector);let r={id:s,input:i,container:e,taxonomy:o.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(o.dataset.max)||0,canSearch:"search"in o.dataset,hasAutocomplete:"autocomplete"in o.dataset,autocompleteDropdown:e.querySelector(".autocomplete-dropdown")??!1,canCreate:"creatable"in o.dataset,isRequired:"required"in o.dataset,selectedTerms:new Set,toggle:o,selectedContainer:Object.hasOwn(t,"selected")?t.selected:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&r.hasAutocomplete&&(this.hasAutocomplete=!0,this.initAutocomplete());const a=i.value.trim();if(""!==a){a.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>r.selectedTerms.add(e)))}return Object.hasOwn(t,"selectedItems")&&t.selectedItems.forEach((e=>{r.selectedTerms.add(e)})),this.fields.set(s,r),this.isInitializing&&this.taxonomiesToFetch.add(r.taxonomy),r.selectedTerms.size>0&&this.initFieldDisplay(s),s}registerFilterButton(e,t={}){const i=this.createFieldId(e);e.dataset.fieldId=i,t.buttonSelector&&this.triggers.add(t.buttonSelector);const s={id:i,input:null,container:t.container||e.closest(".filters")||e.parentElement,taxonomy:e.dataset.taxonomy,name:`filter_${e.dataset.taxonomy}`,maxSelection:parseInt(e.dataset.max)||0,canSearch:"search"in e.dataset,hasAutocomplete:!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(t.selectedItems||[]),toggle:e,selectedContainer:t.selected||null,isFilterMode:!0,...t};return this.fields.set(i,s),this.isInitializing?this.taxonomiesToFetch.add(s.taxonomy):this.store.setFilter("taxonomy",s.taxonomy),i}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;Array.from(t.selectedTerms).forEach((t=>{const i=this.store.get(t);i&&this.addTermToDisplay(e,i.id,i.name,i.path)}))}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),this.hasAutocomplete&&this.initAutocomplete()}initAutocomplete(){this.autocompleteHandler=window.debounce((e=>this.handleAutocomplete(e)),300),document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this)),document.addEventListener("focus",(e=>{if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);i&&this.preloadTaxonomy(i.taxonomy)}),!0)}handleClick(e){const t=window.targetCheck(e,Array.from(this.triggers));if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t,!0)}catch(e){console.error("Error handling toggle click:",e),this.error?.log&&this.error.log(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e,t=!1){this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=this.getSingular(this.currentConfig.taxonomy),this.currentPlural=this.getPlural(this.currentConfig.taxonomy),t&&this.modalInstance.handleOpen(),this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms.forEach((e=>{const t=this.store.get(e);t&&this.selectedTerms.set(e,{id:e,name:t.name,path:t.path})}))}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,hasAutocomplete:!1,autocompleteDropdown:document.querySelector(".autocomplete-dropdown")??!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s,!0),this.modalInstance.handleOpen()}openModal(){this.currentConfig?(!this.creator&&this.currentConfig.canCreate&&"jvbTaxCreator"in window&&(this.creator=new window.jvbTaxCreator(this)),this.updateModalForTaxonomy(),this.updateModalSelections(),this.updateSelectionCount(),window.removeChildren(this.ui.termsList),this.showLoading()):console.error("No active field set")}updateSelectionCount(){if(!this.currentConfig)return;const e=this.selectedTerms.size,t=this.currentConfig.maxSelection,i=this.modal?.querySelector(".selection-count");i&&(i.textContent=t>0?`${e} of ${t} selected`:`${e} selected`)}getSingular(e){return jvbSettings.labels[e]?.single||e}getPlural(e){return jvbSettings.labels[e]?.plural||e}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.notify("selected-terms",{terms:this.selectedTerms,taxonomy:this.currentConfig.taxonomy}),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),!this.hasAutocomplete&&this.creator&&delete this.creator,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const o=i.selectedContainer.querySelector(`[data-id="${s}"]`);o&&o.remove();const r=Array.from(i.selectedTerms);i.input.value=r.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const o=this.fields.get(e);if(!o||o.selectedContainer.querySelector(`[data-id="${t}"]`))return;const r=window.getTemplate("selectedTerm").cloneNode(!0);r.dataset.id=t,r.dataset.path=s,r.dataset.name=i,r.dataset.taxonomy=o.taxonomy,r.querySelector("span").textContent=s,r.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(r)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(e){const t=e.target.value.trim();this.searchHandler&&clearTimeout(this.searchHandler),this.searchHandler=setTimeout((()=>{this.store.setFilters({search:t,page:1,parent:t?0:this.store.filters.parent||0}),window.removeChildren(this.ui.termsList)}),300)}async handleAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);if(!i)return;const s=e.target.value.trim();if(i.currentAutocompleteQuery=s,s.length<2)return i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!0),void(this.isAutocompleteActive=!1);this.activeField=t,this.isAutocompleteActive=!0,i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!1),this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1})}cleanupAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target);this.fields.get(t)&&this.creator&&delete this.creator}showAutocompleteError(e){const t=this.fields.get(e);if(!t)return;t.config.autocompleteDropdown||(t.config.autocompleteDropdown=t.element.querySelector(".autocomplete-dropdown"));const i=t.config.autocompleteDropdown;i&&(window.removeChildren(i),this.showEmptyState("Hmmm... something went wrong",i))}showAutocompleteResults(e,t,i){if(!e||!e.autocompleteDropdown)return;const s=e.autocompleteDropdown;window.removeChildren(s),0===t.length?this.showEmptyState("No items found.",s):t.forEach((t=>{const i=this.createAutocompleteTermElement(e,t);i&&s.appendChild(i)}));const o=e.currentAutocompleteQuery||i;if(e.canCreate&&o&&window.jvbTaxCreator){const e=this.createNewTermOption(o);s.appendChild(e)}s.hidden=!1}createNewTermOption(e){const t=document.createElement("button");return t.type="button",t.className="autocomplete-item create-term",t.dataset.query=e,t.innerHTML=`<strong>Create:</strong> "${e}"`,t}createAutocompleteTermElement(e,t){const i=document.createElement("button");return i.type="button",i.className="autocomplete-item",i.dataset.id=t.id,i.dataset.name=t.name,i.dataset.path=t.path||t.name,i.textContent=t.path||t.name,i.addEventListener("click",(()=>{e.selectedTerms.add(parseInt(t.id)),this.addTermToDisplay(e.id,t.id,t.name,t.path),e.input.value=Array.from(e.selectedTerms).join(","),e.input.dispatchEvent(new Event("change",{bubbles:!0})),e.autocompleteDropdown.hidden=!0;const i=e.container.querySelector("input[data-autocomplete]");i&&(i.value="")})),i}navigateToParent(){this.store.setFilters({parent:0,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.termsList),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.store.setFilters({parent:t,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=0===t}loadMoreTerms(){const e=this.store.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e=null,t=!1,i=!1){if(e||(e=this.store.getFiltered()),t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.store.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s;const o=document.createDocumentFragment();e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&o.appendChild(t)})),this.ui.termsList.appendChild(o)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),o=t.querySelector("label"),r=t.querySelector("span, .term-name");if(s&&o&&r&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,o.htmlFor=s.id,o.title=e.path||e.name,o.dataset.path=e.path,r.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.store?.filters?.search||"",t=this.store?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found.",t=null){t||(t=this.ui.termsList);const i=window.getTemplate("noResults").cloneNode(!0);e&&i.querySelector("span")&&(i.querySelector("span").textContent=e),t.appendChild(i)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}handleBatchDataLoaded(e,t){const i=e.split(",").map((e=>e.trim())),s=this.store.getStore();i.forEach((e=>{const t={taxonomy:e,page:1,search:"",parent:0},i=this.generateCacheKeyForFilters(t),o={key:i,items:Array.from(this.store.data.values()).filter((t=>t.taxonomy===e)).map((e=>e.id)),timestamp:Date.now(),endpoint:s.config.endpoint,filters:t};if(s.cache.set(i,o),s.db?.objectStoreNames.contains("cache")){s.db.transaction(["cache"],"readwrite").objectStore("cache").put(o)}this.updateFieldsForTaxonomy(e)})),this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}generateCacheKeyForFilters(e){const t=Object.keys(e).sort().reduce(((t,i)=>(t[i]=e[i],t)),{});return JSON.stringify(t)}async preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.hasAutocomplete=!1,this.isInitializing=!0,this.taxonomiesToFetch=new Set,this.triggers=new Set([".taxonomy-toggle"]),this.subscribers=new Set;const e=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0});this.store=e.terms,this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.hasAutocomplete&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":const e=this.store.filters.taxonomy;if(e?.includes(",")&&this.handleBatchDataLoaded(e,t),e){(e.includes(",")?e.split(",").map((e=>e.trim())):[e]).forEach((e=>{this.updateFieldsForTaxonomy(e)}))}if(this.modal?.open&&this.handleTermsLoaded(t),this.isAutocompleteActive&&this.activeField){const e=this.fields.get(this.activeField),i=t.data?.items||[],s=t.filters?.search||"";this.showAutocompleteResults(e,i,s),this.isAutocompleteActive=!1}break;case"filters-changed":this.modal?.open&&this.showLoading();break;case"fetch-error":this.isAutocompleteActive&&this.activeField&&(this.showAutocompleteError(this.activeField),this.isAutocompleteActive=!1),this.handleFetchError(t.error)}}handleTermsLoaded(e){this.hideLoading();const t=this.store.getFiltered(),i=this.store.lastResponse?.page||{},s=e.filters?.search&&e.filters.search.length>0,o=i.page>1;this.notify("terms-loaded",{terms:t,filters:e.filters}),0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,o)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldButtonState(e){const t=this.fields.get(e);if(!t)return;const i=Array.from(this.store.data.values()).some((e=>e.taxonomy===t.taxonomy));t.toggle&&(t.toggle.disabled=!i&&!t.canCreate,t.toggle.title=i?`Select ${this.getPlural(t.taxonomy)}`:`No ${this.getSingular(t.taxonomy)} available`)}updateFieldsForTaxonomy(e){this.getFieldsForTaxonomy(e).forEach((e=>{this.updateFieldButtonState(e.id)}))}getFieldsForTaxonomy(e){return Array.from(this.fields.values()).filter((t=>t.taxonomy===e))}scanExistingFields(e=null){e||(e=document.body);e.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return!1;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=Object.hasOwn(t,"button")?t.button:e.querySelector("button.taxonomy-toggle");Object.hasOwn(t,"buttonSelector")&&this.triggers.add(t.buttonSelector);let r={id:s,input:i,container:e,taxonomy:o.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(o.dataset.max)||0,canSearch:"search"in o.dataset,hasAutocomplete:"autocomplete"in o.dataset,autocompleteDropdown:e.querySelector(".autocomplete-dropdown")??!1,canCreate:"creatable"in o.dataset,isRequired:"required"in o.dataset,selectedTerms:new Set,toggle:o,selectedContainer:Object.hasOwn(t,"selected")?t.selected:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&r.hasAutocomplete&&(this.hasAutocomplete=!0,this.initAutocomplete());const a=i.value.trim();if(""!==a){a.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>r.selectedTerms.add(e)))}return Object.hasOwn(t,"selectedItems")&&t.selectedItems.forEach((e=>{r.selectedTerms.add(e)})),this.fields.set(s,r),this.isInitializing&&this.taxonomiesToFetch.add(r.taxonomy),r.selectedTerms.size>0&&this.initFieldDisplay(s),s}registerFilterButton(e,t={}){const i=this.createFieldId(e);e.dataset.fieldId=i,t.buttonSelector&&this.triggers.add(t.buttonSelector);const s={id:i,input:null,container:t.container||e.closest(".filters")||e.parentElement,taxonomy:e.dataset.taxonomy,name:`filter_${e.dataset.taxonomy}`,maxSelection:parseInt(e.dataset.max)||0,canSearch:"search"in e.dataset,hasAutocomplete:!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(t.selectedItems||[]),toggle:e,selectedContainer:t.selected||null,isFilterMode:!0,...t};return this.fields.set(i,s),this.isInitializing?this.taxonomiesToFetch.add(s.taxonomy):this.store.setFilter("taxonomy",s.taxonomy),i}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;Array.from(t.selectedTerms).forEach((t=>{const i=this.store.get(t);i&&this.addTermToDisplay(e,i.id,i.name,i.path)}))}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),this.hasAutocomplete&&this.initAutocomplete()}initAutocomplete(){this.autocompleteHandler=e=>{window.debouncer.schedule("taxonomy-autocomplete",(()=>this.handleAutocomplete(e)),300)},document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this)),document.addEventListener("focus",(e=>{if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);i&&this.preloadTaxonomy(i.taxonomy)}),!0)}handleClick(e){const t=window.targetCheck(e,Array.from(this.triggers));if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t,!0)}catch(e){console.error("Error handling toggle click:",e),this.error?.log&&this.error.log(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e,t=!1){this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=this.getSingular(this.currentConfig.taxonomy),this.currentPlural=this.getPlural(this.currentConfig.taxonomy),t&&this.modalInstance.handleOpen(),this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms.forEach((e=>{const t=this.store.get(e);t&&this.selectedTerms.set(e,{id:e,name:t.name,path:t.path})}))}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,hasAutocomplete:!1,autocompleteDropdown:document.querySelector(".autocomplete-dropdown")??!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s,!0),this.modalInstance.handleOpen()}openModal(){this.currentConfig?(!this.creator&&this.currentConfig.canCreate&&"jvbTaxCreator"in window&&(this.creator=new window.jvbTaxCreator(this)),this.updateModalForTaxonomy(),this.updateModalSelections(),this.updateSelectionCount(),window.removeChildren(this.ui.termsList),this.showLoading()):console.error("No active field set")}updateSelectionCount(){if(!this.currentConfig)return;const e=this.selectedTerms.size,t=this.currentConfig.maxSelection,i=this.modal?.querySelector(".selection-count");i&&(i.textContent=t>0?`${e} of ${t} selected`:`${e} selected`)}getSingular(e){return jvbSettings.labels[e]?.single||e}getPlural(e){return jvbSettings.labels[e]?.plural||e}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.notify("selected-terms",{terms:this.selectedTerms,taxonomy:this.currentConfig.taxonomy}),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),!this.hasAutocomplete&&this.creator&&delete this.creator,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const o=i.selectedContainer.querySelector(`[data-id="${s}"]`);o&&o.remove();const r=Array.from(i.selectedTerms);i.input.value=r.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const o=this.fields.get(e);if(!o||o.selectedContainer.querySelector(`[data-id="${t}"]`))return;const r=window.getTemplate("selectedTerm").cloneNode(!0);r.dataset.id=t,r.dataset.path=s,r.dataset.name=i,r.dataset.taxonomy=o.taxonomy,r.querySelector("span").textContent=s,r.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(r)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(e){const t=e.target.value.trim();this.searchHandler&&clearTimeout(this.searchHandler),this.searchHandler=setTimeout((()=>{this.store.setFilters({search:t,page:1,parent:t?0:this.store.filters.parent||0}),window.removeChildren(this.ui.termsList)}),300)}async handleAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);if(!i)return;const s=e.target.value.trim();if(i.currentAutocompleteQuery=s,s.length<2)return i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!0),void(this.isAutocompleteActive=!1);this.activeField=t,this.isAutocompleteActive=!0,i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!1),this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1})}cleanupAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target);this.fields.get(t)&&this.creator&&delete this.creator}showAutocompleteError(e){const t=this.fields.get(e);if(!t)return;t.config.autocompleteDropdown||(t.config.autocompleteDropdown=t.element.querySelector(".autocomplete-dropdown"));const i=t.config.autocompleteDropdown;i&&(window.removeChildren(i),this.showEmptyState("Hmmm... something went wrong",i))}showAutocompleteResults(e,t,i){if(!e||!e.autocompleteDropdown)return;const s=e.autocompleteDropdown;window.removeChildren(s),0===t.length?this.showEmptyState("No items found.",s):t.forEach((t=>{const i=this.createAutocompleteTermElement(e,t);i&&s.appendChild(i)}));const o=e.currentAutocompleteQuery||i;if(e.canCreate&&o&&window.jvbTaxCreator){const e=this.createNewTermOption(o);s.appendChild(e)}s.hidden=!1}createNewTermOption(e){const t=document.createElement("button");return t.type="button",t.className="autocomplete-item create-term",t.dataset.query=e,t.innerHTML=`<strong>Create:</strong> "${e}"`,t}createAutocompleteTermElement(e,t){const i=document.createElement("button");return i.type="button",i.className="autocomplete-item",i.dataset.id=t.id,i.dataset.name=t.name,i.dataset.path=t.path||t.name,i.textContent=t.path||t.name,i.addEventListener("click",(()=>{e.selectedTerms.add(parseInt(t.id)),this.addTermToDisplay(e.id,t.id,t.name,t.path),e.input.value=Array.from(e.selectedTerms).join(","),e.input.dispatchEvent(new Event("change",{bubbles:!0})),e.autocompleteDropdown.hidden=!0;const i=e.container.querySelector("input[data-autocomplete]");i&&(i.value="")})),i}navigateToParent(){this.store.setFilters({parent:0,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.termsList),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.store.setFilters({parent:t,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=0===t}loadMoreTerms(){const e=this.store.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e=null,t=!1,i=!1){if(e||(e=this.store.getFiltered()),t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.store.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s;const o=document.createDocumentFragment();e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&o.appendChild(t)})),this.ui.termsList.appendChild(o)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),o=t.querySelector("label"),r=t.querySelector("span, .term-name");if(s&&o&&r&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,o.htmlFor=s.id,o.title=e.path||e.name,o.dataset.path=e.path,r.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.store?.filters?.search||"",t=this.store?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found.",t=null){t||(t=this.ui.termsList);const i=window.getTemplate("noResults").cloneNode(!0);e&&i.querySelector("span")&&(i.querySelector("span").textContent=e),t.appendChild(i)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}handleBatchDataLoaded(e,t){const i=e.split(",").map((e=>e.trim())),s=this.store.getStore();i.forEach((e=>{const t={taxonomy:e,page:1,search:"",parent:0},i=this.generateCacheKeyForFilters(t),o={key:i,items:Array.from(this.store.data.values()).filter((t=>t.taxonomy===e)).map((e=>e.id)),timestamp:Date.now(),endpoint:s.config.endpoint,filters:t};if(s.cache.set(i,o),s.db?.objectStoreNames.contains("cache")){s.db.transaction(["cache"],"readwrite").objectStore("cache").put(o)}this.updateFieldsForTaxonomy(e)})),this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}generateCacheKeyForFilters(e){const t=Object.keys(e).sort().reduce(((t,i)=>(t[i]=e[i],t)),{});return JSON.stringify(t)}async preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSelector=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/settings.min.js b/assets/js/min/settings.min.js
index 8d72a78..f10869a 100644
--- a/assets/js/min/settings.min.js
+++ b/assets/js/min/settings.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.cache=new window.jvbCache("settings"),this.cache.loadFromCache(),this.findSettings(),this.debouncer=window.debouncer,this.isLoggedIn=null!==jvbSettings.currentUser,this.initListeners(),this.loadSettings(),this.subscribers=new Set}findSettings(){this.settings=document.querySelectorAll("[data-setting]")??[]}addSetting(e,t="",s=null){t=""===t?e.name:t,e.dataset.setting=t;let n=this.cache.get(t);n&&("INPUT"===e.tagName&&["checkbox","radio"].includes(e.type)?e.checked=n===e.value:"DETAILS"===e.tagName&&(e.open="on"===n)),this.debouncer.schedule("add-setting",(()=>{this.findSettings.bind(this)}),300)}loadSettings(){for(const e of this.settings){let t=e.name;if(Object.hasOwn(e.dataset,"theme"))this.checkTheme(e);else{let s=this.cache.get(t);s&&("on"===e.value?e.checked="on"===s:["checkbox","radio"].includes(e.tagName)?e.checked=e.value===s:e.value=s)}}}checkTheme(e){const t=window.matchMedia("(prefers-color-scheme: dark)");let s=this.cache.get("dark-mode");!t||s&&"off"===s?"on"===s&&(e.checked=!0):e.checked=!0}initListeners(){this.changeHandler=this.handleChange.bind(this),document.addEventListener("change",this.changeHandler)}handleChange(e){if(!Object.hasOwn(e.target.dataset,"setting"))return;let t=e.target.value;"on"===e.target.value&&(t=e.target.checked?"on":"off"),this.saveSetting(e.target.name,t)}saveSetting(e,t){let s;this.isLoggedIn&&(s=this.cache.get(e)),this.cache.set(e,t),this.isLoggedIn&&s&&s!==t&&this.saveToServer(e,t)}async saveToServer(e,t){if(!this.isLoggedIn||!["dark-mode"].includes(e))return;const s={"X-WP-Nonce":jvbSettings?.nonce,"Content-Type":"application/json"},n={user:jvbSettings.currentUser,setting:e,value:t},i=await fetch(`${jvbSettings.api}settings`,{method:"POST",headers:s,body:JSON.stringify(n)});await i.json()}loadSetting(e){return this.cache.get(e)}loadUserSetting(e){}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){document.removeEventListener("change",this.changeHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbUserSettings=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.cache=new window.jvbCache("settings"),this.cache.loadFromCache(),this.findSettings(),this.debouncer=window.debouncer,this.isLoggedIn=null!==window.auth.getUser(),this.initListeners(),this.loadSettings(),this.subscribers=new Set}findSettings(){this.settings=document.querySelectorAll("[data-setting]")??[]}addSetting(e,t="",s=null){t=""===t?e.name:t,e.dataset.setting=t;let n=this.cache.get(t);n&&("INPUT"===e.tagName&&["checkbox","radio"].includes(e.type)?e.checked=n===e.value:"DETAILS"===e.tagName&&(e.open="on"===n)),this.debouncer.schedule("add-setting",(()=>{this.findSettings.bind(this)}),300)}loadSettings(){for(const e of this.settings){let t=e.name;if(Object.hasOwn(e.dataset,"theme"))this.checkTheme(e);else{let s=this.cache.get(t);s&&("on"===e.value?e.checked="on"===s:["checkbox","radio"].includes(e.tagName)?e.checked=e.value===s:e.value=s)}}}checkTheme(e){const t=window.matchMedia("(prefers-color-scheme: dark)");let s=this.cache.get("dark-mode");!t||s&&"off"===s?"on"===s&&(e.checked=!0):e.checked=!0}initListeners(){this.changeHandler=this.handleChange.bind(this),document.addEventListener("change",this.changeHandler)}handleChange(e){if(!Object.hasOwn(e.target.dataset,"setting"))return;let t=e.target.value;"on"===e.target.value&&(t=e.target.checked?"on":"off"),this.saveSetting(e.target.name,t)}saveSetting(e,t){let s;this.isLoggedIn&&(s=this.cache.get(e)),this.cache.set(e,t),this.isLoggedIn&&s&&s!==t&&this.saveToServer(e,t)}async saveToServer(e,t){if(!this.isLoggedIn||!["dark-mode"].includes(e))return;const s={"X-WP-Nonce":window.auth.getNonce(),"Content-Type":"application/json"},n={user:window.auth.getUser(),setting:e,value:t},i=await fetch(`${jvbSettings.api}settings`,{method:"POST",headers:s,body:JSON.stringify(n)});await i.json()}loadSetting(e){return this.cache.get(e)}loadUserSetting(e){}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){document.removeEventListener("change",this.changeHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUserSettings=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/swiper.min.js b/assets/js/min/swiper.min.js
deleted file mode 100644
index 10b3a47..0000000
--- a/assets/js/min/swiper.min.js
+++ /dev/null
@@ -1 +0,0 @@
-window.jvbSwiper=new class{constructor(){this.isInitialized=!1,this.initSubscribers(),this.initHandlers(),this.swipe={startX:null,endX:null,startY:null,endY:null,minSwipe:50},this.pinch={active:!1,startDistance:0,lastDistance:0,scale:1}}initHandlers(){this.touchStartHandler=this.handleTouchStart.bind(this),this.touchMoveHandler=this.handleTouchMove.bind(this),this.touchEndHandler=this.handleTouchEnd.bind(this)}initListeners(){this.isInitialized||(this.isInitialized=!0,document.addEventListener("touchstart",this.touchStartHandler),document.addEventListener("touchmove",this.touchMoveHandler),document.addEventListener("touchend",this.touchEndHandler))}cleanupListeners(){this.subscribers.size>0||(this.isInitialized=!1,document.removeEventListener("touchstart",this.touchStartHandler),document.removeEventListener("touchmove",this.touchMoveHandler),document.removeEventListener("touchend",this.touchEndHandler))}handleTouchStart(t){if(2===t.touches.length){const i=t.touches[0].clientX-t.touches[1].clientX,s=t.touches[0].clientY-t.touches[1].clientY,e=Math.sqrt(i*i+s*s);return this.pinch.active=!0,this.pinch.startDistance=this.pinch.lastDistance=e,void this.notify("pinch-start",{distance:e})}this.swipe.startX=t.touches[0].clientX,this.swipe.startY=t.touches[0].clientY}handleTouchMove(t){if(this.pinch.active&&2===t.touches.length){const i=t.touches[0].clientX-t.touches[1].clientX,s=t.touches[0].clientY-t.touches[1].clientY,e=Math.sqrt(i*i+s*s),n=e/this.pinch.startDistance;return this.pinch.lastDistance=e,this.pinch.scale=n,this.notify("pinch-move",{e:t,distance:e,scale:n}),void(e>this.pinch.startDistance?this.notify("pinch-out",{scale:n}):this.notify("pinch-in",{scale:n}))}this.swipe.endX=t.touches[0].clientX,this.swipe.endY=t.touches[0].clientY}handleTouchEnd(t){if(this.pinch.active)return this.notify("pinch-end",{finalScale:this.pinch.scale}),void(this.pinch.active=!1);if(!(this.swipe.startX&&this.swipe.endX&&this.swipe.startY&&this.swipe.endY))return;const i=this.swipe.startX-this.swipe.endX,s=this.swipe.startY-this.swipe.endY;if(Math.abs(i)>this.swipe.minSwipe){let t=i>0?"swipe-right":"swipe-left";this.notify(t)}if(Math.abs(s)>this.swipe.minSwipe){let t=s>0?"swipe-up":"swipe-down";this.notify(t)}this.swipe.startX=this.swipe.startY=this.swipe.endX=this.swipe.endY=null}initSubscribers(){this.subscribers=new Set}subscribe(t){return this.isInitialized||this.initListeners(),this.subscribers.add(t),()=>this.subscribers.delete(t)}unsubscribe(t){this.subscribers.delete(t),0===this.subscribers.size&&this.cleanupListeners()}notify(t,i={}){this.subscribers.forEach((s=>{try{s(t,i)}catch(t){console.error("Subscriber error:",t)}}))}destroy(){this.subscribers.clear(),this.cleanupListeners()}};
\ No newline at end of file
diff --git a/assets/js/min/tabs.min.js b/assets/js/min/tabs.min.js
index cf10a6e..4abd03c 100644
--- a/assets/js/min/tabs.min.js
+++ b/assets/js/min/tabs.min.js
@@ -1 +1 @@
-window.jvbTabs=class{constructor(t,a={},e=null){this.tabs=t.querySelector(".tabs"),this.a11y=window.jvbA11y,this.updateURL=!0,this.parent=e,this.childTabs=new Map,"updateURL"in a&&!1===a.updateURL&&(this.updateURL=!1),this.callbacks=a,this.activeTab=this.updateURL?this.getInitialTabFromHash():t.querySelector("button.tab.active")?.dataset.tab,this.container=t,this.tabs&&this.tabs.addEventListener("click",(t=>{const a=t.target.closest("[data-tab]");if(a){let t=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(a.dataset.tab,t)}})),this.initializeChildTabs(),this.selectDropdown=document.querySelector("select.tab-list"),this.selectDropdown&&this.selectDropdown.addEventListener("change",(t=>{let a=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(t.target.value,a)}));let s=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.activeTab||(this.activeTab=document.querySelector("button.tab")?.dataset.tab),this.switchTab(this.activeTab,s)}initializeChildTabs(){this.tabs.querySelectorAll("button").forEach((t=>{let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`);if(a&&a.querySelector(".tabs")){let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`),e=new window.jvbTabs(a,{},this);this.childTabs.set(t.dataset.tab,e)}}))}getInitialTabFromHash(){if(!window.location.hash)return!1;const t=window.location.hash.substring(1).split("/");if(this.parent){if(this.parent&&t.length>1){const a=this.getParentDepth();if(a<t.length){const e=t[a];if(this.tabs.querySelector(`[data-tab="${e}"]`))return e}}}else{const a=t[0];if(this.tabs.querySelector(`[data-tab="${a}"]`))return a}return null}getParentDepth(){let t=0,a=this.parent;for(;a;)t++,a=a.parent;return t}getFullTabPath(t){return this.parent?`${this.parent.getFullTabPath(this.parent.activeTab)}/${t}`:t}switchTab(t,a=!1){if(document.activeElement?.blur(),this.tabs.querySelectorAll("[data-tab]").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-selected",a.dataset.tab===t)})),this.container.querySelectorAll(".tab-content").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-hidden",a.dataset.tab!==t),a.hidden=a.dataset.tab!==t})),this.activeTab=t,this.callbacks[t]&&this.callbacks[t](),a)if(this.parent)this.parent.updateUrlFromChild();else{let a=t;const e=this.childTabs.get(t);e&&e.activeTab&&(a=e.getFullTabPath(e.activeTab)),window.history.pushState({tab:a},"",`#${a}`)}this.selectDropdown&&this.selectDropdown.querySelector(`option[value="${t}"]`)&&(this.selectDropdown.value=t),this.a11y.announce(`Switched to ${t} tab`)}updateUrlFromChild(){if(console.log("Updating URL"),!("updateURL"in this.callbacks)||this.callbacks.updateURL)if(this.parent)this.parent.updateUrlFromChild();else{const t=this.getFullTabPath(this.activeTab);window.history.pushState({tab:t},"",`#${t}`)}}};
\ No newline at end of file
+window.jvbTabs=class{constructor(t,a={},e=null){this.tabs=t.querySelector(".tabs"),this.a11y=window.jvbA11y,this.updateURL=!0,this.parent=e,this.childTabs=new Map,"updateURL"in a&&!1===a.updateURL&&(this.updateURL=!1),this.callbacks=a,this.activeTab=this.updateURL?this.getInitialTabFromHash():t.querySelector("button.tab.active")?.dataset.tab,this.container=t,this.tabs&&this.tabs.addEventListener("click",(t=>{const a=t.target.closest("[data-tab]");if(a){let t=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(a.dataset.tab,t)}})),this.initializeChildTabs(),this.selectDropdown=document.querySelector("select.tab-list"),this.selectDropdown&&this.selectDropdown.addEventListener("change",(t=>{let a=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.switchTab(t.target.value,a)}));let s=!("updateURL"in this.callbacks)||this.callbacks.updateURL;this.activeTab||(this.activeTab=document.querySelector("button.tab")?.dataset.tab),this.switchTab(this.activeTab,s)}initializeChildTabs(){this.tabs.querySelectorAll("button").forEach((t=>{let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`);if(a&&a.querySelector(".tabs")){let a=this.container.querySelector(`.tab-content[data-tab="${t.dataset.tab}"]`),e=new window.jvbTabs(a,{updateURL:!1},this);this.childTabs.set(t.dataset.tab,e)}}))}getInitialTabFromHash(){if(!window.location.hash)return!1;const t=window.location.hash.substring(1).split("/");if(this.parent){if(this.parent&&t.length>1){const a=this.getParentDepth();if(a<t.length){const e=t[a];if(this.tabs.querySelector(`[data-tab="${e}"]`))return e}}}else{const a=t[0];if(this.tabs.querySelector(`[data-tab="${a}"]`))return a}return null}getParentDepth(){let t=0,a=this.parent;for(;a;)t++,a=a.parent;return t}getFullTabPath(t){return this.parent?`${this.parent.getFullTabPath(this.parent.activeTab)}/${t}`:t}switchTab(t,a=!1){document.activeElement?.blur(),this.tabs.querySelectorAll("[data-tab]").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-selected",a.dataset.tab===t)})),this.container.querySelectorAll(".tab-content").forEach((a=>{a.classList.toggle("active",a.dataset.tab===t),a.setAttribute("aria-hidden",a.dataset.tab!==t),a.hidden=a.dataset.tab!==t})),this.activeTab=t,this.callbacks[t]&&this.callbacks[t]();const e=this.childTabs.get(t);if(e){const t=e.container.querySelector("button.tab")?.dataset.tab;t&&e.switchTab(t,!1)}a&&(this.parent?this.parent.updateUrlFromChild():window.history.pushState({tab:t},"",`#${t}`)),this.selectDropdown&&this.selectDropdown.querySelector(`option[value="${t}"]`)&&(this.selectDropdown.value=t),this.a11y.announce(`Switched to ${t} tab`)}updateUrlFromChild(){if(console.log("Updating URL"),!("updateURL"in this.callbacks)||this.callbacks.updateURL)if(this.parent)this.parent.updateUrlFromChild();else{const t=this.getFullTabPath(this.activeTab);window.history.pushState({tab:t},"",`#${t}`)}}};
\ No newline at end of file
diff --git a/assets/js/min/ui.min.js b/assets/js/min/ui.min.js
deleted file mode 100644
index df75d92..0000000
--- a/assets/js/min/ui.min.js
+++ /dev/null
@@ -1 +0,0 @@
-window.UIHandler=class{constructor(){this.elements={},this.activeComponents=new Set,this.componentStates=new Map,this.observers=new Map,this.handleOutsideClick=this.handleOutsideClick.bind(this),this.handleEscapeKey=this.handleEscapeKey.bind(this)}bindElements(){console.error("bindElements must be implemented by child class")}bindComponentEvents(){this.handlers&&Object.entries(this.handlers).forEach((([e,t])=>{const n=this.elements[e];n&&(n instanceof NodeList||Array.isArray(n)?n.forEach((e=>{this.bindEventsToElement(e,t)})):this.bindEventsToElement(n,t))}))}bindEventsToElement(e,t){"function"==typeof t?e.addEventListener("click",t.bind(this)):"object"==typeof t&&Object.entries(t).forEach((([t,n])=>{"forEach"!==t&&"function"==typeof n&&e.addEventListener(t,n)}))}bindEvents(){document.addEventListener("click",this.handleOutsideClick),document.addEventListener("keydown",this.handleEscapeKey)}isComponentActive(e){return this.activeComponents.has(e)}setComponentState(e,t,n={}){const{element:s,toggle:i,activeClass:r="open",focusElement:o=null,ariaLabel:c=null,ariaHidden:a=null,cleanup:l=null}=n;s&&(t?this.activeComponents.add(e):this.activeComponents.delete(e),s.classList.toggle(r,t),i&&(i.setAttribute("aria-expanded",t.toString()),c&&i.setAttribute("aria-label",c)),null!==a&&s.setAttribute("aria-hidden",(!t).toString()),o&&"function"==typeof o.focus&&o.focus(),!t&&l&&l(),this.componentStates.set(e,{isActive:t,activeClass:r,options:n}))}initializeKeyboardNavigation(e){this.keyboardConfig=e,Object.entries(e).forEach((([e,t])=>{const n=this.elements[e];n&&n.addEventListener("keydown",(e=>{const n=t[e.key];n&&n.call(this,e)}))}))}handleOutsideClick(e){console.error("handleOutsideClick must be implemented by child class")}handleEscapeKey(e){console.error("handleEscapeKey must be implemented by child class")}initializeHandlers(e){e&&"object"==typeof e?this.handlers=Object.entries(e).reduce(((e,[t,n])=>("function"==typeof n?e[t]=n.bind(this):n.forEach?e[t]={...n,handler:n.handler?.bind(this)}:"object"==typeof n&&(e[t]=Object.entries(n).reduce(((e,[t,n])=>(e[t]="function"==typeof n?n.bind(this):n,e)),{})),e)),{}):console.error("Invalid handlers configuration")}createObserver(e,t){return new IntersectionObserver(t,{root:null,rootMargin:"0px",threshold:0,...e})}initializeObserver(e,t,n,s){if(!t||!t.length)return;this.cleanupObserver(e);const i=this.createObserver(n,s);return this.observers.set(e,{observer:i,elements:new Set(t)}),t.forEach((e=>{e&&i.observe(e)})),i}cleanupObserver(e){const t=this.observers.get(e);if(t){const{observer:n,elements:s}=t;s.forEach((e=>{e&&n.unobserve(e)})),n.disconnect(),this.observers.delete(e)}}cleanupAllObservers(){this.observers.forEach(((e,t)=>{this.cleanupObserver(t)}))}cleanup(){document.removeEventListener("click",this.handleOutsideClick),document.removeEventListener("keydown",this.handleEscapeKey),this.cleanupComponentEvents(),this.cleanupAllObservers()}cleanupComponentEvents(){Object.entries(this.handlers).forEach((([e,t])=>{const n=this.elements[e];n&&(t.forEach&&n.forEach?n.forEach((e=>{e._boundHandler&&(e.removeEventListener("click",e._boundHandler),delete e._boundHandler)})):"object"==typeof t&&Object.entries(t).forEach((([e,t])=>{"forEach"!==e&&n.removeEventListener(e,t)})))}))}handleSearchCheckboxes(e){if(!e)return;const t=e.querySelector('input[type="checkbox"][value="1"]'),n=e.querySelectorAll('input[type="checkbox"]:not([value="1"])');if(!t)return;const s=e=>{const s=e.target;s===t?s.checked&&n.forEach((e=>{e.checked=!1})):s.checked?t.checked=!1:Array.from(n).some((e=>e.checked))||(t.checked=!0)};e.querySelectorAll('input[type="checkbox"]').forEach((e=>{e.addEventListener("change",s)})),e._removeCheckboxListeners=()=>{e.querySelectorAll('input[type="checkbox"]').forEach((e=>{e.removeEventListener("change",s)}))}}};
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 2ecf58b..7c52d8f 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.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":s&&this.handleOperationCancelled(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":this.handleOperationComplete(t,s);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),o=this.buildFieldUI(e),r={id:t,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(r),this.fieldElements.set(t,{element:e,ui:o,config:s}),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"single"!==s.type&&this.initSortable(t),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initSortable(e){if(!window.Sortable)return;!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 s=t.classList.contains("group")?t.closest(".upload-group")?.dataset.groupId:null;this.createSortableForGrid(t,e,s)}));const s=t.element.querySelector(".empty-group");s&&!s.sortableInstance&&(s.sortableInstance=new Sortable(s,{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(((s,o)=>{if(o.startsWith(e)){s.el.querySelectorAll(".item").forEach((e=>{const s=e.dataset.uploadId;t.has(s)?Sortable.utils.select(e):Sortable.utils.deselect(e)}))}}))}handleDrop(e,t){const s=e.to,o=e.from,r=e.items?.length>0?e.items:[e.item],a=r.map((e=>e.dataset.uploadId));switch(this.getDropTargetType(s)){case"empty-group":this.handleDropToEmptyGroup(r,a,t);break;case"preview":default:this.handleDropToPreview(r,a,t);break;case"group":this.handleDropToGroup(r,a,s,o,t)}this.updateSortableState(s),o!==s&&this.updateSortableState(o)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}handleDropToGroup(e,t,s,o,r){try{if(s===o)return void this.handleReorder({to:s,items:e});t.forEach((e=>{this.addToGroup(e,s,!1)})),this.schedulePersistance(r);const a=e.length>1?`Moved ${e.length} items to group`:"Moved item to group";this.a11y.announce(a);const i=this.selectionHandlers.get(r);i?.clearSelection()}catch(t){this.handleDropError(e,r,t)}}handleDropToPreview(e,t,s){try{t.forEach((e=>{this.removeFromGroup(e)})),this.schedulePersistance(s);const o=e.length>1?`Moved ${e.length} items to preview`:"Moved item to preview";this.a11y.announce(o);const r=this.selectionHandlers.get(s);r?.clearSelection()}catch(t){this.handleDropError(e,s,t)}}handleDropError(e,t,s,o="An error occurred"){console.error("Drop error:",s);const r=this.fieldElements.get(t);r?.ui?.preview&&e.forEach((e=>r.ui.preview.appendChild(e))),this.a11y.announce(`${o}. Items returned to preview.`)}handleDropToEmptyGroup(e,t,s){try{const o=this.createGroup(s);if(!o)return void this.handleDropError(e,s,new Error("Group creation failed"),"Failed to create group");e.forEach(((e,s)=>{o.grid.appendChild(e),this.addToGroup(t[s],o.grid,!1)})),this.schedulePersistance(s);const r=e.length>1?`Created group with ${e.length} items`:"Created group with item";this.a11y.announce(r);const a=this.selectionHandlers.get(s);a?.clearSelection()}catch(t){this.handleDropError(e,s,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,s=t.closest(".field, .upload");if(!s)return;e.items&&e.items.length>0?e.items:e.item;let o=Array.from(t.querySelectorAll(".item:not(.sortable-ghost):not(.sortable-clone)")).map((e=>e.dataset.uploadId)).filter((e=>e));console.log("Reordered items:",o);let r=s.querySelector('input[type="hidden"]');r&&o.length>0&&(r.value=o.join(","));const a=this.getFieldIdFromElement(t);if(a){const e=this.getFieldData(a);if(t.classList.contains("group")){const s=t.dataset.groupId,r=e?.groups?.find((e=>e.id===s));r&&(r.uploads=o)}this.schedulePersistance(a)}this.a11y.announce("Item reordered"),s.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex,items:o},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 s=Array.from(e.dataTransfer.files);if(0===s.length)return;const o=this.getFieldIdFromElement(t);o&&(this.processFiles(o,s),this.a11y.announce(`${s.length} file(s) dropped for upload`))}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}if(t){const s=this.getFieldData(t);"post_group"===s?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return;o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const r=t.length;let a=0;this.updateUploadProgress(e,0,r,"Processing files...");const i=Array.from(t).map((async t=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,l={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(l);const n=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,s.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:n,meta:l.meta,subtype:c},"post_group"===s.config.destination);o.ui.preview&&(o.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:n,location:o.ui.preview}));const p=this.uploadStore.get(i);return p&&(p.status="processed",await this.uploadStore.save(p)),s.uploads.add(i),await this.saveFieldData(s),a++,this.updateUploadProgress(e,a,r,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),a++,this.updateUploadProgress(e,a,r,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((o,r)=>{let a,i=!1;a=setTimeout((()=>{i||(i=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),r(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:a}),this.handleProcess(e,t).then((e=>{i||(i=!0,clearTimeout(a),this.worker.tasks.delete(t),o(e))})).catch((e=>{i||(i=!0,clearTimeout(a),this.worker.tasks.delete(t),r(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((o,r)=>{const a=new Image,i=document.createElement("canvas"),l=i.getContext("2d");let n=null;const d=()=>{a.onload=null,a.onerror=null,n&&(URL.revokeObjectURL(n),n=null),i.width=1,i.height=1,l.clearRect(0,0,1,1)};a.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(a,t);i.width=n,i.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(a,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);i.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});o(s)}else r(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),r(new Error(`Canvas processing failed: ${e.message}`))}},a.onerror=()=>{d(),r(new Error(`Failed to load image: ${e.name}`))};try{n=this.createPreviewUrl(e),a.src=n}catch(e){d(),r(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,o){return new Promise(((r,a)=>{if(!this.worker.worker)return void a(new Error("Worker not available"));const i=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===i)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});r(s)}else a(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),a(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:i,file:e,maxDimension:s,quality:o,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n\t\t\t\tself.onmessage = async function(e) {\n\t\t\t\t\tconst { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst bitmap = await createImageBitmap(file);\n\t\t\t\t\t\tconst scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n\t\t\t\t\t\tconst width = Math.round(bitmap.width * scale);\n\t\t\t\t\t\tconst height = Math.round(bitmap.height * scale);\n\t\t\t\t\t\tconst canvas = new OffscreenCanvas(width, height);\n\t\t\t\t\t\tconst ctx = canvas.getContext('2d');\n\t\t\t\t\t\tctx.imageSmoothingEnabled = true;\n\t\t\t\t\t\tctx.imageSmoothingQuality = 'high';\n\t\t\t\t\t\tctx.drawImage(bitmap, 0, 0, width, height);\n\t\t\t\t\t\tbitmap.close();\n\t\t\t\t\t\tconst blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });\n\t\t\t\t\t\tself.postMessage({ messageId, success: true, blob: blob, format: outputFormat });\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tself.postMessage({ messageId, success: false, error: error.message });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t"],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:o}=e;if(s<=t&&o<=t)return{width:s,height:o};const r=Math.min(t/s,t/o);return{width:Math.round(s*r),height:Math.round(o*r)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}async submitUploads(e){const t=this.getFieldData(e);this.fieldElements.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=this.getFieldGroups(e);if(0===o.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const r=[],a=new FormData;let i=[];for(const e of o){const t={images:[],fields:{}};for(let[s,o]of Object.entries(e.changes))t.fields[s]=o;const o=s.filter((t=>{const s=this.uploadStore.get(t);return s?.groupId===e.id}));for(const e of o){const s=await this.getBlobData(e);if(s){a.append("files[]",s);const o={upload_id:e,index:i.length},r=this.uploadElements.get(e),l=r?.element?.querySelector('[name="featured"]');l?.checked&&(t.fields.featured=e),t.images.push(o),i.push(e)}}r.push(t)}const l=s.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of l){const t={images:[],fields:{}},s=await this.getBlobData(e);if(s){a.append("files[]",s);const o={upload_id:e,index:i.length};t.images.push(o),i.push(e)}r.push(t)}a.append("content",t.config.content),a.append("user",t.config.itemID),a.append("posts",JSON.stringify(r)),a.append("upload_ids",JSON.stringify(i));const n={endpoint:"uploads/groups",method:"POST",data:a,title:`Creating ${r.length} ${t.config.content}${r.length>1?"s":""} from uploads...`,popup:`Creating ${r.length} post${r.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(n);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),this.a11y.announce(`Creating ${r.length} post${r.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}}async queueUpload(e){const t=this.getFieldData(e);if(!t?.uploads||0===t.uploads.size)return;const s=Array.from(t.uploads),o=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");const r={endpoint:"uploads",method:"POST",data:o,title:`Uploading ${s.length} file${s.length>1?"s":""} to server...`,popup:`Uploading ${s.length} file${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(r);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let o=[];const r=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const r=await this.getBlobData(e);r&&(s.append("files[]",r),o.push(t.id))}));return await Promise.all(r),s.append("upload_ids",JSON.stringify(o)),s}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),s=this.uploadStore.get(t);if(!s)return;if(!this.getFieldData(s.fieldId))return;let o={};o[e.target.name]=e.target.value,s.meta={...s.meta,...o},await this.uploadStore.save(s);let r={};r[s.attachmentId??s.id]=s.meta;const a={endpoint:"uploads/meta",method:"POST",data:r,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(a)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:s.id})}}async handleOperationComplete(e,t){if((e.result?.data||e.serverData?.data||[]).forEach((e=>{const t=this.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploadStore.save(t),this.updateUploadStatus(e.upload_id,"completed"))})),!t)return;const s=this.getFieldData(t);if(!s)return;const o=Array.from(s.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of o)await this.clearUpload(e,!1),s.uploads.delete(e);0===s.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(s),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const s=this.uploadStore.get(t);s&&(s.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(s),this.updateUploadStatus(t,s.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s)await this.clearUpload(e,!1);await this.clearFieldFromStores(e),this.updateFieldState(e),this.a11y.announce("Upload cancelled")}getFieldGroups(e){const t=this.getFieldData(e);return t?.groups?t.groups.map((e=>({id:e.id,uploads:e.uploads||[],changes:e.changes||{}}))):[]}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}async restoreSelectedUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,s]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=s,await this.restoreField(t))}}async restoreField(e){const{config:t,context:s,uploads:o,groups:r,id:a}=e;s?.modalType&&await this.openModalForRestore(s);let i=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!i){const e=`${t.content}_${t.itemID}_${t.name}`;i=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!i)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=i.dataset.uploader;l&&this.fieldElements.has(l)||(l=this.registerUploader(i));const n=this.fieldElements.get(l),d=this.getFieldData(l);if(!n||!d)return void console.error("Failed to register field for restoration");d.state=e.state||"ready",n.ui||(n.ui=this.buildFieldUI(i)),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),n.ui.dropZone&&(n.ui.dropZone.hidden=!0),r&&r.length>0&&await this.restoreGroups(l,r);const c=o instanceof Set?Array.from(o):Array.isArray(o)?o:[];for(const e of c){const t=this.uploadStore.get(e);t&&await this.restoreUpload(l,t)}await this.saveFieldData(d),this.updateFieldState(l),this.maybeLockUploads(l),this.refreshSortable(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}async restoreUpload(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(!s||!o)return void console.error("Field not found for upload restoration:",e);const r=await this.getBlobData(t.id);if(!r)return void console.warn("Blob data not found for upload:",t.id);const a=this.createPreviewUrl(r),i=this.getSubtypeFromMime(r.type),l=this.createUploadElement({id:t.id,preview:a,meta:t.meta||{originalName:r.name,size:r.size,type:r.type},subtype:i},"post_group"===o.config.destination);let n;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){n=e.grid;const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads||(s.uploads=[]),s.uploads.includes(t.id)||s.uploads.push(t.id))}else n=s.ui.preview,t.groupId=null}else n=s.ui.preview;n?n.appendChild(l):s.ui.preview&&(s.ui.preview.appendChild(l),n=s.ui.preview),this.uploadElements.set(t.id,{element:l,preview:a,location:n}),o.uploads||(o.uploads=new Set),o.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),n&&this.updateSortableState(n)}async restoreGroups(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(s&&o){for(const s of t){const t=this.createGroup(e,s.id);if(!t){console.warn("Failed to create group:",s.id);continue}const r=o.groups?.find((e=>e.id===s.id));if(r&&(s.changes&&(r.changes={...s.changes}),s.uploads&&(r.uploads=[...s.uploads]),s.changes)){const e=t.element.querySelector('[name*="post_title"]'),o=t.element.querySelector('[name*="post_excerpt"]');e&&s.changes.post_title&&(e.value=s.changes.post_title),o&&s.changes.post_excerpt&&(o.value=s.changes.post_excerpt)}}await this.saveFieldData(o)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:s}=e;let o=null;switch(t){case"create":o=document.querySelector('[data-action="create"]');break;case"edit":s&&(o=document.querySelector(`[data-action="edit"][data-id="${s}"]`));break;case"bulkEdit":o=document.querySelector('[data-action="bulk-edit"]')}o?(o.click(),await new Promise((e=>setTimeout(e,300)))):console.warn("Modal trigger not found for restoration:",e)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,o=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,o)).toFixed(s))+" "+["Bytes","KB","MB","GB"][o]}async clearUpload(e,t=!0){const s=this.uploadElements.get(e);if(s&&(this.revokePreviewUrl(s.preview),s.element)){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}if(this.uploadElements.delete(e),await this.uploadStore.delete(e),t){const t=this.uploadStore.get(e);t?.fieldId&&await this.schedulePersistance(t.fieldId)}}async clearFieldFromStores(e){const t=this.getFieldData(e);if(t?.uploads){const e=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const t of e)await this.uploadStore.delete(t)}await this.fieldStore.delete(e)}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}updateFieldState(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t||!s)return;const o=t.element,r=s.uploads?.size||0,a=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;o.dataset.hasUploads=r>0?"true":"false",o.dataset.uploadCount=r.toString(),o.dataset.hasGroups=a?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${r} item${1!==r?"s":""}`)}updateUploadProgress(e,t,s,o){const r=this.fieldElements.get(e);if(!r?.ui?.progress?.progress)return;const a=r.ui.progress,i=s>0?t/s*100:0;a.fill&&(a.fill.style.width=`${i}%`),a.text&&(a.text.textContent=o),a.count&&(a.count.textContent=`${t}/${s}`),a.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.getFieldData(e);s&&(s.state=t,this.saveFieldData(s))}updateUploadStatus(e,t){const s=this.uploadStore.get(e);s&&(s.status=t,this.uploadStore.save(s),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),s=this.uploadStore.get(e);if(!s||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${s.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(s.status),s.status)}showUploadProgress(e,t=!0){const s=this.uploadElements.get(e);if(!s?.element)return;const o=s.element.querySelector(".progress");o&&(t?(o.style.removeProperty("animation"),o.hidden=!1):(o.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{o.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const o=this.uploadElements.get(e);if(!o?.element)return;const r=o.element.querySelector(".progress");if(!r)return;const a=r.querySelector(".fill"),i=r.querySelector(".details"),l=r.querySelector(".icon");a&&(a.style.width=`${t}%`),s&&i&&(i.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t?.ui?.dropZone||!s)return;const o=s.uploads?.size||0,r="post_group"===s.config.destination?20:s.config?.maxFiles||999;t.ui.dropZone.hidden=o>=r,t.element.classList.toggle("at-max-uploads",o>=r),"post_group"===s.config.destination&&o>=r&&this.a11y.announce("Maximum of 20 uploads reached. Please submit current uploads before adding more.")}createSortableForGrid(e,t,s=null){if(!e||e.sortableInstance)return;const o=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=o;const r=s?`${t}-group-${s}`:`${t}-preview`;return this.sortableInstances.set(r,o),o}createGroup(e,t=null){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const r=this.createGroupElement(t,e);if(!r)return null;o.ui.groups||(o.ui.groups={groups:new Map,container:null,empty:null,display:null}),o.ui.groups.groups.set(t,r),o.ui.groups.container&&o.ui.groups.empty?o.ui.groups.container.insertBefore(r,o.ui.groups.empty):o.ui.groups.container&&o.ui.groups.container.appendChild(r);const a=r.querySelector(".item-grid.group");this.groupElements.set(t,{element:r,grid:a,fieldId:e}),s.groups||(s.groups=[]);return s.groups.find((e=>e.id===t))||(s.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(s)),this.addGroupSelectionHandler(e,t),a&&this.createSortableForGrid(a,e,t),{id:t,element:r,grid:a}}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let o=window.getTemplate("groupMetadata");const r=s.querySelector(".fields");if(r&&o){r.append(o);const a=r.querySelector('[name="post_title"]'),i=r.querySelector('[name="post_excerpt"]');a&&(a.id=`${e}_title`,a.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const l=this.getFieldData(t);if(l&&""!==l.config.content){let e=s.querySelector("summary");e&&(e.textContent=l.config.content+" Fields")}}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid.group");return a&&(a.dataset.groupId=e),s}deleteGroup(e,t=!0){const s=this.groupElements.get(e);if(!s)return;const o=this.getFieldData(s.fieldId);if(!o)return;const r=o.groups?.find((t=>t.id===e));let a=!0;t&&r?.uploads?.length>0&&(a=!window.confirm("Delete uploads in group?")),t&&a&&r?.uploads&&r.uploads.forEach((e=>{this.removeFromGroup(e)})),o.groups&&(o.groups=o.groups.filter((t=>t.id!==e)),this.saveFieldData(o)),s.element&&(s.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${s.fieldId}-group-${e}`,l=this.sortableInstances.get(i);l?.destroy&&l.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){const o=this.uploadStore.get(e),r=this.uploadElements.get(e);if(!o||!r)return;const a=this.getFieldData(o.fieldId),i=this.fieldElements.get(o.fieldId);if(!a||!i)return;if(!t&&r.location===i.ui.preview||t===r.location)return;if(o.groupId){const t=a.groups?.find((e=>e.id===o.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(o.groupId))}const l=r.element.querySelector('[name*="select-item"]');l&&(l.checked=!1);let n=r.element.querySelector('[name="featured"]');if(n&&(n.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,o.groupId=null;else{const s=t.dataset.groupId;n&&(n.name=s+"_"+n.name);const r=a.groups?.find((e=>e.id===s));r&&(r.uploads||(r.uploads=[]),r.uploads.push(e),o.groupId=s)}r.location=t,t.append(r.element),this.uploadStore.save(o),s&&this.saveFieldData(a),this.updateSortableState(t),r.location&&r.location!==t&&this.updateSortableState(r.location)}removeFromGroup(e){const t=this.uploadStore.get(e),s=this.uploadElements.get(e);if(!t||!s)return;const o=this.getFieldData(t.fieldId),r=this.fieldElements.get(t.fieldId);if(!o||!r)return;if(t.groupId){const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}r.ui?.preview&&(r.ui.preview.appendChild(s.element),s.location=r.ui.preview);const a=s.element.querySelector('[name="featured"]');a&&(a.hidden=!0,a.checked=!1),this.uploadStore.save(t),this.updateSortableState(r.ui.preview)}removeUpload(e,t){const s=this.getFieldData(e),o=this.uploadStore.get(t),r=this.uploadElements.get(t);if(!s||!o)return;if(s.uploads?.delete(t),o.groupId){const e=s.groups?.find((e=>e.id===o.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(o.groupId))}r?.element?.remove(),this.clearUpload(t),this.saveFieldData(s),this.updateFieldState(e),this.maybeLockUploads(e);const a=this.selectionHandlers.get(e);a&&a.deselect(t),this.a11y.announce("Upload removed")}handleGroupMetaChange(e){const t=this.getGroupFromElement(e);if(!t)return;const s=this.getFieldData(t.fieldId),o=s?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!o)return;o.changes||(o.changes={});let r=e.name;r.includes("group")&&(r=r.replace(`${o.id}_`,"").replace(`${o.id}[`,"").replace("]","")),o.changes[r]=e.value,this.saveFieldData(s),this.schedulePersistance(t.fieldId)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":const t=this.fieldElements.get(s);t&&(t.element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s));break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"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),s=t?.dataset.uploader;if(!s)return;const o=this.selected.get(s);if(o&&0!==o.size){const e=this.createGroup(s);if(!e)return;o.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${o.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,o=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(o)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,o=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(o,s),this.a11y.announce("Item removed"),this.schedulePersistance(o))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fieldElements.get(e);if(!t?.element)return;const s=new window.jvbHandleSelection({container:t.element,ui:{selectAll:t.element.querySelector('[name="select-all-uploads"]'),bulkControls:t.element.querySelector(".selection-actions"),count:t.element.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return s.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.syncSortableSelection(e,s.selectedItems),this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,s),s}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const o=this.groupElements.get(t);if(!o?.element)return;const r=new window.jvbHandleSelection({container:o.element,ui:{selectAll:o.element.querySelector(this.selectors.groups.selectAll),bulkControls:o.element.querySelector(this.selectors.groups.actions),count:o.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return r.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,r),r}handleSelectAll(e,t){}getCurrentSelection(e){let t=[];for(let[s,o]of this.selectionHandlers)(e===s||s.includes(e))&&o.selectedItems.size>0&&(t=t.concat([...o.selectedItems]));return t}getFieldData(e){const t=this.fieldStore.get(e);return t?(Array.isArray(t.uploads)?t.uploads=new Set(t.uploads):t.uploads||(t.uploads=new Set),Array.isArray(t.groups)||(t.groups=[]),t):null}async saveFieldData(e){console.log("💾 Saving:",e.id,{uploads:e.uploads?.size,groups:e.groups?.length}),await this.fieldStore.save({...e,timestamp:Date.now()})}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",getRuntimeData:e=>this.fieldElements.get(e),getStoreData:e=>this.getFieldData(e)},upload:{selector:this.selectors.items.item,key:"uploadId",getRuntimeData:e=>this.uploadElements.get(e),getStoreData:e=>this.uploadStore.get(e)},group:{selector:this.selectors.groups.container,key:"groupId",getRuntimeData:e=>this.groupElements.get(e),getStoreData:e=>{const t=this.groupElements.get(e);if(!t)return null;const s=this.getFieldData(t.fieldId);return s?.groups?.find((t=>t.id===e))}}},o=s[t];if(!o)return null;const r=e.closest(o.selector);if(!r)return null;const a=r.dataset[o.key];return{...o.getRuntimeData(a),...o.getStoreData(a)}}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){const t=this.getFromElement(e,"field");return t?.id??null}getUploadIdFromElement(e){const t=this.getFromElement(e,"upload");return t?.id??null}getGroupIdFromElement(e){const t=this.getFromElement(e,"group");return t?.id??null}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]||0}getModalType(e){if(!e?.element)return null;if(void 0!==e._cachedModalType)return e._cachedModalType;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return;s.dataset.uploadId=e.id,s.dataset.subtype=e.subtype||"image";let[o,r,a,i,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch(o&&(o.value=e.id),e.subtype){case"image":r&&(r.src=e.preview,r.alt=e.meta?.originalName||""),a?.remove(),i?.remove();break;case"video":a&&(a.src=e.preview),r?.remove(),i?.remove();break;case"document":const t=e.meta?.originalName||"",s=t.split(".").pop()?.toLowerCase()||"",o={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(o[s]||"file");i&&(i.innerText=t,i.prepend(l)),r?.remove(),a?.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,r=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,r&&(r.htmlFor=o)}})),s}normalizeFieldData(e){return e?(Array.isArray(e.uploads)?e.uploads=new Set(e.uploads):e.uploads||(e.uploads=new Set),Array.isArray(e.groups)||(e.groups=[]),e.groups=e.groups.map((e=>({...e,uploads:Array.isArray(e.uploads)?e.uploads:[]}))),e):null}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),250)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const s=await t.arrayBuffer(),o=this.uploadStore.get(e)||{id:e};o.blobData={buffer:s,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(o)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const s=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([s],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.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();console.log("Checking for stored uploads...",{fieldStates:e.length,uploadStoreSize:this.uploadStore.data.size}),console.log(this.uploadStore.getAll()),console.log(this.fieldStore.getAll());const t=e.filter((e=>{if(!e.uploads)return!1;return(e.uploads instanceof Set?Array.from(e.uploads):Array.isArray(e.uploads)?e.uploads:[]).some((e=>{const t=this.uploadStore.get(e);return t&&!t.operationId&&["completed","processed","local_processing","processed-original"].includes(t.status)}))}));console.log("Found pending fields:",t.length),0!==t.length&&this.showRecoveryNotification(t)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let o,r=window.getTemplate("restoreNotification");if(!r)return void console.error("Restore notification template not found");if(s>0){o=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else o=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const a=r.querySelector(".restore-details");a&&(a.textContent=o);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const o=e.querySelector(".item-grid.restore");for(let e of t.uploads){const s=this.uploadStore.get(e);let r=window.getTemplate("uploadItem");if(!r)continue;const a=await this.getBlobData(s.id);if(a)try{const e=this.createPreviewUrl(a);let[o,i,l,n,d]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];r.dataset.uploadId=s.id,r.dataset.fieldId=t.id;let c=this.getSubtypeFromMime(a.type);switch(r.dataset.subtype=c,c){case"image":[i.src,i.alt]=[e,a.name??s.meta?.originalName??""],l.remove(),n.remove();break;case"video":l.src=e,i.remove(),n.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")}n.innerText=s.originalFile.name,n.prepend(t),i.remove(),l.remove()}r.dataset.previewUrl=e}catch(e){console.warn("Failed to create preview for upload:",s.id,e)}const i=r.querySelector("summary span");i&&(i.textContent=s.meta?.originalName||"Unknown file");const l=r.querySelector("details");l&&s.meta&&(l.textContent=`${this.formatBytes(s.meta.size)} • ${s.meta.type}`),r.querySelectorAll("input").forEach((e=>{let t=e.id;if(t){let o=t+s.id,r=e.parentNode.querySelector(`label[for="${t}"]`);e.id=o,r&&(r.htmlFor=o)}})),o&&o.appendChild(r)}r.querySelector(".wrap").appendChild(o)}document.querySelector(".field.upload").appendChild(r),r=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection({container:r,ui:{selectAll:r.querySelector("#select-all-restore"),count:r.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}async handleRestoreAll(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{let s=e.dataset.uploadId,o=e.dataset.fieldId;t.push({uploadId:s,fieldId:o})})),await this.restoreSelectedUploads(t),this.cleanupRestore()}showSaveIndicator(e){}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){await this.fieldStore.clear(),await this.uploadStore.clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.uploadElements.clear(),this.fieldElements.clear(),this.groupElements.clear(),this.selected.clear(),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
\ No newline at end of file
+(()=>{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.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":s&&this.handleOperationCancelled(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":this.handleOperationComplete(t,s);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),o=this.buildFieldUI(e),a={id:t,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(a),this.fieldElements.set(t,{element:e,ui:o,config:s}),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"single"!==s.type&&this.initSortable(t),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initSortable(e){if(!window.Sortable)return;!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 s=t.classList.contains("group")?t.closest(".upload-group")?.dataset.groupId:null;this.createSortableForGrid(t,e,s)}));const s=t.element.querySelector(".empty-group");s&&!s.sortableInstance&&(s.sortableInstance=new Sortable(s,{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(((s,o)=>{if(o.startsWith(e)){s.el.querySelectorAll(".item").forEach((e=>{const s=e.dataset.uploadId;t.has(s)?Sortable.utils.select(e):Sortable.utils.deselect(e)}))}}))}handleDrop(e,t){const s=e.to,o=e.from,a=e.items?.length>0?e.items:[e.item],r=a.map((e=>e.dataset.uploadId));switch(this.getDropTargetType(s)){case"empty-group":this.handleDropToEmptyGroup(a,r,t);break;case"preview":default:this.handleDropToPreview(a,r,t);break;case"group":this.handleDropToGroup(a,r,s,o,t)}this.updateSortableState(s),o!==s&&this.updateSortableState(o)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}handleDropToGroup(e,t,s,o,a){try{if(s===o)return void this.handleReorder({to:s,items:e});t.forEach((e=>{this.addToGroup(e,s,!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,s){try{t.forEach((e=>{this.removeFromGroup(e)})),this.schedulePersistance(s);const o=e.length>1?`Moved ${e.length} items to preview`:"Moved item to preview";this.a11y.announce(o);const a=this.selectionHandlers.get(s);a?.clearSelection()}catch(t){this.handleDropError(e,s,t)}}handleDropError(e,t,s,o="An error occurred"){console.error("Drop error:",s);const a=this.fieldElements.get(t);a?.ui?.preview&&e.forEach((e=>a.ui.preview.appendChild(e))),this.a11y.announce(`${o}. Items returned to preview.`)}handleDropToEmptyGroup(e,t,s){try{const o=this.createGroup(s);if(!o)return void this.handleDropError(e,s,new Error("Group creation failed"),"Failed to create group");e.forEach(((e,s)=>{o.grid.appendChild(e),this.addToGroup(t[s],o.grid,!1)})),this.schedulePersistance(s);const a=e.length>1?`Created group with ${e.length} items`:"Created group with item";this.a11y.announce(a);const r=this.selectionHandlers.get(s);r?.clearSelection()}catch(t){this.handleDropError(e,s,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,s=t.closest(".field, .upload");if(!s)return;e.items&&e.items.length>0?e.items:e.item;let o=Array.from(t.querySelectorAll(".item:not(.sortable-ghost):not(.sortable-clone)")).map((e=>e.dataset.uploadId)).filter((e=>e)),a=s.querySelector('input[type="hidden"]');a&&o.length>0&&(a.value=o.join(","));const r=this.getFieldIdFromElement(t);if(r){const e=this.getFieldData(r);if(t.classList.contains("group")){const s=t.dataset.groupId,a=e?.groups?.find((e=>e.id===s));a&&(a.uploads=o)}this.schedulePersistance(r)}this.a11y.announce("Item reordered"),s.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex,items:o},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 s=Array.from(e.dataTransfer.files);if(0===s.length)return;const o=this.getFieldIdFromElement(t);o&&(this.processFiles(o,s),this.a11y.announce(`${s.length} file(s) dropped for upload`))}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}if(t){const s=this.getFieldData(t);"post_group"===s?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return;o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const 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)}`,l={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(l);const n=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,s.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:n,meta:l.meta,subtype:c},"post_group"===s.config.destination);o.ui.preview&&(o.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:n,location:o.ui.preview}));const p=this.uploadStore.get(i);return p&&(p.status="processed",await this.uploadStore.save(p)),s.uploads.add(i),await this.saveFieldData(s),r++,this.updateUploadProgress(e,r,a,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),r++,this.updateUploadProgress(e,r,a,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((o,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}`)))}),s),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),o(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 s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((o,a)=>{const r=new Image,i=document.createElement("canvas"),l=i.getContext("2d");let n=null;const d=()=>{r.onload=null,r.onerror=null,n&&(URL.revokeObjectURL(n),n=null),i.width=1,i.height=1,l.clearRect(0,0,1,1)};r.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(r,t);i.width=n,i.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(r,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);i.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});o(s)}else 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{n=this.createPreviewUrl(e),r.src=n}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,s,o){return new Promise(((a,r)=>{if(!this.worker.worker)return void r(new Error("Worker not available"));const i=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===i)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});a(s)}else r(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),r(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:i,file:e,maxDimension:s,quality:o,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n\t\t\t\tself.onmessage = async function(e) {\n\t\t\t\t\tconst { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst bitmap = await createImageBitmap(file);\n\t\t\t\t\t\tconst scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n\t\t\t\t\t\tconst width = Math.round(bitmap.width * scale);\n\t\t\t\t\t\tconst height = Math.round(bitmap.height * scale);\n\t\t\t\t\t\tconst canvas = new OffscreenCanvas(width, height);\n\t\t\t\t\t\tconst ctx = canvas.getContext('2d');\n\t\t\t\t\t\tctx.imageSmoothingEnabled = true;\n\t\t\t\t\t\tctx.imageSmoothingQuality = 'high';\n\t\t\t\t\t\tctx.drawImage(bitmap, 0, 0, width, height);\n\t\t\t\t\t\tbitmap.close();\n\t\t\t\t\t\tconst blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });\n\t\t\t\t\t\tself.postMessage({ messageId, success: true, blob: blob, format: outputFormat });\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tself.postMessage({ messageId, success: false, error: error.message });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t"],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:o}=e;if(s<=t&&o<=t)return{width:s,height:o};const a=Math.min(t/s,t/o);return{width:Math.round(s*a),height:Math.round(o*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 s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=this.getFieldGroups(e);if(0===o.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const a=[],r=new FormData;let i=[];for(const e of o){const t={images:[],fields:{}};for(let[s,o]of Object.entries(e.changes))t.fields[s]=o;const o=s.filter((t=>{const s=this.uploadStore.get(t);return s?.groupId===e.id}));for(const e of o){const s=await this.getBlobData(e);if(s){r.append("files[]",s);const o={upload_id:e,index:i.length},a=this.uploadElements.get(e),l=a?.element?.querySelector('[name="featured"]');l?.checked&&(t.fields.featured=e),t.images.push(o),i.push(e)}}a.push(t)}const l=s.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of l){const t={images:[],fields:{}},s=await this.getBlobData(e);if(s){r.append("files[]",s);const o={upload_id:e,index:i.length};t.images.push(o),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 n={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(n);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),this.a11y.announce(`Creating ${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 s=Array.from(t.uploads),o=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");const a={endpoint:"uploads",method:"POST",data:o,title:`Uploading ${s.length} file${s.length>1?"s":""} to server...`,popup:`Uploading ${s.length} file${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(a);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let o=[];const a=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const a=await this.getBlobData(e);a&&(s.append("files[]",a),o.push(t.id))}));return await Promise.all(a),s.append("upload_ids",JSON.stringify(o)),s}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),s=this.uploadStore.get(t);if(!s)return;if(!this.getFieldData(s.fieldId))return;let o={};o[e.target.name]=e.target.value,s.meta={...s.meta,...o},await this.uploadStore.save(s);let a={};a[s.attachmentId??s.id]=s.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:s.id})}}async handleOperationComplete(e,t){if((e.result?.data||e.serverData?.data||[]).forEach((e=>{const t=this.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploadStore.save(t),this.updateUploadStatus(e.upload_id,"completed"))})),!t)return;const s=this.getFieldData(t);if(!s)return;const o=Array.from(s.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of o)await this.clearUpload(e,!1),s.uploads.delete(e);0===s.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(s),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const s=this.uploadStore.get(t);s&&(s.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(s),this.updateUploadStatus(t,s.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s)await this.clearUpload(e,!1);await this.clearFieldFromStores(e),this.updateFieldState(e),this.a11y.announce("Upload cancelled")}getFieldGroups(e){const t=this.getFieldData(e);return t?.groups?t.groups.map((e=>({id:e.id,uploads:e.uploads||[],changes:e.changes||{}}))):[]}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}async restoreSelectedUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,s]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=s,await this.restoreField(t))}}async restoreField(e){const{config:t,context:s,uploads:o,groups:a,id:r}=e;s?.modalType&&await this.openModalForRestore(s);let i=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!i){const e=`${t.content}_${t.itemID}_${t.name}`;i=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!i)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=i.dataset.uploader;l&&this.fieldElements.has(l)||(l=this.registerUploader(i));const n=this.fieldElements.get(l),d=this.getFieldData(l);if(!n||!d)return void console.error("Failed to register field for restoration");d.state=e.state||"ready",n.ui||(n.ui=this.buildFieldUI(i)),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),n.ui.dropZone&&(n.ui.dropZone.hidden=!0),a&&a.length>0&&await this.restoreGroups(l,a);const c=o instanceof Set?Array.from(o):Array.isArray(o)?o:[];for(const e of c){const t=this.uploadStore.get(e);t&&await this.restoreUpload(l,t)}await this.saveFieldData(d),this.updateFieldState(l),this.maybeLockUploads(l),this.refreshSortable(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}async restoreUpload(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(!s||!o)return void console.error("Field not found for upload restoration:",e);const 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),l=this.createUploadElement({id:t.id,preview:r,meta:t.meta||{originalName:a.name,size:a.size,type:a.type},subtype:i},"post_group"===o.config.destination);let n;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){n=e.grid;const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads||(s.uploads=[]),s.uploads.includes(t.id)||s.uploads.push(t.id))}else n=s.ui.preview,t.groupId=null}else n=s.ui.preview;n?n.appendChild(l):s.ui.preview&&(s.ui.preview.appendChild(l),n=s.ui.preview),this.uploadElements.set(t.id,{element:l,preview:r,location:n}),o.uploads||(o.uploads=new Set),o.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),n&&this.updateSortableState(n)}async restoreGroups(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(s&&o){for(const s of t){const t=this.createGroup(e,s.id);if(!t){console.warn("Failed to create group:",s.id);continue}const a=o.groups?.find((e=>e.id===s.id));if(a&&(s.changes&&(a.changes={...s.changes}),s.uploads&&(a.uploads=[...s.uploads]),s.changes)){const e=t.element.querySelector('[name*="post_title"]'),o=t.element.querySelector('[name*="post_excerpt"]');e&&s.changes.post_title&&(e.value=s.changes.post_title),o&&s.changes.post_excerpt&&(o.value=s.changes.post_excerpt)}}await this.saveFieldData(o)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:s}=e;let o=null;switch(t){case"create":o=document.querySelector('[data-action="create"]');break;case"edit":s&&(o=document.querySelector(`[data-action="edit"][data-id="${s}"]`));break;case"bulkEdit":o=document.querySelector('[data-action="bulk-edit"]')}o?(o.click(),await new Promise((e=>setTimeout(e,300)))):console.warn("Modal trigger not found for restoration:",e)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,o=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,o)).toFixed(s))+" "+["Bytes","KB","MB","GB"][o]}async clearUpload(e,t=!0){const s=this.uploadElements.get(e);if(s&&(this.revokePreviewUrl(s.preview),s.element)){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}if(this.uploadElements.delete(e),await this.uploadStore.delete(e),t){const t=this.uploadStore.get(e);t?.fieldId&&await this.schedulePersistance(t.fieldId)}}async clearFieldFromStores(e){const t=this.getFieldData(e);if(t?.uploads){const e=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const t of e)await this.uploadStore.delete(t)}await this.fieldStore.delete(e)}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}updateFieldState(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t||!s)return;const o=t.element,a=s.uploads?.size||0,r=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;o.dataset.hasUploads=a>0?"true":"false",o.dataset.uploadCount=a.toString(),o.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,s,o){const a=this.fieldElements.get(e);if(!a?.ui?.progress?.progress)return;const r=a.ui.progress,i=s>0?t/s*100:0;r.fill&&(r.fill.style.width=`${i}%`),r.text&&(r.text.textContent=o),r.count&&(r.count.textContent=`${t}/${s}`),r.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.getFieldData(e);s&&(s.state=t,this.saveFieldData(s))}updateUploadStatus(e,t){const s=this.uploadStore.get(e);s&&(s.status=t,this.uploadStore.save(s),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),s=this.uploadStore.get(e);if(!s||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${s.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(s.status),s.status)}showUploadProgress(e,t=!0){const s=this.uploadElements.get(e);if(!s?.element)return;const o=s.element.querySelector(".progress");o&&(t?(o.style.removeProperty("animation"),o.hidden=!1):(o.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{o.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const o=this.uploadElements.get(e);if(!o?.element)return;const a=o.element.querySelector(".progress");if(!a)return;const r=a.querySelector(".fill"),i=a.querySelector(".details"),l=a.querySelector(".icon");r&&(r.style.width=`${t}%`),s&&i&&(i.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t?.ui?.dropZone||!s)return;const o=s.uploads?.size||0,a="post_group"===s.config.destination?20:s.config?.maxFiles||999;t.ui.dropZone.hidden=o>=a,t.element.classList.toggle("at-max-uploads",o>=a),"post_group"===s.config.destination&&o>=a&&this.a11y.announce("Maximum of 20 uploads reached. Please submit current uploads before adding more.")}createSortableForGrid(e,t,s=null){if(!e||e.sortableInstance)return;const o=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=o;const a=s?`${t}-group-${s}`:`${t}-preview`;return this.sortableInstances.set(a,o),o}createGroup(e,t=null){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const a=this.createGroupElement(t,e);if(!a)return null;o.ui.groups||(o.ui.groups={groups:new Map,container:null,empty:null,display:null}),o.ui.groups.groups.set(t,a),o.ui.groups.container&&o.ui.groups.empty?o.ui.groups.container.insertBefore(a,o.ui.groups.empty):o.ui.groups.container&&o.ui.groups.container.appendChild(a);const r=a.querySelector(".item-grid.group");this.groupElements.set(t,{element:a,grid:r,fieldId:e}),s.groups||(s.groups=[]);return s.groups.find((e=>e.id===t))||(s.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(s)),this.addGroupSelectionHandler(e,t),r&&this.createSortableForGrid(r,e,t),{id:t,element:a,grid:r}}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let o=window.getTemplate("groupMetadata");const a=s.querySelector(".fields");if(a&&o){a.append(o);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 l=this.getFieldData(t);if(l&&""!==l.config.content){let e=s.querySelector("summary");e&&(e.textContent=l.config.content+" Fields")}}else s.querySelector("details")?.remove();const r=s.querySelector(".item-grid.group");return r&&(r.dataset.groupId=e),s}deleteGroup(e,t=!0){const s=this.groupElements.get(e);if(!s)return;const o=this.getFieldData(s.fieldId);if(!o)return;const a=o.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)})),o.groups&&(o.groups=o.groups.filter((t=>t.id!==e)),this.saveFieldData(o)),s.element&&(s.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${s.fieldId}-group-${e}`,l=this.sortableInstances.get(i);l?.destroy&&l.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){const o=this.uploadStore.get(e),a=this.uploadElements.get(e);if(!o||!a)return;const r=this.getFieldData(o.fieldId),i=this.fieldElements.get(o.fieldId);if(!r||!i)return;if(!t&&a.location===i.ui.preview||t===a.location)return;if(o.groupId){const t=r.groups?.find((e=>e.id===o.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(o.groupId))}const l=a.element.querySelector('[name*="select-item"]');l&&(l.checked=!1);let n=a.element.querySelector('[name="featured"]');if(n&&(n.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,o.groupId=null;else{const s=t.dataset.groupId;n&&(n.name=s+"_"+n.name);const a=r.groups?.find((e=>e.id===s));a&&(a.uploads||(a.uploads=[]),a.uploads.push(e),o.groupId=s)}a.location=t,t.append(a.element),this.uploadStore.save(o),s&&this.saveFieldData(r),this.updateSortableState(t),a.location&&a.location!==t&&this.updateSortableState(a.location)}removeFromGroup(e){const t=this.uploadStore.get(e),s=this.uploadElements.get(e);if(!t||!s)return;const o=this.getFieldData(t.fieldId),a=this.fieldElements.get(t.fieldId);if(!o||!a)return;if(t.groupId){const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}a.ui?.preview&&(a.ui.preview.appendChild(s.element),s.location=a.ui.preview);const r=s.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.uploadStore.save(t),this.updateSortableState(a.ui.preview)}removeUpload(e,t){const s=this.getFieldData(e),o=this.uploadStore.get(t),a=this.uploadElements.get(t);if(!s||!o)return;if(s.uploads?.delete(t),o.groupId){const e=s.groups?.find((e=>e.id===o.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(o.groupId))}a?.element?.remove(),this.clearUpload(t),this.saveFieldData(s),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 s=this.getFieldData(t.fieldId),o=s?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!o)return;o.changes||(o.changes={});let a=e.name;a.includes("group")&&(a=a.replace(`${o.id}_`,"").replace(`${o.id}[`,"").replace("]","")),o.changes[a]=e.value,this.saveFieldData(s),this.schedulePersistance(t.fieldId)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":const t=this.fieldElements.get(s);t&&(t.element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s));break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"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),s=t?.dataset.uploader;if(!s)return;const o=this.selected.get(s);if(o&&0!==o.size){const e=this.createGroup(s);if(!e)return;o.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${o.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,o=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(o)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,o=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(o,s),this.a11y.announce("Item removed"),this.schedulePersistance(o))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fieldElements.get(e);if(!t?.element)return;const s=new window.jvbHandleSelection({container:t.element,ui:{selectAll:t.element.querySelector('[name="select-all-uploads"]'),bulkControls:t.element.querySelector(".selection-actions"),count:t.element.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return s.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.syncSortableSelection(e,s.selectedItems),this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,s),s}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const o=this.groupElements.get(t);if(!o?.element)return;const a=new window.jvbHandleSelection({container:o.element,ui:{selectAll:o.element.querySelector(this.selectors.groups.selectAll),bulkControls:o.element.querySelector(this.selectors.groups.actions),count:o.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return a.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,a),a}handleSelectAll(e,t){}getCurrentSelection(e){let t=[];for(let[s,o]of this.selectionHandlers)(e===s||s.includes(e))&&o.selectedItems.size>0&&(t=t.concat([...o.selectedItems]));return t}getFieldData(e){const t=this.fieldStore.get(e);return t?(Array.isArray(t.uploads)?t.uploads=new Set(t.uploads):t.uploads||(t.uploads=new Set),Array.isArray(t.groups)||(t.groups=[]),t):null}async saveFieldData(e){await this.fieldStore.save({...e,timestamp:Date.now()})}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",getRuntimeData:e=>this.fieldElements.get(e),getStoreData:e=>this.getFieldData(e)},upload:{selector:this.selectors.items.item,key:"uploadId",getRuntimeData:e=>this.uploadElements.get(e),getStoreData:e=>this.uploadStore.get(e)},group:{selector:this.selectors.groups.container,key:"groupId",getRuntimeData:e=>this.groupElements.get(e),getStoreData:e=>{const t=this.groupElements.get(e);if(!t)return null;const s=this.getFieldData(t.fieldId);return s?.groups?.find((t=>t.id===e))}}},o=s[t];if(!o)return null;const a=e.closest(o.selector);if(!a)return null;const r=a.dataset[o.key];return{...o.getRuntimeData(r),...o.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}getModalType(e){if(!e?.element)return null;if(void 0!==e._cachedModalType)return e._cachedModalType;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return;s.dataset.uploadId=e.id,s.dataset.subtype=e.subtype||"image";let[o,a,r,i,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch(o&&(o.value=e.id),e.subtype){case"image":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||"",s=t.split(".").pop()?.toLowerCase()||"",o={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(o[s]||"file");i&&(i.innerText=t,i.prepend(l)),a?.remove(),r?.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,a=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,a&&(a.htmlFor=o)}})),s}normalizeFieldData(e){return e?(Array.isArray(e.uploads)?e.uploads=new Set(e.uploads):e.uploads||(e.uploads=new Set),Array.isArray(e.groups)||(e.groups=[]),e.groups=e.groups.map((e=>({...e,uploads:Array.isArray(e.uploads)?e.uploads:[]}))),e):null}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),250)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const s=await t.arrayBuffer(),o=this.uploadStore.get(e)||{id:e};o.blobData={buffer:s,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(o)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const s=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([s],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.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&&this.showRecoveryNotification(e)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let o,a=window.getTemplate("restoreNotification");if(!a)return void console.error("Restore notification template not found");if(s>0){o=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else o=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const r=a.querySelector(".restore-details");r&&(r.textContent=o);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const o=e.querySelector(".item-grid.restore");for(let e of t.uploads){const s=this.uploadStore.get(e);let a=window.getTemplate("uploadItem");if(!a)continue;const r=await this.getBlobData(s.id);if(r)try{const e=this.createPreviewUrl(r);let[o,i,l,n,d]=[a.querySelector('[name="featured"]'),a.querySelector("img"),a.querySelector("video"),a.querySelector("label > span"),a.querySelector("details")];a.dataset.uploadId=s.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??s.meta?.originalName??""],l.remove(),n.remove();break;case"video":l.src=e,i.remove(),n.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")}n.innerText=s.originalFile.name,n.prepend(t),i.remove(),l.remove()}a.dataset.previewUrl=e}catch(e){console.warn("Failed to create preview for upload:",s.id,e)}const i=a.querySelector("summary span");i&&(i.textContent=s.meta?.originalName||"Unknown file");const l=a.querySelector("details");l&&s.meta&&(l.textContent=`${this.formatBytes(s.meta.size)} • ${s.meta.type}`),a.querySelectorAll("input").forEach((e=>{let t=e.id;if(t){let o=t+s.id,a=e.parentNode.querySelector(`label[for="${t}"]`);e.id=o,a&&(a.htmlFor=o)}})),o&&o.appendChild(a)}a.querySelector(".wrap").appendChild(o)}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 s=e.dataset.uploadId,o=e.dataset.fieldId;t.push({uploadId:s,fieldId:o})})),await this.restoreSelectedUploads(t),this.cleanupRestore()}showSaveIndicator(e){}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){await this.fieldStore.clear(),await this.uploadStore.clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.uploadElements.clear(),this.fieldElements.clear(),this.groupElements.clear(),this.selected.clear(),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(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 bcac47e..1f5ffb6 100644
--- a/assets/js/min/utility.min.js
+++ b/assets/js/min/utility.min.js
@@ -1 +1 @@
-(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),a=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:a<7?`${a} ${1===a?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.icon=null,window.getIcon=function(t,e=""){if(void 0===t)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return e=""!==e&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${e.slice(0,2)}`:"",n.classList.add(`icon-${t}${e}`),n},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let a=!0;return async function(){for(;a;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){a=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return Array.isArray(e)&&(e=e.join(",")),"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}}})();
\ No newline at end of file
+(()=>{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
diff --git a/assets/js/min/view.min.js b/assets/js/min/view.min.js
index 7f934f3..43d6c0a 100644
--- a/assets/js/min/view.min.js
+++ b/assets/js/min/view.min.js
@@ -1 +1 @@
-window.jvbViews=class{constructor(e,t){this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.settings=window.jvbUserSettings,this.store=t,this.isTimeline=!!document.querySelector("[data-timeline]"),this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.subscribers=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"form.table",form:"table",body:"table body",header:"table thead",footer:"table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container)}init(){this.store.subscribe(((e,t)=>{switch(e){case"items-saved":case"item-saved":case"item-deleted":break;case"data-loaded":this.handleItemsUpdate()}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const r=Math.min(s,l),a=Math.max(s,l);let d=0;for(let e=r;e<=a;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){this.ui.table.columns.filter((t=>t.className===e))}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}));const e=document.querySelector("[data-view]:checked");e&&(this.currentView=e.dataset.view)}handleDataUpdate(e){console.log(e);const t=e.data?.items||e.items||[];this.render(t)}handleItemsUpdate(){this.render()}render(){if(!this.store)return void console.error("No store connected to renderer");const e=this.store.getFiltered();if(0!==e.length){switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}else this.renderEmpty()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.getTemplate("emptyState");e&&(this.ui.grid.appendChild(e),this.a11y?.announce("No items found"))}renderGrid(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderGridItem(e);t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){if(this.items.grid.has(e.id))return this.items.grid.get(e.id);const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,r,a]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,r.dataset.id,a.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.id,e.id],[l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""],this.items.grid.set(e.id,t),t}toggleTable(e){if(this.ui.table.selectedColumns.hidden=!e,e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.form=this.ui.table.table.querySelector("table"),this.ui.table.header=this.ui.table.form.querySelector("thead"),this.ui.table.footer=this.ui.table.form.querySelector("tfoot"),this.ui.table.body=this.ui.table.form.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,e?this.notify("table-view",this.ui.table.table):this.notify("not-table-view",this.ui.table.table),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns.hidden=!e}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t=this.isTimeline?this.renderTimelineTableItem(e):this.renderTableItem(e);this.ui.table.body?this.ui.table.body.append(t):(this.ui.table.footer||(this.ui.table.footer=this.ui.table.table.querySelector("tfoot")),this.ui.table.form.insertBefore(t,this.ui.table.footer))})),window.jvbSelector.scanExistingFields()}renderTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);const t=window.getTemplate("tableView");return t.dataset.id=e.id,[t.querySelector(".select-item").id,t.querySelector(".select-item").value,t.querySelector(".select-item").checked,t.querySelector(".select-item + label").htmlFor,t.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],new window.jvbPopulate(t,e.fields,e.images),this.cleanupTableRow(t),this.items.table.set(e.id,t),t}renderTimelineTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);const t=window.getTemplate("tableView");t.dataset.id=e.id,[t.querySelector(".select-item").id,t.querySelector(".select-item").value,t.querySelector(".select-item").checked,t.querySelector(".select-item + label").htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id];let i=t.querySelector(".timeline-point"),s=t,l=t.querySelector("tr.shared");return new window.jvbPopulate(l,e.fields,e.images),this.prefixTimelineFieldNames(l,e.id),this.cleanupTableRow(l),e.fields.timeline&&"object"==typeof e.fields.timeline&&Object.entries(e.fields.timeline).forEach((([t,l],r)=>{let a=i.cloneNode(!0);a.dataset.index=r,a.dataset.imageId=t,new window.jvbPopulate(a,l,e.images),this.cleanupTableRow(a);let d=e.images[l.post_thumbnail];d&&(a.querySelector(".field.upload").title=d["image-title"]),this.prefixTimelineFieldNames(a,l.id),s.insertBefore(a,i)})),i.remove(),this.items.table.set(e.id,t),t}prefixTimelineFieldNames(e,t){e.querySelectorAll("input, textarea, select").forEach((e=>{const i=e.name;if(!i||i.startsWith("[")||"form-id"===i||i.startsWith("_"))return;let s=e.nextElementSibling;e.name=`[${t}]${i}`,s&&"LABEL"===s.tagName&&(e.id=`[${t}]${e.id}`,s.htmlFor=e.id)}))}cleanupTableRow(e){e.querySelectorAll("td[data-field]").forEach((e=>{e.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach((e=>{e.closest(".radio-options")||e.remove()})),"true_false"===e.dataset.fieldType&&e.querySelector(".toggle-label")?.remove(),["checkbox","radio","select"].includes(e.dataset.fieldType)&&e.querySelector(".label")?.remove()}))}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t=this.renderListItem(e);this.ui.grid.appendChild(t)}))}renderListItem(e){if(this.items.list.has(e.id))return this.items.list.get(e.id);const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),this.items.list.set(e.id,t),t}setupTimelineDragHandler(){this.isTimeline&&"table"===this.currentView&&(this.timelineDragHandler&&this.timelineDragHandler.destroy(),this.timelineDragHandler=new window.jvbDragHandler({draggableSelector:".timeline-point",dropTargetSelector:".timeline-point",handleSelector:".drag-handle",getItemId:e=>e.dataset.imageId,getSelectedItems:()=>[],validateDrop:(e,t)=>{const i=document.querySelector(`.timeline-point[data-image-id="${e[0]}"]`);return!!i&&i.closest("tbody")===t.closest("tbody")},onDragStart:(e,t)=>{t.classList.add("is-dragging")},onDrop:(e,t)=>{const i=document.querySelector(`.timeline-point[data-image-id="${e[0]}"]`);if(!i)return;document.querySelectorAll(".drop-above, .drop-below").forEach((e=>{e.classList.remove("drop-above","drop-below")}));const s=i.closest("tbody");"above"===t.dataset.dropPosition?s.insertBefore(i,t):s.insertBefore(i,t.nextSibling),i.classList.remove("is-dragging"),this.updateTimelineOrder(s)},onDragEnd:(e,t)=>{document.querySelectorAll(".is-dragging, .drop-above, .drop-below").forEach((e=>{e.classList.remove("is-dragging","drop-above","drop-below")}))},previewElement:".drag-handle",previewOptions:{offset:{x:-20,y:-20},showCount:!1}}),this.addTimelineDragHoverLogic())}addTimelineDragHoverLogic(){let e=null;document.addEventListener("pointermove",(t=>{if(!document.querySelector(".timeline-point.is-dragging"))return;const i=t.target.closest(".timeline-point:not(.is-dragging)");if(!i)return void(e&&(e.classList.remove("drop-above","drop-below"),delete e.dataset.dropPosition,e=null));const s=i.getBoundingClientRect(),l=s.top+s.height/2,r=t.clientY<l;e&&e!==i&&(e.classList.remove("drop-above","drop-below"),delete e.dataset.dropPosition),i.classList.remove("drop-above","drop-below"),i.classList.add(r?"drop-above":"drop-below"),i.dataset.dropPosition=r?"above":"below",e=i}))}updateTimelineOrder(e){const t=parseInt(e.dataset.id),i=Array.from(e.querySelectorAll(".timeline-point")),s=this.store.get(t);if(!s)return;let l={};i.forEach(((e,t)=>{const i=e.dataset.imageId;l[i]=s.fields.timeline[i]})),s.fields.timeline=l,this.store.save(s),this.notify("order-changed",t),this.a11y?.announce(`Timeline order updated. ${i.length} steps reordered.`)}extractRowFields(e){const t={};return e.querySelectorAll("[data-field]").forEach((e=>{const i=e.dataset.field,s=e.querySelector("input, textarea, select");s&&("checkbox"===s.type?t[i]=s.checked:t[i]=s.value)})),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((i=>i(e,t)))}};
\ No newline at end of file
+window.jvbViews=class{constructor(e,t){this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.settings=window.jvbUserSettings,this.store=t,this.isTimeline=!!document.querySelector("[data-timeline]"),this.items={list:new Map,grid:new Map,table:new Map},this.currentView=this.container.dataset.view??"grid",this.selectedItems=new Set,this.subscribers=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"form.table",form:"table",body:"table body",header:"table thead",footer:"table tfoot",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container)}init(){this.store.subscribe(((e,t)=>{switch(e){case"items-saved":case"item-saved":case"item-deleted":break;case"data-loaded":this.handleItemsUpdate()}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const r=Math.min(s,l),a=Math.max(s,l);let d=0;for(let e=r;e<=a;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){this.ui.table.columns.filter((t=>t.className===e))}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}));const e=document.querySelector("[data-view]:checked");e&&(this.currentView=e.dataset.view)}handleItemsUpdate(){this.render()}render(){if(!this.store)return void console.error("No store connected to renderer");const e=this.store.getFiltered();if(0===e.length)return console.log("Nothing to show"),void this.renderEmpty();switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.getTemplate("emptyState");e&&(this.ui.grid.appendChild(e),this.a11y?.announce("No items found"))}renderGrid(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i=this.renderGridItem(e);t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){if(this.items.grid.has(e.id))return this.items.grid.get(e.id);const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,r,a]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,r.dataset.id,a.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.id,e.id],[l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""],this.items.grid.set(e.id,t),t}toggleTable(e){if(this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e),e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.form=this.ui.table.table.querySelector("table"),this.ui.table.header=this.ui.table.form.querySelector("thead"),this.ui.table.footer=this.ui.table.form.querySelector("tfoot"),this.ui.table.body=this.ui.table.form.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,e?this.notify("table-view",this.ui.table.table):this.notify("not-table-view",this.ui.table.table),this.ui.table.body&&window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns&&(this.ui.table.selectedColumns.hidden=!e)}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t=this.isTimeline?this.renderTimelineTableItem(e):this.renderTableItem(e);this.ui.table.body?this.ui.table.body.append(t):(this.ui.table.footer||(this.ui.table.footer=this.ui.table.table.querySelector("tfoot")),this.ui.table.form.insertBefore(t,this.ui.table.footer))})),window.jvbSelector.scanExistingFields()}renderTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);const t=window.getTemplate("tableView");t.dataset.id=e.id,[t.querySelector(".select-item").id,t.querySelector(".select-item").value,t.querySelector(".select-item").checked,t.querySelector(".select-item + label").htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id];let i=t.querySelector(`input[name="post_status"][value="${e.status}"]`);if(i&&(i.checked=!0),Object.hasOwn(this.ui.table.table.dataset,"edit"))new window.jvbPopulate(t,e.fields,e.images);else for(let[i,s]of Object.entries(e)){let e=t.querySelector(`[data-field="${i}"]`);if(e){let t=e.querySelector("p");"date"===e.dataset.fieldType&&(s=window.formatTimeAgo(s)),t.textContent=s}}return this.cleanupTableRow(t),this.items.table.set(e.id,t),t}renderTimelineTableItem(e){if(this.items.table.has(e.id))return this.items.table.get(e.id);const t=window.getTemplate("tableView");t.dataset.id=e.id,[t.querySelector(".select-item").id,t.querySelector(".select-item").value,t.querySelector(".select-item").checked,t.querySelector(".select-item + label").htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id];let i=t.querySelector(".timeline-point"),s=t,l=t.querySelector("tr.shared");return new window.jvbPopulate(l,e.fields,e.images),this.prefixTimelineFieldNames(l,e.id),this.cleanupTableRow(l),e.fields.timeline&&"object"==typeof e.fields.timeline&&Object.entries(e.fields.timeline).forEach((([t,l],r)=>{let a=i.cloneNode(!0);a.dataset.index=r,a.dataset.imageId=t,new window.jvbPopulate(a,l,e.images),this.cleanupTableRow(a);let d=e.images[l.post_thumbnail];d&&(a.querySelector(".field.upload").title=d["image-title"]),this.prefixTimelineFieldNames(a,l.id),s.insertBefore(a,i)})),i.remove(),this.items.table.set(e.id,t),t}prefixTimelineFieldNames(e,t){e.querySelectorAll("input, textarea, select").forEach((e=>{const i=e.name;if(!i||i.startsWith("[")||"form-id"===i||i.startsWith("_"))return;let s=e.nextElementSibling;e.name=`[${t}]${i}`,s&&"LABEL"===s.tagName&&(e.id=`[${t}]${e.id}`,s.htmlFor=e.id)}))}cleanupTableRow(e){e.querySelectorAll("td[data-field]").forEach((e=>{e.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach((e=>{e.closest(".radio-options")||e.remove()})),"true_false"===e.dataset.fieldType&&e.querySelector(".toggle-label")?.remove(),["checkbox","radio","select"].includes(e.dataset.fieldType)&&e.querySelector(".label")?.remove()}))}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t=this.renderListItem(e);this.ui.grid.appendChild(t)}))}renderListItem(e){if(this.items.list.has(e.id))return this.items.list.get(e.id);const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),this.items.list.set(e.id,t),t}setupTimelineDragHandler(){this.isTimeline&&"table"===this.currentView&&(this.timelineDragHandler&&this.timelineDragHandler.destroy(),this.timelineDragHandler=new window.jvbDragHandler({draggableSelector:".timeline-point",dropTargetSelector:".timeline-point",handleSelector:".drag-handle",getItemId:e=>e.dataset.imageId,getSelectedItems:()=>[],validateDrop:(e,t)=>{const i=document.querySelector(`.timeline-point[data-image-id="${e[0]}"]`);return!!i&&i.closest("tbody")===t.closest("tbody")},onDragStart:(e,t)=>{t.classList.add("is-dragging")},onDrop:(e,t)=>{const i=document.querySelector(`.timeline-point[data-image-id="${e[0]}"]`);if(!i)return;document.querySelectorAll(".drop-above, .drop-below").forEach((e=>{e.classList.remove("drop-above","drop-below")}));const s=i.closest("tbody");"above"===t.dataset.dropPosition?s.insertBefore(i,t):s.insertBefore(i,t.nextSibling),i.classList.remove("is-dragging"),this.updateTimelineOrder(s)},onDragEnd:(e,t)=>{document.querySelectorAll(".is-dragging, .drop-above, .drop-below").forEach((e=>{e.classList.remove("is-dragging","drop-above","drop-below")}))},previewElement:".drag-handle",previewOptions:{offset:{x:-20,y:-20},showCount:!1}}),this.addTimelineDragHoverLogic())}addTimelineDragHoverLogic(){let e=null;document.addEventListener("pointermove",(t=>{if(!document.querySelector(".timeline-point.is-dragging"))return;const i=t.target.closest(".timeline-point:not(.is-dragging)");if(!i)return void(e&&(e.classList.remove("drop-above","drop-below"),delete e.dataset.dropPosition,e=null));const s=i.getBoundingClientRect(),l=s.top+s.height/2,r=t.clientY<l;e&&e!==i&&(e.classList.remove("drop-above","drop-below"),delete e.dataset.dropPosition),i.classList.remove("drop-above","drop-below"),i.classList.add(r?"drop-above":"drop-below"),i.dataset.dropPosition=r?"above":"below",e=i}))}updateTimelineOrder(e){const t=parseInt(e.dataset.id),i=Array.from(e.querySelectorAll(".timeline-point")),s=this.store.get(t);if(!s)return;let l={};i.forEach(((e,t)=>{const i=e.dataset.imageId;l[i]=s.fields.timeline[i]})),s.fields.timeline=l,this.store.save(s),this.notify("order-changed",t),this.a11y?.announce(`Timeline order updated. ${i.length} steps reordered.`)}extractRowFields(e){const t={};return e.querySelectorAll("[data-field]").forEach((e=>{const i=e.dataset.field,s=e.querySelector("input, textarea, select");s&&("checkbox"===s.type?t[i]=s.checked:t[i]=s.value)})),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((i=>i(e,t)))}};
\ No newline at end of file
diff --git a/assets/js/on-this-page.min.js b/assets/js/on-this-page.min.js
deleted file mode 100644
index e49f293..0000000
--- a/assets/js/on-this-page.min.js
+++ /dev/null
@@ -1 +0,0 @@
-class OnThisPage extends UIHandler{constructor(){super(),this.navOpen=!1,this.toggleNav=this.toggleNav.bind(this),this.bindElements(),this.elements.nav&&(this.elements.toggle&&(this.elements.toggle.addEventListener("click",this.toggleNav),this.bindEvents()),this.setupSectionObserver())}bindElements(){const e=document.querySelector("nav.on-this-page");e&&(this.elements={nav:e,toggle:e.querySelector("button.toggle"),links:e.querySelectorAll("a"),sections:Array.from(e.querySelectorAll("a")).map((e=>{const t=e.getAttribute("href");return document.querySelector(t)})).filter(Boolean)})}bindComponentEvents(){}toggleNav(e){e?.preventDefault(),e?.stopPropagation();const{nav:t,toggle:n}=this.elements;t&&n&&(this.navOpen=!this.navOpen,this.navOpen?(t.classList.add("open"),n.setAttribute("aria-label","Hide Index"),n.setAttribute("aria-expanded","true"),this.bindLinkHandlers()):(t.classList.remove("open"),n.setAttribute("aria-label","Show Index"),n.setAttribute("aria-expanded","false"),this.cleanupLinkHandlers()))}bindLinkHandlers(){const{links:e}=this.elements;e?.forEach((e=>{e._boundHandler=()=>{this.navOpen=!1,this.elements.nav.classList.remove("open"),this.elements.toggle.setAttribute("aria-label","Show Index"),this.elements.toggle.setAttribute("aria-expanded","false"),this.cleanupLinkHandlers()},e.addEventListener("click",e._boundHandler)}))}cleanupLinkHandlers(){const{links:e}=this.elements;e?.forEach((e=>{e._boundHandler&&(e.removeEventListener("click",e._boundHandler),delete e._boundHandler)}))}setupSectionObserver(){const{sections:e}=this.elements;e?.length&&this.initializeObserver("sections",e,{rootMargin:"-50% 0% -50% 0%",threshold:0},(e=>{e.forEach((e=>{if(!e.isIntersecting)return;const t=e.target.id,n=this.elements.nav?.querySelector(`a[href="#${t}"]`);n&&this.updateActiveClasses(n)}))}))}updateActiveClasses(e){const t=e.closest("li");if(!t)return;this.elements.nav.querySelectorAll("li").forEach((e=>{e.classList.remove("active","adj")})),t.classList.add("active"),t.previousElementSibling&&t.previousElementSibling.classList.add("adj"),t.nextElementSibling&&t.nextElementSibling.classList.add("adj")}isComponentActive(e){return"nav"===e?this.navOpen:super.isComponentActive(e)}handleOutsideClick(e){this.navOpen&&!this.elements.nav.contains(e.target)&&this.toggleNav(e)}handleEscapeKey(e){"Escape"===e.key&&this.navOpen&&(this.toggleNav(e),e.preventDefault())}cleanup(){this.cleanupLinkHandlers(),super.cleanup()}}document.addEventListener("DOMContentLoaded",(()=>{document.querySelector("nav.on-this-page")&&(window.onThisPage=new OnThisPage)}));
\ No newline at end of file
diff --git a/base/seo.php b/base/seo.php
new file mode 100644
index 0000000..5fa7d08
--- /dev/null
+++ b/base/seo.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * JVB_SCHEMA: Site-wide schema configuration
+ *
+ * Structure:
+ *   - business: LocalBusiness/Organization for home page
+ *   - website: WebSite schema configuration
+ *   - actions: PotentialAction definitions
+ *   - attribution: Developer/maintainer info
+ */
+
+use JVBase\managers\SEO\SchemaBuilder;
+
+$schema = apply_filters('jvb_schema', []);
+$registry = SchemaBuilder::getInstance();
+$checked = [];
+foreach ($schema as $key => $config) {
+
+	if (array_key_exists('type', $config)) {
+		$type = $config['type'];
+	} elseif ($key === 'website') {
+		$type = 'WebSite';
+	}
+	$exists = !is_null($registry->getTypeDefinition($type));
+	if (!$exists) {
+//		error_log('[JVB_SCHEMA] No definitions for: '.print_r($type, true));
+		continue;
+	}
+	$allowed = $registry->getFieldsForType($type);
+	$filtered = array_filter($config, function ($item) use ($allowed) {
+		return in_array($item, $allowed);
+	}, ARRAY_FILTER_USE_KEY);
+
+	if (empty($filtered)) {
+//		error_log('[JVB_SCHEMA] No valid filters for '.$type.'.');
+		continue;
+	}
+	$removed = array_filter($config, function ($item) use ($allowed) {
+		return !in_array($item, $allowed);
+	}, ARRAY_FILTER_USE_KEY);
+
+	if (!empty($removed)) {
+//		error_log('[JVB_SCHEMA] Invalid fields detected for '.$type.': '.print_r($removed, true));
+	}
+	$checked[$key] = $filtered;
+}
+
+define('JVB_SCHEMA', $checked);
+
+
+/**
+JVB_CONTENT['artwork'] = [
+	'singular' => 'Artwork',
+	'plural' => 'Artworks',
+	// ... other config
+
+	'seo' => [
+		'meta' => [
+			'title' => '{{post_title}} by {{linked_user.display_name}} | {{site_title}}',
+			'description' => '{{style.primary.name}} artwork by {{linked_user.display_name}}. {{short_bio|default:View this piece and more.}}',
+			'archive_title' => 'Artwork Gallery',
+			'archive_description' => 'Browse our collection of tattoo artwork and designs.',
+		],
+		'schema' => [
+			'type' => 'VisualArtwork',
+			'mappings' => [
+				'artform' => 'style.primary',           // DefinedTerm from taxonomy
+				'creator' => 'linked_user',             // Person from linked user
+				'image' => 'featured_image',            // ImageObject
+				'artMedium' => 'medium.names:3',        // Comma-separated term names
+			],
+			'overrides' => [
+				'inLanguage' => 'en',
+			]
+		]
+	]
+];
+
+JVB_CONTENT['artist'] = [
+	'singular' => 'Artist',
+	'plural' => 'Artists',
+
+	'seo' => [
+		'meta' => [
+			'title' => '{{post_title}} - {{artist_type|default:Tattoo Artist}} in {{city.primary.name|default:Edmonton}}',
+			'description' => '{{short_bio|truncate:155}}',
+		],
+		'schema' => [
+			'type' => 'Person',
+			'mappings' => [
+				'image' => 'image_portrait',
+				'jobTitle' => 'artist_type',
+				'worksFor' => 'shop.primary',           // LocalBusiness reference
+				'knowsAbout' => 'style.names',          // Array of style names
+				'areaServed' => 'city.primary.name',
+			]
+		]
+	]
+];
+
+JVB_TAXONOMY['shop'] = [
+	'singular' => 'Shop',
+	'plural' => 'Shops',
+
+	'seo' => [
+		'meta' => [
+			'title' => '{{term_name}} - Tattoo Shop in {{city.primary.name|default:Edmonton}}',
+			'description' => '{{tagline|default:Visit}} {{term_name}}. {{short_bio|truncate:120}}',
+		],
+		'schema' => [
+			'type' => 'TattooParlor',  // or LocalBusiness
+			'mappings' => [
+				'address' => 'location',
+				'telephone' => 'phone',
+				'email' => 'email',
+				'openingHoursSpecification' => 'hours',
+				'image' => 'image',
+				'priceRange' => 'price_range',
+				'paymentAccepted' => 'payment_accepted',
+			],
+			'overrides' => [
+				'additionalType' => 'https://schema.org/TattooParlor',
+			]
+		]
+	]
+];
+
+JVB_TAXONOMY['style'] = [
+	'singular' => 'Style',
+	'plural' => 'Styles',
+
+	'seo' => [
+		'meta' => [
+			'title' => '{{term_name}} Tattoos in Edmonton | {{site_title}}',
+			'description' => '{{tagline|default:Explore}} {{term_name}} tattoo artists and designs. {{characteristics|strip|truncate:100}}',
+		],
+		'schema' => [
+			'type' => 'DefinedTerm',
+			'mappings' => [
+				'alternateName' => 'alternate_name',
+			]
+		]
+	]
+];
+
+**/
diff --git a/build/faq/style-index-rtl.css b/build/faq/style-index-rtl.css
index 2f27fa1..2d0bcfc 100644
--- a/build/faq/style-index-rtl.css
+++ b/build/faq/style-index-rtl.css
@@ -1 +1 @@
-nav#faq{--height:fit-content;background-color:var(--base-100);border-radius:var(--outerRadius);display:block;padding:1.5rem;touch-action:auto}nav#faq ol{counter-reset:faq;display:block;height:-moz-fit-content;height:fit-content;list-style:decimal-leading-zero}nav#faq ol li{counter-increment:faq}nav#faq ol li:before{content:counter(faq);display:block;font-family:var(--heading);font-weight:var(--hBold)}nav#faq h2{font-size:var(--large);right:0;margin:.5rem 0}nav#faq a{padding:.5rem}.faq-block{max-width:none;padding-bottom:3rem;width:100%}.faq-block>*{margin:1rem auto;max-width:var(--alignWide)}.faq-block h2{margin:5rem 0 1.5rem}.faq-block h3{margin:0;text-transform:none}.faq-block :target{background-color:var(--base);outline:none}.faq-block :target h2{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem 1.5rem}.faq-block details{margin:1rem auto;max-width:var(--maxWidth);padding:.75rem}.faq-block details+details{margin-top:3rem}.faq-block details .button{display:block;height:-moz-fit-content;height:fit-content;margin-right:auto}
+nav#faq{background-color:var(--base-100);border-radius:var(--radius-outer);display:block;height:-moz-max-content;height:max-content;padding:1.5rem;touch-action:auto}nav#faq ol{counter-reset:faq;display:block;height:-moz-fit-content;height:fit-content;list-style:decimal-leading-zero}nav#faq ol li{counter-increment:faq;width:-moz-max-content;width:max-content}nav#faq ol li:before{content:counter(faq);display:block;font-family:var(--heading);font-weight:var(--fw-h-bold)}nav#faq h2{font-size:var(--txt-large);right:0;margin:.5rem 0}nav#faq a{padding:.5rem}.faq-block{max-width:none;padding-bottom:3rem;width:100%}.faq-block>*{margin:1rem auto;max-width:var(--wide)}.faq-block h2{margin:5rem 0 1.5rem}.faq-block h3{margin:0;text-transform:none}.faq-block :target{background-color:var(--base);outline:none}.faq-block :target h2{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem 1.5rem}.faq-block details{margin:1rem auto;max-width:var(--content);padding:.75rem}.faq-block details+details{margin-top:3rem}.faq-block details .button{display:flex;height:-moz-fit-content;height:fit-content;margin-right:auto}
diff --git a/build/faq/style-index.css b/build/faq/style-index.css
index daf20ad..c1ab882 100644
--- a/build/faq/style-index.css
+++ b/build/faq/style-index.css
@@ -1 +1 @@
-nav#faq{--height:fit-content;background-color:var(--base-100);border-radius:var(--outerRadius);display:block;padding:1.5rem;touch-action:auto}nav#faq ol{counter-reset:faq;display:block;height:-moz-fit-content;height:fit-content;list-style:decimal-leading-zero}nav#faq ol li{counter-increment:faq}nav#faq ol li:before{content:counter(faq);display:block;font-family:var(--heading);font-weight:var(--hBold)}nav#faq h2{font-size:var(--large);left:0;margin:.5rem 0}nav#faq a{padding:.5rem}.faq-block{max-width:none;padding-bottom:3rem;width:100%}.faq-block>*{margin:1rem auto;max-width:var(--alignWide)}.faq-block h2{margin:5rem 0 1.5rem}.faq-block h3{margin:0;text-transform:none}.faq-block :target{background-color:var(--base);outline:none}.faq-block :target h2{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem 1.5rem}.faq-block details{margin:1rem auto;max-width:var(--maxWidth);padding:.75rem}.faq-block details+details{margin-top:3rem}.faq-block details .button{display:block;height:-moz-fit-content;height:fit-content;margin-left:auto}
+nav#faq{background-color:var(--base-100);border-radius:var(--radius-outer);display:block;height:-moz-max-content;height:max-content;padding:1.5rem;touch-action:auto}nav#faq ol{counter-reset:faq;display:block;height:-moz-fit-content;height:fit-content;list-style:decimal-leading-zero}nav#faq ol li{counter-increment:faq;width:-moz-max-content;width:max-content}nav#faq ol li:before{content:counter(faq);display:block;font-family:var(--heading);font-weight:var(--fw-h-bold)}nav#faq h2{font-size:var(--txt-large);left:0;margin:.5rem 0}nav#faq a{padding:.5rem}.faq-block{max-width:none;padding-bottom:3rem;width:100%}.faq-block>*{margin:1rem auto;max-width:var(--wide)}.faq-block h2{margin:5rem 0 1.5rem}.faq-block h3{margin:0;text-transform:none}.faq-block :target{background-color:var(--base);outline:none}.faq-block :target h2{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem 1.5rem}.faq-block details{margin:1rem auto;max-width:var(--content);padding:.75rem}.faq-block details+details{margin-top:3rem}.faq-block details .button{display:flex;height:-moz-fit-content;height:fit-content;margin-left:auto}
diff --git a/build/feed/style-index-rtl.css b/build/feed/style-index-rtl.css
index 425fcc7..22875c2 100644
--- a/build/feed/style-index-rtl.css
+++ b/build/feed/style-index-rtl.css
@@ -1 +1 @@
-.feed-block .feed-filters{padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{right:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--bWeight);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7;transition:opacity var(--transition-base),filter var(--transition-base)}.feed.item img[data-loaded=true]{filter:none;opacity:1}.feed.item[data-timeline]{aspect-ratio:unset}.feed.item[data-timeline] summary{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] summary span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] summary span:first-of-type{bottom:0;left:50%;text-align:left}.feed.item[data-timeline] summary span:last-of-type{right:50%;top:0}.feed.item[data-timeline] summary>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-left:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed.item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed.item:hover .handle,.feed.item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed.item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed.item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-light);border-radius:var(--innerRadius);bottom:0;right:0;padding:.25rem 1.1rem .25rem .25rem;position:absolute;left:0;z-index:1}.feed.item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;left:.7rem;width:1.5rem;z-index:11}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
+.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)}}
diff --git a/build/feed/style-index.css b/build/feed/style-index.css
index 4ef7862..bbc8a27 100644
--- a/build/feed/style-index.css
+++ b/build/feed/style-index.css
@@ -1 +1 @@
-.feed-block .feed-filters{padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{left:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--bWeight);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7;transition:opacity var(--transition-base),filter var(--transition-base)}.feed.item img[data-loaded=true]{filter:none;opacity:1}.feed.item[data-timeline]{aspect-ratio:unset}.feed.item[data-timeline] summary{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] summary span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] summary span:first-of-type{bottom:0;right:50%;text-align:right}.feed.item[data-timeline] summary span:last-of-type{left:50%;top:0}.feed.item[data-timeline] summary>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-right:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed.item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed.item:hover .handle,.feed.item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed.item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed.item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-light);border-radius:var(--innerRadius);bottom:0;left:0;padding:.25rem .25rem .25rem 1.1rem;position:absolute;right:0;z-index:1}.feed.item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;right:.7rem;width:1.5rem;z-index:11}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
+.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)}}
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 3596020..6fa9985 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '66b80732cd4eebba785a');
+<?php return array('dependencies' => array(), 'version' => '90c2ce15e482c81ed55a');
diff --git a/build/feed/view.js b/build/feed/view.js
index 85ea840..cee6ec0 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=window.isEmptyObject(this.taxonomyFilters),this.ui.clearFilter.hidden=window.isEmptyObject(this.taxonomyFilters)}createItemElement(e){let t=window.getTemplate("feed-item");return Object.hasOwn(t.dataset,"timeline")?this.createTimelineElement(e,t):t}createTimelineElement(e,t){var i,s;let[r,a,o,n,l,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.debounce((()=>{this.updateImageSizes()}),250)):window.addEventListener("resize",window.debounce((()=>{this.updateImageSizes()}),250)),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}loadImage(e){const t=this.getAppropriateImageSize(e);t&&t!==e.src&&(e.src=t,e.dataset.loaded="true")}getAppropriateImageSize(e){return window.innerWidth<768&&e.dataset.small?e.dataset.small:e.dataset.medium?e.dataset.medium:e.src}observeImages(e){e.querySelectorAll("img[data-small], img[data-medium]").forEach((e=>{e.dataset.loaded||this.imageObserver.observe(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(function(){window.feedBlock=new e}))})();
\ No newline at end of file
+(()=>{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",(function(){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 8bb1b2c..7c0da0d 100644
--- a/build/forms/view.asset.php
+++ b/build/forms/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '6af2556d0306f0da3d78');
+<?php return array('dependencies' => array(), 'version' => '6084ed3247c497c65c42');
diff --git a/build/forms/view.js b/build/forms/view.js
index 73415e6..428e09c 100644
--- a/build/forms/view.js
+++ b/build/forms/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((e=>{this.controller.registerForm(e)})),this.controller.subscribe(((e,o)=>{"form-submit"===e&&this.handleFormSubmission(o)}))}async handleFormSubmission(e){let[o,t,r]=[e.formId,e.config,e.data],s=t.element,n={"X-WP-Nonce":jvbSettings.nonce,"Content-Type":"application/json"};e.form_type=o,e.form_id=s.id,s.closest(".jvb-form-block"),this.controller.showFormStatus(o,"uploading");try{const e=await fetch(`${jvbSettings.api}forms`,{method:"POST",headers:n,body:JSON.stringify(r)});if(!e.ok){this.controller.showFormStatus(o,"error");const t=await e.json().catch((()=>({})));throw new Error(t.message||`Request failed with status ${e.status}`)}this.controller.showFormStatus(o,"submitted"),this.controller.showSummary(o,".jvb-form-block"),this.controller.store.delete(o)}catch(e){throw e}}updateUI(e,o){let t=window.getTemplate("formSummary");t.querySelector("h2").textContent="Success!",console.log("Form Response: ",e),console.log(t);for(let[o,r]of Object.entries(e)){let e=t.querySelector(`#${o}`);if(e){let o=e.querySelector("h4");o.innerText.includes("%s")?o.innerHTML=o.replace("%s","<b>"+r+"</b>"):e.querySelector("div").innerHTML=r}}o.append(t)}}document.addEventListener("DOMContentLoaded",(function(){new e}))})();
\ 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})})),this.controller.subscribe(((o,t)=>{"form-submit"===o&&this.handleFormSubmission(t)}))}async handleFormSubmission(o){let t=o.formId,e=o.config,r=o.fullData,s=e.element,n={"X-WP-Nonce":jvbSettings.nonce,"Content-Type":"application/json"};s.closest(".jvb-form-block"),this.controller.showFormStatus(t,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",headers:n,body:JSON.stringify(r)});if(!o.ok){this.controller.showFormStatus(t,"error");const e=await o.json().catch((()=>({})));throw new Error(e.message||`Request failed with status ${o.status}`)}this.controller.showFormStatus(t,"submitted"),this.controller.showSummary(t,".jvb-form-block")}catch(o){throw o}finally{this.controller.store.delete(t)}}}document.addEventListener("DOMContentLoaded",(function(){new o}))})();
\ No newline at end of file
diff --git a/build/glossary/style-index-rtl.css b/build/glossary/style-index-rtl.css
index 1710c41..873ac34 100644
--- a/build/glossary/style-index-rtl.css
+++ b/build/glossary/style-index-rtl.css
@@ -1 +1 @@
-:root{--navWidth:40vw}@media(min-width:768px){:root{--navWidth:22vw}}nav.glossary-index{height:60vh;position:fixed;left:0;top:50%;transform:translateY(-50%);width:var(--navWidth);z-index:var(--z-3)}nav.glossary-index>ul{--dir:column;--align:flex-start;height:100%;overflow:hidden auto;scroll-behavior:smooth;touch-action:pan-y;width:100%}nav.glossary-index a,nav.glossary-index li{width:100%}nav.glossary-index a{--justify:center;background-color:var(--overlay-heavy);word-wrap:anywhere;transition:background-color .2s ease;white-space:wrap}nav.glossary-index a.active,nav.glossary-index a:focus,nav.glossary-index a:hover{background-color:rgba(var(--action-rgb),var(--rgb-heavy));color:var(--action-contrast)}.glossary dd{margin-right:.5rem;width:calc(100% + .75rem)}.glossary dd,.glossary dt{right:0;position:relative;transition:margin var(--transition-base),right var(--transition-base),color var(--transition-base),width var(--transition-base)}.glossary dt.active,.glossary dt:target{color:var(--action-0);right:-1.5rem;outline:none;padding:0}.glossary dt.active+dd,.glossary dt:target+dd{right:-1.5rem}dl.glossary,main header{margin-right:0;margin-left:0;max-width:100vw;padding:0 2rem 0 var(--navWidth)}@media(min-width:768px){dl.glossary,main header{margin-right:auto;margin-left:var(--navWidth);max-width:var(--maxWidth);padding-left:var(--height)}}@media(max-width:768px){.glossary h2{font-size:var(--medium)}.glossary p{font-size:var(--small)}.glossary-index a,.glossary-index li{height:-moz-fit-content;height:fit-content}.glossary-index a{font-size:var(--small);min-height:2em;padding:.25rem}body:has(.glossary) h1{font-size:var(--xxlarge)}}
+:root{--navWidth:40vw}@media(min-width:768px){:root{--navWidth:22vw}}nav.glossary-index{height:60vh;position:fixed;left:0;top:50%;transform:translateY(-50%);width:var(--navWidth);z-index:var(--z-3)}nav.glossary-index>ul{--dir:column;--align:flex-start;--justify:flex-start;height:100%;max-height:100%;overflow:hidden auto;scroll-behavior:smooth;touch-action:pan-y;width:100%}nav.glossary-index a,nav.glossary-index li{width:100%}nav.glossary-index a{--justify:center;background-color:rgba(var(--base-rgb),var(--op-6));word-wrap:anywhere;white-space:wrap}nav.glossary-index a.active,nav.glossary-index a:focus,nav.glossary-index a:hover{background-color:rgba(var(--action-rgb),var(--op-6));color:var(--action-contrast)}.glossary dd{margin-right:.5rem;width:calc(100% + .75rem)}.glossary dd,.glossary dt{right:0;position:relative;transition:margin var(--trans-base),right var(--trans-base),width var(--trans-base)}.glossary dt.active,.glossary dt:target{color:var(--action-0);right:-1.5rem;outline:none;padding:0}.glossary dt.active+dd,.glossary dt:target+dd{right:-1.5rem}dl.glossary,main header{grid-column:full;padding:0 2rem 0 var(--navWidth)}@media(min-width:768px){dl.glossary,main header{margin-right:auto;margin-left:var(--navWidth);max-width:var(--content);padding-left:var(--btn)}}@media(max-width:768px){.glossary h2{font-size:var(--txt-medium)}.glossary p{font-size:var(--txt-x-small)}.glossary-index a,.glossary-index li{height:-moz-fit-content;height:fit-content}.glossary-index a{font-size:var(--txt-x-small);min-height:2em;padding:.25rem}body:has(.glossary) h1{font-size:var(--txt-xx-large)}}
diff --git a/build/glossary/style-index.css b/build/glossary/style-index.css
index 932eb11..56bddbc 100644
--- a/build/glossary/style-index.css
+++ b/build/glossary/style-index.css
@@ -1 +1 @@
-:root{--navWidth:40vw}@media(min-width:768px){:root{--navWidth:22vw}}nav.glossary-index{height:60vh;position:fixed;right:0;top:50%;transform:translateY(-50%);width:var(--navWidth);z-index:var(--z-3)}nav.glossary-index>ul{--dir:column;--align:flex-start;height:100%;overflow:hidden auto;scroll-behavior:smooth;touch-action:pan-y;width:100%}nav.glossary-index a,nav.glossary-index li{width:100%}nav.glossary-index a{--justify:center;background-color:var(--overlay-heavy);word-wrap:anywhere;transition:background-color .2s ease;white-space:wrap}nav.glossary-index a.active,nav.glossary-index a:focus,nav.glossary-index a:hover{background-color:rgba(var(--action-rgb),var(--rgb-heavy));color:var(--action-contrast)}.glossary dd{margin-left:.5rem;width:calc(100% + .75rem)}.glossary dd,.glossary dt{left:0;position:relative;transition:margin var(--transition-base),left var(--transition-base),color var(--transition-base),width var(--transition-base)}.glossary dt.active,.glossary dt:target{color:var(--action-0);left:-1.5rem;outline:none;padding:0}.glossary dt.active+dd,.glossary dt:target+dd{left:-1.5rem}dl.glossary,main header{margin-left:0;margin-right:0;max-width:100vw;padding:0 var(--navWidth) 0 2rem}@media(min-width:768px){dl.glossary,main header{margin-left:auto;margin-right:var(--navWidth);max-width:var(--maxWidth);padding-right:var(--height)}}@media(max-width:768px){.glossary h2{font-size:var(--medium)}.glossary p{font-size:var(--small)}.glossary-index a,.glossary-index li{height:-moz-fit-content;height:fit-content}.glossary-index a{font-size:var(--small);min-height:2em;padding:.25rem}body:has(.glossary) h1{font-size:var(--xxlarge)}}
+:root{--navWidth:40vw}@media(min-width:768px){:root{--navWidth:22vw}}nav.glossary-index{height:60vh;position:fixed;right:0;top:50%;transform:translateY(-50%);width:var(--navWidth);z-index:var(--z-3)}nav.glossary-index>ul{--dir:column;--align:flex-start;--justify:flex-start;height:100%;max-height:100%;overflow:hidden auto;scroll-behavior:smooth;touch-action:pan-y;width:100%}nav.glossary-index a,nav.glossary-index li{width:100%}nav.glossary-index a{--justify:center;background-color:rgba(var(--base-rgb),var(--op-6));word-wrap:anywhere;white-space:wrap}nav.glossary-index a.active,nav.glossary-index a:focus,nav.glossary-index a:hover{background-color:rgba(var(--action-rgb),var(--op-6));color:var(--action-contrast)}.glossary dd{margin-left:.5rem;width:calc(100% + .75rem)}.glossary dd,.glossary dt{left:0;position:relative;transition:margin var(--trans-base),left var(--trans-base),width var(--trans-base)}.glossary dt.active,.glossary dt:target{color:var(--action-0);left:-1.5rem;outline:none;padding:0}.glossary dt.active+dd,.glossary dt:target+dd{left:-1.5rem}dl.glossary,main header{grid-column:full;padding:0 var(--navWidth) 0 2rem}@media(min-width:768px){dl.glossary,main header{margin-left:auto;margin-right:var(--navWidth);max-width:var(--content);padding-right:var(--btn)}}@media(max-width:768px){.glossary h2{font-size:var(--txt-medium)}.glossary p{font-size:var(--txt-x-small)}.glossary-index a,.glossary-index li{height:-moz-fit-content;height:fit-content}.glossary-index a{font-size:var(--txt-x-small);min-height:2em;padding:.25rem}body:has(.glossary) h1{font-size:var(--txt-xx-large)}}
diff --git a/build/gmbreviews/style-index-rtl.css b/build/gmbreviews/style-index-rtl.css
index 0e50017..89a29d1 100644
--- a/build/gmbreviews/style-index-rtl.css
+++ b/build/gmbreviews/style-index-rtl.css
@@ -1 +1 @@
-.gmb-reviews{max-width:none}.gmb-reviews>.row.btw{max-width:var(--alignWide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--wider);padding:0}.gmb-reviews ul li{background-color:var(--base-100);margin:2rem 0;padding:1rem;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){right:-2rem}.gmb-reviews ul li:nth-of-type(2n){left:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:none}.gmb-reviews>.row.btw{max-width:var(--wide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--wider);padding:0}.gmb-reviews ul li{background-color:var(--base-100);margin:2rem 0;padding:1rem;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){right:-2rem}.gmb-reviews ul li:nth-of-type(2n){left:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
diff --git a/build/gmbreviews/style-index.css b/build/gmbreviews/style-index.css
index 83c6f01..e8a3bc2 100644
--- a/build/gmbreviews/style-index.css
+++ b/build/gmbreviews/style-index.css
@@ -1 +1 @@
-.gmb-reviews{max-width:none}.gmb-reviews>.row.btw{max-width:var(--alignWide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--wider);padding:0}.gmb-reviews ul li{background-color:var(--base-100);margin:2rem 0;padding:1rem;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){left:-2rem}.gmb-reviews ul li:nth-of-type(2n){right:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:none}.gmb-reviews>.row.btw{max-width:var(--wide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--wider);padding:0}.gmb-reviews ul li{background-color:var(--base-100);margin:2rem 0;padding:1rem;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){left:-2rem}.gmb-reviews ul li:nth-of-type(2n){right:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
diff --git a/build/list/style-index-rtl.css b/build/list/style-index-rtl.css
index 7b5a24a..bcc86a9 100644
--- a/build/list/style-index-rtl.css
+++ b/build/list/style-index-rtl.css
@@ -1 +1 @@
-.directory-list ul{list-style:none;margin:0;padding:0}.directory-list>ul li{position:relative}.directory-list>ul li h3{background-color:var(--base-50);font-size:20vw;right:0;margin:0!important;padding:.5rem 1rem;position:sticky;text-align:center;top:0;width:100%;z-index:5}.directory-list>ul li li{border-radius:var(--innerRadius);display:flex;justify-content:space-between;padding:.5rem 1rem}.directory-list>ul li>ul{display:flex;flex-direction:column;gap:.125rem}.directory-list>ul li li:nth-of-type(odd){background-color:var(--base-200)}.directory-list>ul li li:nth-of-type(2n){background-color:var(--base-100)}@media(min-width:768px){.directory-list>ul li h3{font-size:10vw}}
+.directory-list ul{list-style:none;margin:0;padding:0}.directory-list>ul li{position:relative}.directory-list>ul li h3{background-color:var(--base-50);font-size:20vw;right:0;margin:0!important;padding:.5rem 1rem;position:sticky;text-align:center;top:0;width:100%;z-index:5}.directory-list>ul li li{border-radius:var(--radius);display:flex;justify-content:space-between;padding:.5rem 1rem}.directory-list>ul li>ul{display:flex;flex-direction:column;gap:.125rem}.directory-list>ul li li:nth-of-type(odd){background-color:var(--base-200)}.directory-list>ul li li:nth-of-type(2n){background-color:var(--base-100)}@media(min-width:768px){.directory-list>ul li h3{font-size:10vw}}
diff --git a/build/list/style-index.css b/build/list/style-index.css
index ec59799..1caf122 100644
--- a/build/list/style-index.css
+++ b/build/list/style-index.css
@@ -1 +1 @@
-.directory-list ul{list-style:none;margin:0;padding:0}.directory-list>ul li{position:relative}.directory-list>ul li h3{background-color:var(--base-50);font-size:20vw;left:0;margin:0!important;padding:.5rem 1rem;position:sticky;text-align:center;top:0;width:100%;z-index:5}.directory-list>ul li li{border-radius:var(--innerRadius);display:flex;justify-content:space-between;padding:.5rem 1rem}.directory-list>ul li>ul{display:flex;flex-direction:column;gap:.125rem}.directory-list>ul li li:nth-of-type(odd){background-color:var(--base-200)}.directory-list>ul li li:nth-of-type(2n){background-color:var(--base-100)}@media(min-width:768px){.directory-list>ul li h3{font-size:10vw}}
+.directory-list ul{list-style:none;margin:0;padding:0}.directory-list>ul li{position:relative}.directory-list>ul li h3{background-color:var(--base-50);font-size:20vw;left:0;margin:0!important;padding:.5rem 1rem;position:sticky;text-align:center;top:0;width:100%;z-index:5}.directory-list>ul li li{border-radius:var(--radius);display:flex;justify-content:space-between;padding:.5rem 1rem}.directory-list>ul li>ul{display:flex;flex-direction:column;gap:.125rem}.directory-list>ul li li:nth-of-type(odd){background-color:var(--base-200)}.directory-list>ul li li:nth-of-type(2n){background-color:var(--base-100)}@media(min-width:768px){.directory-list>ul li h3{font-size:10vw}}
diff --git a/build/summary/style-index-rtl.css b/build/summary/style-index-rtl.css
index 7e78cbe..ea88fca 100644
--- a/build/summary/style-index-rtl.css
+++ b/build/summary/style-index-rtl.css
@@ -1 +1 @@
-details>div{margin:1rem 0}main>header:not(:has(img)){margin-top:3rem!important}header a:before{display:none!important}header+details{margin:1.5rem var(--ml) 3rem var(--mr)!important;max-width:var(--alignMed)}main{padding-top:0!important}
+details>div{margin:1rem 0}main>header:not(:has(img)){margin-top:3rem!important}header a:before{display:none!important}header+details{margin:1.5rem auto 3rem!important;max-width:var(--wide)}main{padding-top:0!important}
diff --git a/build/summary/style-index.css b/build/summary/style-index.css
index e77fd4b..ea88fca 100644
--- a/build/summary/style-index.css
+++ b/build/summary/style-index.css
@@ -1 +1 @@
-details>div{margin:1rem 0}main>header:not(:has(img)){margin-top:3rem!important}header a:before{display:none!important}header+details{margin:1.5rem var(--mr) 3rem var(--ml)!important;max-width:var(--alignMed)}main{padding-top:0!important}
+details>div{margin:1rem 0}main>header:not(:has(img)){margin-top:3rem!important}header a:before{display:none!important}header+details{margin:1.5rem auto 3rem!important;max-width:var(--wide)}main{padding-top:0!important}
diff --git a/build/timeline/style-index-rtl.css b/build/timeline/style-index-rtl.css
index 7f9d2fa..d10c16d 100644
--- a/build/timeline/style-index-rtl.css
+++ b/build/timeline/style-index-rtl.css
@@ -1 +1 @@
-main{--gap:0}main section:last-of-type{margin-bottom:0}#at-a-glance{max-width:var(--alignWide);--gap:0}#at-a-glance img{border:2px solid var(--action-0);height:auto;width:100%}#at-a-glance h3{font-size:var(--small)}#at-a-glance .before img{border-right:0;border-left-width:1px;border-top:0}#at-a-glance .after img{border-bottom:0;border-right-width:1px;border-left:0}.timeline-point.timeline-point{--lineWidth:1px;--gap:2rem;background-color:var(--base);margin:0;max-width:100vw;overflow:hidden;padding:0;position:relative}.timeline-point.timeline-point .open-gallery{border-radius:4px;padding:.5rem;position:sticky;width:40%}.timeline-point.timeline-point .info{padding:1rem .5rem .5rem;position:relative;width:60%}.timeline-point.timeline-point .info h2{font-size:var(--medium);margin:0 0 .5rem;position:relative}.timeline-point.timeline-point .info h2 .icon{--w:2.5rem;background-color:var(--action-100);right:-2.5rem;position:absolute;top:.25rem;transform:rotate(90deg)}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{background-color:var(--action-0);content:"";display:block;height:100%;right:45%;position:absolute;width:var(--lineWidth)}.timeline-point.timeline-point:before{height:1rem}.timeline-point.timeline-point:after{top:4rem}.timeline-point.timeline-point#before-treatment:before,.timeline-point.timeline-point:last-of-type:after{display:none}@media(min-width:768px){#at-a-glance h3{font-size:var(--xlarge)}.timeline-point.timeline-point{--gap:4rem}.timeline-point.timeline-point .open-gallery{width:50%}.timeline-point.timeline-point .info{padding:25vh 1rem 1rem;width:50%}.timeline-point.timeline-point .info h2 .icon{--w:4rem;right:-4.15rem;top:0}.timeline-point.timeline-point .info a{align-items:center;display:flex;flex-wrap:wrap}.timeline-point.timeline-point .info time{font-size:var(--small);text-transform:uppercase}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{right:calc(50% + 2rem)}.timeline-point.timeline-point:before{height:calc(25vh - 2rem)}.timeline-point.timeline-point:after{top:calc(25vh + 6rem)}}
+main{--gap:0}main section:last-of-type{margin-bottom:0}#at-a-glance{margin:0 auto;max-width:var(--wide);--gap:0}#at-a-glance img{border:2px solid var(--action-0);height:auto;width:100%}#at-a-glance h3{font-size:var(--txt-x-small)}#at-a-glance .before img{border-right:0;border-left-width:1px;border-top:0}#at-a-glance .after img{border-bottom:0;border-right-width:1px;border-left:0}.timeline-point.timeline-point{--lineWidth:1px;--gap:2rem;background-color:var(--base);margin:0;max-width:100vw;overflow:hidden;padding:0;position:relative}.timeline-point.timeline-point img{border-radius:4px;padding:.5rem;position:sticky;width:40%}.timeline-point.timeline-point .info{padding:1rem .5rem .5rem;position:relative;width:60%}.timeline-point.timeline-point .info h2{font-size:var(--txt-medium);margin:0 0 .5rem;position:relative}.timeline-point.timeline-point .info h2 .icon{--w:2.5rem;background-color:var(--action-100);right:-2.5rem;position:absolute;top:.25rem;transform:rotate(90deg)}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{background-color:var(--action-0);content:"";display:block;height:100%;right:45%;position:absolute;width:var(--lineWidth)}.timeline-point.timeline-point:before{height:1rem}.timeline-point.timeline-point:after{top:4rem}.timeline-point.timeline-point#before-treatment:before,.timeline-point.timeline-point:last-of-type:after{display:none}@media(min-width:768px){#at-a-glance h3{font-size:var(--txt-x-large)}.timeline-point.timeline-point{--gap:4rem}.timeline-point.timeline-point img{width:50%}.timeline-point.timeline-point .info{padding:25vh 1rem 1rem;width:50%}.timeline-point.timeline-point .info h2 .icon{--w:4rem;right:-4.15rem;top:0}.timeline-point.timeline-point .info a{align-items:center;display:flex;flex-wrap:wrap}.timeline-point.timeline-point .info time{font-size:var(--txt-x-small);text-transform:uppercase}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{right:calc(50% + 2rem)}.timeline-point.timeline-point:before{height:calc(25vh - 2rem)}.timeline-point.timeline-point:after{top:calc(25vh + 6rem)}}
diff --git a/build/timeline/style-index.css b/build/timeline/style-index.css
index eaaa6d9..ea196c4 100644
--- a/build/timeline/style-index.css
+++ b/build/timeline/style-index.css
@@ -1 +1 @@
-main{--gap:0}main section:last-of-type{margin-bottom:0}#at-a-glance{max-width:var(--alignWide);--gap:0}#at-a-glance img{border:2px solid var(--action-0);height:auto;width:100%}#at-a-glance h3{font-size:var(--small)}#at-a-glance .before img{border-left:0;border-right-width:1px;border-top:0}#at-a-glance .after img{border-bottom:0;border-left-width:1px;border-right:0}.timeline-point.timeline-point{--lineWidth:1px;--gap:2rem;background-color:var(--base);margin:0;max-width:100vw;overflow:hidden;padding:0;position:relative}.timeline-point.timeline-point .open-gallery{border-radius:4px;padding:.5rem;position:sticky;width:40%}.timeline-point.timeline-point .info{padding:1rem .5rem .5rem;position:relative;width:60%}.timeline-point.timeline-point .info h2{font-size:var(--medium);margin:0 0 .5rem;position:relative}.timeline-point.timeline-point .info h2 .icon{--w:2.5rem;background-color:var(--action-100);left:-2.5rem;position:absolute;top:.25rem;transform:rotate(-90deg)}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{background-color:var(--action-0);content:"";display:block;height:100%;left:45%;position:absolute;width:var(--lineWidth)}.timeline-point.timeline-point:before{height:1rem}.timeline-point.timeline-point:after{top:4rem}.timeline-point.timeline-point#before-treatment:before,.timeline-point.timeline-point:last-of-type:after{display:none}@media(min-width:768px){#at-a-glance h3{font-size:var(--xlarge)}.timeline-point.timeline-point{--gap:4rem}.timeline-point.timeline-point .open-gallery{width:50%}.timeline-point.timeline-point .info{padding:25vh 1rem 1rem;width:50%}.timeline-point.timeline-point .info h2 .icon{--w:4rem;left:-4.15rem;top:0}.timeline-point.timeline-point .info a{align-items:center;display:flex;flex-wrap:wrap}.timeline-point.timeline-point .info time{font-size:var(--small);text-transform:uppercase}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{left:calc(50% + 2rem)}.timeline-point.timeline-point:before{height:calc(25vh - 2rem)}.timeline-point.timeline-point:after{top:calc(25vh + 6rem)}}
+main{--gap:0}main section:last-of-type{margin-bottom:0}#at-a-glance{margin:0 auto;max-width:var(--wide);--gap:0}#at-a-glance img{border:2px solid var(--action-0);height:auto;width:100%}#at-a-glance h3{font-size:var(--txt-x-small)}#at-a-glance .before img{border-left:0;border-right-width:1px;border-top:0}#at-a-glance .after img{border-bottom:0;border-left-width:1px;border-right:0}.timeline-point.timeline-point{--lineWidth:1px;--gap:2rem;background-color:var(--base);margin:0;max-width:100vw;overflow:hidden;padding:0;position:relative}.timeline-point.timeline-point img{border-radius:4px;padding:.5rem;position:sticky;width:40%}.timeline-point.timeline-point .info{padding:1rem .5rem .5rem;position:relative;width:60%}.timeline-point.timeline-point .info h2{font-size:var(--txt-medium);margin:0 0 .5rem;position:relative}.timeline-point.timeline-point .info h2 .icon{--w:2.5rem;background-color:var(--action-100);left:-2.5rem;position:absolute;top:.25rem;transform:rotate(-90deg)}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{background-color:var(--action-0);content:"";display:block;height:100%;left:45%;position:absolute;width:var(--lineWidth)}.timeline-point.timeline-point:before{height:1rem}.timeline-point.timeline-point:after{top:4rem}.timeline-point.timeline-point#before-treatment:before,.timeline-point.timeline-point:last-of-type:after{display:none}@media(min-width:768px){#at-a-glance h3{font-size:var(--txt-x-large)}.timeline-point.timeline-point{--gap:4rem}.timeline-point.timeline-point img{width:50%}.timeline-point.timeline-point .info{padding:25vh 1rem 1rem;width:50%}.timeline-point.timeline-point .info h2 .icon{--w:4rem;left:-4.15rem;top:0}.timeline-point.timeline-point .info a{align-items:center;display:flex;flex-wrap:wrap}.timeline-point.timeline-point .info time{font-size:var(--txt-x-small);text-transform:uppercase}.timeline-point.timeline-point:after,.timeline-point.timeline-point:before{left:calc(50% + 2rem)}.timeline-point.timeline-point:before{height:calc(25vh - 2rem)}.timeline-point.timeline-point:after{top:calc(25vh + 6rem)}}
diff --git a/build/video/block.json b/build/video/block.json
index 0c88755..6b909af 100644
--- a/build/video/block.json
+++ b/build/video/block.json
@@ -94,6 +94,7 @@
   },
   "textdomain": "jvb",
   "editorScript": "file:./index.js",
+  "viewScript": "file:./view.js",
   "editorStyle": "file:./index.css",
   "style": "file:./style-index.css"
 }
\ No newline at end of file
diff --git a/build/video/style-index-rtl.css b/build/video/style-index-rtl.css
index 97527aa..25a0db1 100644
--- a/build/video/style-index-rtl.css
+++ b/build/video/style-index-rtl.css
@@ -1 +1 @@
-.video-cover{display:flex;min-height:75vh;overflow:hidden;position:relative;width:100%}.video-cover .wrap{background-color:var(--contrast-200)}.video-cover .video-container{background-color:var(--action-50);bottom:0;display:flex;right:0;min-height:100%;min-width:100%;position:absolute;left:0;top:0;z-index:0}.video-cover .video-container.fade{animation:fadeIn 1s ease-in}.video-cover .video-container video{filter:grayscale(100%) contrast(1);flex:1 0 100%;mix-blend-mode:multiply;-o-object-fit:cover;object-fit:cover;opacity:.85;pointer-events:none}.video-cover .inner-wrap{color:var(--action-contrast);padding:2rem;position:relative;width:100%;z-index:2}.video-cover .inner-wrap h1,.video-cover .inner-wrap h2,.video-cover .inner-wrap h3,.video-cover .inner-wrap h4,.video-cover .inner-wrap h5,.video-cover .inner-wrap h6{color:var(--action-contrast);margin:2rem 0 0;text-shadow:0 2px 4px rgba(0,0,0,.5);word-spacing:100vw}.video-cover .inner-wrap p{color:var(--action-contrast);letter-spacing:2px;margin:0;text-shadow:0 1px 2px rgba(0,0,0,.5);text-transform:uppercase}.video-cover .inner-wrap .media-text figure{max-width:50%}@media(min-width:768px){.video-cover .inner-wrap .media-text{--align:flex-start;gap:3rem;max-width:var(--maxWidth)}}.video-cover .inner-wrap .media-text>div{width:-moz-fit-content;width:fit-content}.video-cover .inner-wrap .buttons a{border-color:var(--action-contrast);color:var(--action-contrast);font-weight:500}.video-cover .inner-wrap .buttons a:visited{color:var(--action-0)}.video-cover .inner-wrap .buttons a:visited:hover{color:var(--action-contrast)}.video-cover .inner-wrap .buttons a:hover{background-color:var(--action-0);color:var(--action-contrast)}.video-cover .inner-wrap .outline a{background-color:rgba(var(--base-rgb),var(--overlay-light))}.video-cover .inner-wrap .buttons{margin:3rem 0}.video-cover .inner-wrap .wp-block-button__link{text-shadow:none}.video-cover.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover.align-top-center{align-items:flex-start;justify-content:center}.video-cover.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover.align-center-left{align-items:center;justify-content:flex-start}.video-cover.align-center{align-items:center;justify-content:center}.video-cover.align-center-right{align-items:center;justify-content:flex-end}.video-cover.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover.alignfull{margin-right:calc(50% - 50vw);margin-left:calc(50% - 50vw);max-width:none;width:100vw}.video-cover.alignwide{max-width:1200px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}
+.video-cover{display:flex;min-height:75vh;overflow:hidden;position:relative;width:100%}.video-cover .wrap{background-color:var(--contrast-200)}.video-cover .video-container{background-color:var(--action-50);bottom:0;display:flex;right:0;min-height:100%;min-width:100%;position:absolute;left:0;top:0;z-index:0}.video-cover .video-container.fade{animation:fadeIn 1s ease-in}.video-cover .video-container video{filter:grayscale(100%) contrast(1);flex:1 0 100%;mix-blend-mode:multiply;-o-object-fit:cover;object-fit:cover;opacity:.85;pointer-events:none}.video-cover .inner-wrap{color:var(--action-contrast);padding:2rem;position:relative;width:100%;z-index:2}.video-cover .inner-wrap h1,.video-cover .inner-wrap h2,.video-cover .inner-wrap h3,.video-cover .inner-wrap h4,.video-cover .inner-wrap h5,.video-cover .inner-wrap h6{color:var(--action-contrast);margin:2rem 0 0;text-shadow:0 2px 4px rgba(0,0,0,.5);word-spacing:100vw}.video-cover .inner-wrap p{color:var(--action-contrast);letter-spacing:2px;margin:0;text-shadow:0 1px 2px rgba(0,0,0,.5);text-transform:uppercase}.video-cover .inner-wrap .media-text figure{max-width:50%}@media(min-width:768px){.video-cover .inner-wrap .media-text{--align:flex-start;gap:3rem;max-width:var(--content)}}.video-cover .inner-wrap .media-text>div{width:-moz-fit-content;width:fit-content}.video-cover .inner-wrap .buttons a{border-color:var(--action-contrast);color:var(--action-contrast);font-weight:var(--fw-h-bold)}.video-cover .inner-wrap .buttons a:visited,.video-cover .inner-wrap .buttons a:visited:hover{color:var(--action-contrast)}.video-cover .inner-wrap .buttons a:hover{background-color:var(--action-0);color:var(--action-contrast)}.video-cover .inner-wrap .outline a{background-color:rgba(var(--base-rgb),rgba(var(--base-rgb),var(--op-3)))}.video-cover .inner-wrap .buttons{margin:3rem 0}.video-cover .inner-wrap .buttons li{background-color:rgba(var(--action-rgb),var(--op-4))}.video-cover .inner-wrap .wp-block-button__link{text-shadow:none}.video-cover.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover.align-top-center{align-items:flex-start;justify-content:center}.video-cover.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover.align-center-left{align-items:center;justify-content:flex-start}.video-cover.align-center{align-items:center;justify-content:center}.video-cover.align-center-right{align-items:center;justify-content:flex-end}.video-cover.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover.alignfull{margin-right:calc(50% - 50vw);margin-left:calc(50% - 50vw);max-width:none;width:100vw}.video-cover.alignwide{max-width:1200px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}
diff --git a/build/video/style-index.css b/build/video/style-index.css
index f326678..1dd076a 100644
--- a/build/video/style-index.css
+++ b/build/video/style-index.css
@@ -1 +1 @@
-.video-cover{display:flex;min-height:75vh;overflow:hidden;position:relative;width:100%}.video-cover .wrap{background-color:var(--contrast-200)}.video-cover .video-container{background-color:var(--action-50);bottom:0;display:flex;left:0;min-height:100%;min-width:100%;position:absolute;right:0;top:0;z-index:0}.video-cover .video-container.fade{animation:fadeIn 1s ease-in}.video-cover .video-container video{filter:grayscale(100%) contrast(1);flex:1 0 100%;mix-blend-mode:multiply;-o-object-fit:cover;object-fit:cover;opacity:.85;pointer-events:none}.video-cover .inner-wrap{color:var(--action-contrast);padding:2rem;position:relative;width:100%;z-index:2}.video-cover .inner-wrap h1,.video-cover .inner-wrap h2,.video-cover .inner-wrap h3,.video-cover .inner-wrap h4,.video-cover .inner-wrap h5,.video-cover .inner-wrap h6{color:var(--action-contrast);margin:2rem 0 0;text-shadow:0 2px 4px rgba(0,0,0,.5);word-spacing:100vw}.video-cover .inner-wrap p{color:var(--action-contrast);letter-spacing:2px;margin:0;text-shadow:0 1px 2px rgba(0,0,0,.5);text-transform:uppercase}.video-cover .inner-wrap .media-text figure{max-width:50%}@media(min-width:768px){.video-cover .inner-wrap .media-text{--align:flex-start;gap:3rem;max-width:var(--maxWidth)}}.video-cover .inner-wrap .media-text>div{width:-moz-fit-content;width:fit-content}.video-cover .inner-wrap .buttons a{border-color:var(--action-contrast);color:var(--action-contrast);font-weight:500}.video-cover .inner-wrap .buttons a:visited{color:var(--action-0)}.video-cover .inner-wrap .buttons a:visited:hover{color:var(--action-contrast)}.video-cover .inner-wrap .buttons a:hover{background-color:var(--action-0);color:var(--action-contrast)}.video-cover .inner-wrap .outline a{background-color:rgba(var(--base-rgb),var(--overlay-light))}.video-cover .inner-wrap .buttons{margin:3rem 0}.video-cover .inner-wrap .wp-block-button__link{text-shadow:none}.video-cover.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover.align-top-center{align-items:flex-start;justify-content:center}.video-cover.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover.align-center-left{align-items:center;justify-content:flex-start}.video-cover.align-center{align-items:center;justify-content:center}.video-cover.align-center-right{align-items:center;justify-content:flex-end}.video-cover.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover.alignfull{margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw);max-width:none;width:100vw}.video-cover.alignwide{max-width:1200px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}
+.video-cover{display:flex;min-height:75vh;overflow:hidden;position:relative;width:100%}.video-cover .wrap{background-color:var(--contrast-200)}.video-cover .video-container{background-color:var(--action-50);bottom:0;display:flex;left:0;min-height:100%;min-width:100%;position:absolute;right:0;top:0;z-index:0}.video-cover .video-container.fade{animation:fadeIn 1s ease-in}.video-cover .video-container video{filter:grayscale(100%) contrast(1);flex:1 0 100%;mix-blend-mode:multiply;-o-object-fit:cover;object-fit:cover;opacity:.85;pointer-events:none}.video-cover .inner-wrap{color:var(--action-contrast);padding:2rem;position:relative;width:100%;z-index:2}.video-cover .inner-wrap h1,.video-cover .inner-wrap h2,.video-cover .inner-wrap h3,.video-cover .inner-wrap h4,.video-cover .inner-wrap h5,.video-cover .inner-wrap h6{color:var(--action-contrast);margin:2rem 0 0;text-shadow:0 2px 4px rgba(0,0,0,.5);word-spacing:100vw}.video-cover .inner-wrap p{color:var(--action-contrast);letter-spacing:2px;margin:0;text-shadow:0 1px 2px rgba(0,0,0,.5);text-transform:uppercase}.video-cover .inner-wrap .media-text figure{max-width:50%}@media(min-width:768px){.video-cover .inner-wrap .media-text{--align:flex-start;gap:3rem;max-width:var(--content)}}.video-cover .inner-wrap .media-text>div{width:-moz-fit-content;width:fit-content}.video-cover .inner-wrap .buttons a{border-color:var(--action-contrast);color:var(--action-contrast);font-weight:var(--fw-h-bold)}.video-cover .inner-wrap .buttons a:visited,.video-cover .inner-wrap .buttons a:visited:hover{color:var(--action-contrast)}.video-cover .inner-wrap .buttons a:hover{background-color:var(--action-0);color:var(--action-contrast)}.video-cover .inner-wrap .outline a{background-color:rgba(var(--base-rgb),rgba(var(--base-rgb),var(--op-3)))}.video-cover .inner-wrap .buttons{margin:3rem 0}.video-cover .inner-wrap .buttons li{background-color:rgba(var(--action-rgb),var(--op-4))}.video-cover .inner-wrap .wp-block-button__link{text-shadow:none}.video-cover.align-top-left{align-items:flex-start;justify-content:flex-start}.video-cover.align-top-center{align-items:flex-start;justify-content:center}.video-cover.align-top-right{align-items:flex-start;justify-content:flex-end}.video-cover.align-center-left{align-items:center;justify-content:flex-start}.video-cover.align-center{align-items:center;justify-content:center}.video-cover.align-center-right{align-items:center;justify-content:flex-end}.video-cover.align-bottom-left{align-items:flex-end;justify-content:flex-start}.video-cover.align-bottom-center{align-items:flex-end;justify-content:center}.video-cover.align-bottom-right{align-items:flex-end;justify-content:flex-end}.video-cover.alignfull{margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw);max-width:none;width:100vw}.video-cover.alignwide{max-width:1200px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}
diff --git a/build/video/view.asset.php b/build/video/view.asset.php
new file mode 100644
index 0000000..cc5a512
--- /dev/null
+++ b/build/video/view.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array(), 'version' => '052f02098d20d7fe4bd4');
diff --git a/build/video/view.js b/build/video/view.js
new file mode 100644
index 0000000..2e03534
--- /dev/null
+++ b/build/video/view.js
@@ -0,0 +1 @@
+document.addEventListener("DOMContentLoaded",(function(){const e=[].slice.call(document.querySelectorAll(".video-container video"));function r(e){e.querySelectorAll("source[data-src]").forEach((e=>{e.src=e.dataset.src})),e.load()}if("IntersectionObserver"in window){const t=new IntersectionObserver((function(e,t){e.forEach((e=>{e.isIntersecting&&(r(e.target),t.unobserve(e.target))}))}),{rootMargin:"200px 0px",threshold:.1});e.forEach((e=>t.observe(e)))}else"requestIdleCallback"in window?requestIdleCallback((()=>{e.forEach((e=>r(e)))})):e.forEach((e=>r(e)))}));
\ No newline at end of file
diff --git a/cleanup.php b/cleanup.php
index 6444cb4..461c845 100644
--- a/cleanup.php
+++ b/cleanup.php
@@ -27,12 +27,18 @@
 	// Remove global WordPress styles
 	$global_styles = [
 		'global-styles',
+		'classic-theme-styles',
+		'core-block-supports',
 		'dashicons',
-		'core-block-supports'
+		'common',
+		'wp-block-library',
+		'wp-block-library-theme',
+		'wp-block-styles',
 	];
 
 	foreach ($global_styles as $style) {
 		wp_dequeue_style($style);
+		wp_deregister_style($style);
 	}
 
 	// Remove all block-specific styles
@@ -40,6 +46,7 @@
 	foreach ($wp_styles->queue as $handle) {
 		if (str_starts_with($handle, 'wp-block-')) {
 			wp_dequeue_style($handle);
+			wp_deregister_style($style);
 		}
 	}
 
@@ -61,7 +68,7 @@
 	// Remove third-party styles
 	wp_deregister_style('akismet-widget-style-inline-css');
 }
-add_action('wp_enqueue_scripts', 'jvbRemoveBlockAssets', 999);
+add_action('wp_enqueue_scripts', 'jvbRemoveBlockAssets', 9999);
 
 /*******************************************************************************
 WORDPRESS HEAD CLEANUP
diff --git a/iconsOld.php b/iconsOld.php
deleted file mode 100644
index 860838a..0000000
--- a/iconsOld.php
+++ /dev/null
@@ -1,761 +0,0 @@
-<?php
-namespace JVBase;
-
-use JVBase\managers\CacheManager;
-
-if (!defined('ABSPATH')) {
-	exit; // Exit if accessed directly
-}
-class JVBIcons
-{
-	static array $processing = ['map' => [],'label' => []];
-	protected string $style;
-	protected array $styles = [
-		'regular',
-		'bold',
-		'duotone',
-		'fill',
-		'light',
-		'thin'
-	];
-
-	protected array $labels = [
-		'submenu' 			=> 'Toggle Menu',
-		'dashboard' 		=> 'Dashboard',
-		'arrow-fat-down' 	=> 'Downvote',
-		'arrow-fat-up'		=> 'Upvote',
-		'hamburger'			=> 'Menu',
-		'check-square-offset'=> 'Approval Requests',
-		'h1'				=> 'Heading 1',
-		'h2'				=> 'Heading 2',
-		'h3'				=> 'Heading 3',
-		'note-pencil'		=> 'Notes',
-		'sun-dim'			=> 'Light Mode',
-		'moon'				=> 'Dark Mode',
-		'grid'				=> 'Grid View',
-		'list'				=> 'List View',
-		'clock-clockwise'	=> 'Upcoming Events',
-		'clock-counter-clockwise'=>'Past Events',
-		'repeat'			=> 'Recurring Events',
-		'floppy-disk'		=> 'Save',
-	];
-
-	protected array $map = [
-		'time'			=> 'clock',
-		'back'			=> 'arrow-u-up-left',
-		'logo'			=> 'logo.svg',
-		'logo-basic'	=> 'logo-basic.svg',
-		'save'			=> 'floppy-disk',
-		'restore'		=> 'arrow-counter-clockwise',
-		'help'			=> 'question',
-		'upload'		=> 'cloud-arrow-up',
-		'download'		=> 'cloud-arrow-down',
-		'synced'		=> 'cloud-check',
-		'syncing'		=> 'cloud-sync-thin.svg',
-		'offline'		=> 'cloud-slash',
-		'error'			=> 'cloud-warning',
-		'cart'			=> 'shopping-cart',
-		'delete'		=> 'trash',
-		'edit'			=> 'pencil-simple',
-		'bio'			=> 'user',
-		'login'			=> 'sign-in',
-		'logout'		=> 'sign-out',
-		'future'		=> 'clock-clockwise',
-		'past'			=> 'clock-counter-clockwise',
-		'light'			=> 'sun-dim',
-		'dark'			=> 'moon',
-		'h1'			=> 'text-h-one',
-		'h2'			=> 'text-h-two',
-		'h3'			=> 'text-h-three',
-		'bold'			=> 'text-b',
-		'italic'		=> 'text-italic',
-		'underline'		=> 'text-underline',
-		'strike'		=> 'text-strikethrough',
-		'image'			=> 'image-square',
-		'reply'			=> 'arrow-bend-up-left',
-		'approval'		=> 'check-square-offset',
-		'response'		=> 'chat-teardrop',
-		'karma'			=> 'scales',
-		'show'			=> 'eye',
-		'publish'		=> 'eye',
-		'hide'			=> 'eye-closed',
-		'draft'			=> 'eye-closed',
-		'asc'			=> 'sort-ascending',
-		'desc'			=> 'sort-descending',
-		'all'			=> 'infinity',
-		'random'		=> 'shuffle',
-		'location'		=> 'map-pin',
-		'hours'			=> 'clock',
-		'favourite'		=> 'heart',
-		'email'			=> 'envelope',
-		'text'			=> 'chat',
-		'integrations'	=> 'plugs-connected',
-		'connected'		=> 'plugs-connected',
-		'disconnected'	=> 'plugs',
-		'umami'			=> 'chart-line',
-		'square-up'		=> 'square-logo',
-		'tiktok'		=> 'tiktok-logo',
-		'threads'		=> 'threads-logo',
-		'twitch'		=> 'twitch-logo',
-		'u'		=> 'snapchat-logo',
-		'linktree'		=> 'linktree-logo',
-		'fediverse'		=> 'fediverse-logo',
-		'mastadon'		=> 'mastadon-logo',
-		'youtube'		=> 'youtube-logo',
-		'twitter'		=> 'twitter-logo',
-		'messenger'		=> 'messenger-logo',
-		'facebook'		=> 'facebook-logo',
-		'instagram'		=> 'instagram-logo',
-		'dash'			=> 'door',
-		'event'			=> 'calendar',
-		'events'		=> 'calendar',
-		'settings'		=> 'gear-six',
-		'grid'			=> 'squares-four',
-		'list'			=> 'rows',
-		'pinned'		=> 'push-pin',
-		'search'			=> 'magnifying-glass',
-		'add'			=> 'plus-square',
-		'minus'			=> 'minus-square',
-		'support'		=> 'question',
-		'grab'			=> 'dots-six-vertical',
-		'checkout'		=> 'receipt',
-		'home'			=> 'house',
-		'elbow-right-down' => 'arrow-elbow-right-down',
-		'elbow-right-up'=> 'arrow-elbow-right-up',
-		'elbow-left-down'=> 'arrow-elbow-left-down',
-		'elbow-left-up' => 'arrow-elbow-left-up',
-		'menu'			=> 'list',
-		'submenu'		=> 'caret-down',
-		'close'			=> 'x',
-		'close-square'	=> 'x-square',
-		'dashboard'		=> 'door',
-		'system'		=> 'gear-six',
-		'options'		=> 'gear-six',
-		'news'			=> 'newspaper',
-		'metrics'		=> 'chart-line',
-		'prev'			=> 'caret-left',
-		'up'			=> 'caret-circle-up',
-		'right'			=> 'caret-double-right',
-		'left'			=> 'caret-double-left',
-		'next'			=> 'caret-right',
-		'refresh'		=> 'arrows-clockwise',
-		'pending'		=> 'arrows-clockwise',
-		'copy'			=> 'copy-simple',
-		'align-center'	=> 'text-align-center',
-		'align-left'	=> 'text-align-left',
-		'align-right'	=> 'text-align-right',
-	//	'alphabetical'	=> 'alphabetical.svg',
-		'date'			=> 'calendar',
-		'down'			=> 'caret-double-down',
-		'tattoo'		=> 'drop-simple',
-		'tattoos'		=> 'drop-simple',
-		'theme'			=> 'folder-open',
-		'arttheme'		=> 'folder-open',
-		'style'			=> 'hash',
-		'artstyle'		=> 'hash',
-		'colour'		=> 'drop',
-		'placement'		=> 'person-arms-spread',
-		'artmedia'		=> 'squares-four',
-		'artist'		=> 'user',
-		'client'		=> 'user',
-		'artists'		=> 'users-three',
-		'partner'		=> 'currency-circle-dollar',
-		'shop'			=> 'storefront',
-		'piercing'		=> 'needle',
-		'piercings'		=> 'needle',
-		'artwork'		=> 'palette',
-		'artform'		=> 'shapes',
-		'city'			=> 'map-pin',
-		'type'			=> 'users-three',
-		'pstyle'		=> 'nut',
-		'project'		=> 'code',
-		'map'			=> 'map-trifold',
-		'offer'			=> 'gift',
-		'referrals'		=> 'hand-heart'
-	];
-
-
-	private const ICON_GROUPS = [
-		'navigation' => [
-			'back'          => ['filename' => 'back', 'label' => 'Back', 'size' => 24],
-			'home'          => ['filename' => 'house', 'label' => 'Home'],
-			'right'         => ['filename' => 'arrow-fat-right', 'label' => ''],
-			'elbow-right-down'=> ['filename' => 'arrow-elbow-right-down', 'label' => ''],
-			'elbow-right-up'=> ['filename' => 'arrow-elbow-right-up', 'label' => ''],
-			'elbow-left-down'=> ['filename' => 'arrow-elbow-left-down', 'label' => ''],
-			'elbow-left-up'=> ['filename' => 'arrow-elbow-left-up', 'label' => ''],
-			'menu'          => ['filename' => 'list', 'label' => 'Toggle Menu', 'size' => 32],
-			'submenu'       => ['filename' => 'caret-down', 'label' => 'Toggle Submenu'],
-			'close'         => ['filename' => 'x', 'label' => 'Close', 'size' => 32],
-			'close-square'  => ['filename' => 'x-square', 'label' => 'Close'],
-			'dashboard'     => ['filename' => 'door', 'label'=> 'Behind the Scenes', 'size' => 24],
-			'dash'     		=> ['filename' => 'door', 'label'=> 'Behind the Scenes', 'size' => 24],
-			'settings'      => ['filename' => 'gear-six', 'label'=> 'Settings', 'size' => 24],
-			'system'      => ['filename' => 'gear-six', 'label'=> 'Settings', 'size' => 24],
-			'options'       => ['filename' => 'gear-six', 'label'=> 'Options', 'size' => 24],
-			'news'          => ['filename' => 'newspaper', 'label' => 'News', 'size' => 24],
-			'metrics'       => ['filename' => 'chart-line', 'label' => 'Metrics', 'size' => 24],
-			'prev'          => ['filename' => 'caret-left', 'label' => 'Previous', 'size' => 24],
-			'up'            => ['filename' => 'caret-circle-up', 'label' => 'Up', 'size' => 24],
-			'down'            => ['filename' => 'caret-double-down', 'label' => 'Down', 'size' => 24],
-			'right'            => ['filename' => 'caret-double-right', 'label' => 'Right', 'size' => 24],
-			'next'          => ['filename' => 'caret-right', 'label' => 'Next', 'size' => 24],
-			'refresh'       => ['filename' => 'arrows-clockwise', 'label' => 'Refresh', 'size' => 24],
-			'pending'       => ['filename' => 'arrows-clockwise', 'label' => 'Refresh', 'size' => 24],
-			'clock'         => ['filename' => 'clock', 'label' => 'Hours'],
-			'copy'          => ['filename' => 'copy-simple', 'label' => 'Duplicate'],
-			'list-heart'    => ['filename' => 'list-heart', 'label'=> 'Lists'],
-			'downvoted'      => ['filename' => 'arrow-fat-down-fill.svg', 'label'=> 'Downvote'],
-			'upvoted'        => ['filename' => 'arrow-fat-up-fill.svg', 'label'=> 'Upvote'],
-			'downvote'      => ['filename' => 'arrow-fat-down', 'label'=> 'Downvote'],
-			'upvote'        => ['filename' => 'arrow-fat-up', 'label'=> 'Upvote'],
-			'karma'         => ['filename' => 'scales', 'label'=> 'Karma'],
-			'response'      => ['filename' => 'chat-teardrop', 'label' => 'Responses'],
-			'reply'         => ['filename' => 'arrow-bend-up-left', 'label' => 'Reply'],
-			'approval'      => ['filename' => 'check-square-offset', 'label' => 'Approval Requests'],
-			'check'         => ['filename' => 'check-circle', 'label' => 'done'],
-			'hamburger'         => ['filename' => 'hamburger', 'label' => 'menu']
-		],
-		'formatting' => [
-			'paragraph'     => ['filename' => 'paragraph', 'label' => 'Paragraph', 'size' => 24],
-			'h1'            => ['filename' => 'text-h-one', 'label' => 'Heading 1', 'size' => 24],
-			'h2'            => ['filename' => 'text-h-two', 'label' => 'Heading 2', 'size' => 24],
-			'h3'            => ['filename' => 'text-h-three', 'label' => 'Heading 3', 'size' => 24],
-			'bold'          => ['filename' => 'text-b-bold.svg', 'label' => 'Bold', 'size' => 24],
-			'italic'        => ['filename' => 'text-italic', 'label' => 'Italic', 'size' => 24],
-			'underline'     => ['filename' => 'text-underline', 'label' => 'Underline', 'size' => 24],
-			'strike'        => ['filename' => 'text-strikethrough', 'label' => 'Strikethrough', 'size' => 24],
-			'list-bullets'  => ['filename' => 'list-bullets', 'label' => 'Bulleted List', 'size' => 24],
-			'list-numbers'  => ['filename' => 'list-numbers', 'label' => 'Numbered List', 'size' => 24],
-			'image'         => ['filename' => 'image-square', 'label' => 'Image', 'size' => 24],
-			'align-left'    => ['filename' => 'text-align-left', 'label' => 'Align Left', 'size' => 24],
-			'align-right'   => ['filename' => 'text-align-right', 'label' => 'Align Right', 'size' => 24],
-			'align-center'  => ['filename' => 'text-align-center', 'label' => 'Align Center', 'size' => 24],
-			'link'          =>['filename' => 'link', 'label' => 'Link', 'size' => 24],
-			'note'          => ['filename' => 'note-pencil', 'label' => 'Notes', 'size' => 24],
-			'password'		=> ['filename' => 'password', 'label' => 'Password']
-		],
-		'theme' => [
-			'light'     => ['filename' => 'sun-dim', 'label' => 'Light Mode'],
-			'dark'      => ['filename' => 'moon', 'label' => 'Dark Mode'],
-			'grid'      => ['filename' => 'squares-four', 'label' => 'Grid View'],
-			'list'      => ['filename' => 'rows', 'label' => 'List View'],
-		],
-		'content'   => [
-			'offer'     => ['filename'=>'gift', 'label'=>'Offer'],
-			'logo' => ['filename' => 'logo.svg', 'label' => 'North\'eh', 'size' => 20],
-			'logo-basic' => ['filename' => 'logo-basic.svg', 'label' => 'North\'eh', 'size' => 20],
-			'theme'     => ['filename' => 'folder-open', 'label' => 'Theme', 'size' => 20],
-			'arttheme'  => ['filename' => 'folder-open', 'label' => 'Art Theme', 'size' => 20],
-			'style'     => ['filename' => 'hash', 'label' => 'Style', 'size' => 20],
-			'artstyle'  => ['filename' => 'hash', 'label' => 'Art Style', 'size' => 20],
-			'colour'    => ['filename' => 'drop', 'label' => 'Colour', 'size' => 20],
-			'placement' => ['filename' => 'person-arms-spread', 'label' => 'Placement', 'size' => 20],
-			'artmedia'  => ['filename' => 'squares-four', 'label'   => 'Media', 'size' => 20],
-			'artist'    => ['filename' => 'user', 'label'=> 'Artist', 'size' => 24],
-			'client'    => ['filename' => 'user', 'label'=> 'Artist', 'size' => 24],
-			'artists'   => ['filename' => 'users-three', 'label' => 'Artists'],
-			'partner'   => ['filename' => 'currency-circle-dollar', 'label' => 'Partner'],
-			'shop'      => ['filename' => 'storefront', 'label' => 'Shop', 'size' => 20],
-			'tattoo'    => ['filename' => 'drop-simple', 'label' => 'Tattoo', 'size' => 20],
-			'tattoos'   => ['filename' => 'drop-simple', 'label' => 'Your Tattoos', 'size' => 20],
-			'event'     => ['filename' => 'calendar', 'label' => 'Event', 'size' => 20],
-			'events'     => ['filename' => 'calendar', 'label' => 'Your Events', 'size' => 20],
-			'piercing'  => ['filename' => 'needle', 'label' => 'Piercings', 'size' => 20],
-			'piercings' => ['filename' => 'needle', 'label' => 'Your Piercings', 'size' => 20],
-			'artwork'   => ['filename' => 'palette', 'label' => 'Artwork', 'size' => 20],
-			'artform'   => ['filename' => 'shapes', 'label' => 'Art Form', 'size' => 20],
-			'city'      => ['filename' => 'map-pin', 'label' => 'City', 'size' => 20],
-			'type'      => ['filename' => 'users-three', 'label' => 'Artist Type', 'size' => 20],
-			'pstyle'    => ['filename' => 'nut', 'label' => 'Body Modifications', 'size' => 20],
-			'past'      => ['filename' => 'clock-counter-clockwise', 'label' => 'Past Events', 'size' => 20],
-			'future'    => ['filename' => 'clock-clockwise', 'label' => 'Upcoming Events', 'size' => 20],
-			'repeat'    => ['filename' => 'repeat', 'label' => 'Recurring Events', 'size' => 20],
-			'project'	=> ['filename' => 'code', 'label' => 'Project'],
-			'gauge' => ['filename' => 'gauge', 'label'=> 'Dashboard'],
-			'map'		=> ['filename' => 'map-trifold', 'label' => 'Map'],
-		],
-		'users' => [
-			'new-user'  => ['filename' => 'user-circle-plus', 'label' => 'New Artist'],
-			'user'      => ['filename' => 'user', 'label' => 'Artist', 'size' => 20],
-			'bio'       => ['filename' => 'user', 'label' => 'Your Bio', 'size' => 20],
-			'login'     => ['filename' => 'sign-in', 'label' => 'Login'],
-			'logout'    => ['filename' => 'sign-out', 'label' => 'Logout']
-		],
-		'actions'   => [
-			'save'  => ['filename' => 'floppy-disk', 'label' => 'Save'],
-			'restore'=> ['filename' => 'arrow-counter-clockwise', 'label' => 'Restore', 'size' => 32],
-			'edit'  => ['filename' => 'pencil-simple', 'label' => 'Edit', 'size' => 24],
-			'delete'=> ['filename' => 'trash', 'label' => 'Delete', 'size' => 32],
-			'search'=> ['filename' => 'magnifying-glass', 'label' => 'Search', 'size' => 32],
-			'add'   => ['filename' => 'plus-square', 'label' => 'Add New', 'size' => 32],
-			'minus' => ['filename' => 'minus-square', 'label' => 'Collapse', 'size' => 32],
-			'help'  => ['filename' => 'question', 'label' => 'Toggle Quick Help'],
-			'support'  => ['filename' => 'question', 'label' => 'Toggle Quick Help'],
-			'grab'  => ['filename' => 'dots-six-vertical', 'label'=> 'Grab'],
-			'share' => ['filename' => 'share', 'label' => 'Share', 'size'=> 24],
-			'cart' => ['filename' => 'shopping-cart', 'label' => 'Your Cart', 'size'=> 24],
-			'checkout' => ['filename' => 'receipt', 'label' => 'Checkout', 'size'=> 24],
-			'upload' => ['filename' => 'cloud-arrow-up', 'label' => 'Upload to Server', 'size'=> 24],
-			'download' => ['filename' => 'cloud-arrow-down', 'label' => 'Downloading', 'size'=> 24],
-			'synced' => ['filename' => 'cloud-check', 'label' => 'Synced', 'size'=> 24],
-			'syncing' => ['filename' => 'cloud-sync-thin.svg', 'label' => 'Synced', 'size'=> 24],
-			'cloud' => ['filename' => 'cloud', 'label' => 'Synced', 'size'=> 24],
-//            'pending' => ['filename' => 'cloud', 'label' => 'Synced', 'size'=> 24],
-			'offline' => ['filename' => 'cloud-slash', 'label' => 'Offline', 'size'=> 24],
-			'error' => ['filename' => 'cloud-warning', 'label' => 'Upload Failed', 'size'=> 24],
-
-		],
-		'status' => [
-			'show'          => ['filename' => 'eye', 'label' => 'Show', 'size' => 20],
-			'publish'       => ['filename' => 'eye', 'label' => 'Public', 'size' => 20],
-			'hide'          => ['filename' => 'eye-closed', 'label' => 'Hide', 'size' => 20],
-			'draft'         => ['filename' => 'eye-closed', 'label' => 'Hidden', 'size' => 20],
-			'pinned'        => ['filename' => 'push-pin-simple', 'label' => 'Pin', 'size' => 32],
-			'bell'          => ['filename' => 'bell', 'label' => 'Notification', 'size' => 32],
-			'bell-ringing'  => ['filename' => 'bell-ringing', 'label' => 'Notification', 'size' => 32]
-		],
-		'sorting' => [
-			'table' => ['filename' => 'table', 'label' => 'Table View', 'size' => 20],
-			'columns' => ['filename' => 'columns', 'label' => 'Show Columns', 'size' => 20],
-			'alphabetical' => ['filename' => 'alphabetical', 'label' => 'Alphabetical', 'size' => 20],
-			'calendar' => ['filename' => 'calendar-heart', 'label' => 'Date', 'size' => 20],
-			'asc' => ['filename' => 'sort-ascending', 'label' => 'Sort Ascending', 'size' => 20],
-			'ASC' => ['filename' => 'sort-ascending', 'label' => 'Sort Ascending', 'size' => 20],
-			'desc' => ['filename' => 'sort-descending', 'label' => 'Sort Descending', 'size' => 20],
-			'DESC' => ['filename' => 'sort-descending', 'label' => 'Sort Descending', 'size' => 20],
-			'filter' => ['filename' => 'faders', 'label' => 'Filter'],
-			'all' => ['filename' => 'infinity', 'label' => 'All', 'size' => 32],
-			'random' => ['filename' => 'shuffle', 'label' => 'Random', 'size' => 20]
-		],
-		'business' => [
-			'location' => ['filename' => 'map-pin', 'label' => 'Location'],
-			'hours' => ['filename' => 'clock', 'label' => 'Hours'],
-			'star'  => ['filename' => 'star', 'label' => 'Empty Star'],
-			'star-fill'  => ['filename' => 'star-fill.svg', 'label' => 'Star'],
-			'star-half'  => ['filename' => 'star-half-fill.svg', 'label' => 'Half Star'],
-		],
-		'social' => [
-			'heart'     => ['filename' => 'heart', 'label' => 'Favourite', 'size' => 24],
-			'favourite'     => ['filename' => 'heart', 'label' => 'Favourite', 'size' => 24],
-			'favourites'=> ['filename' => 'heart', 'label' => 'Your Favourites', 'size' => 24],
-			'heart-fill'=> ['filename' => 'heart-fill.svg', 'label' => 'Favourited', 'size' => 24],
-			'share'     => ['filename' => 'share', 'label' => 'Share', 'size' => 32],
-			'email'     => ['filename' => 'envelope','label'=> 'Email'],
-			'text'      => ['filename' => 'chat','label'=> 'Text'],
-			'phone'     => ['filename' => 'phone','label'=> 'Call'],
-		],
-		'external'   => [
-			'integrations' => ['filename' => 'plugs-connected', 'label' => 'Integrations'],
-			'connected' => ['filename' => 'plugs-connected', 'label' => 'Connected'],
-			'disconnected' => ['filename' => 'plugs', 'label' => 'Disconnected'],
-			'instagram' => ['filename' => 'instagram-logo', 'label' => 'Instagram'],
-			'facebook'  => ['filename' => 'facebook-logo', 'label' => 'Facebook'],
-			'messenger' => ['filename' => 'messenger-logo', 'label' => 'Facebook Messenger'],
-			'twitter'   => ['filename' => 'twitter-logo', 'label' => 'Twitter'],
-			'x'   => ['filename' => 'x-logo', 'label' => 'X'],
-			'youtube'   => ['filename' => 'youtube-logo', 'label' => 'YouTube'],
-			'mastadon'   => ['filename' => 'mastadon-logo', 'label' => 'Mastadon'],
-			'fediverse'   => ['filename' => 'fediverse-logo', 'label' => 'Fediverse'],
-			'linktree'   => ['filename' => 'linktree-logo', 'label' => 'LinkTree'],
-			'snapchat'   => ['filename' => 'snapchat-logo', 'label' => 'Snapchat'],
-			'twitch'   => ['filename' => 'twitch-logo', 'label' => 'Twitch'],
-			'threads'   => ['filename' => 'threads-logo', 'label' => 'Threads'],
-			'tiktok'   => ['filename' => 'tiktok-logo', 'label' => 'TikTok'],
-			'square-up'   => ['filename' => 'square-logo', 'label' => 'Square'],
-			'umami'   => ['filename' => 'chart-line', 'label' => 'Umami'],
-		]
-	];
-	private string $defaultStyle = '-thin';
-	private int $defaultSize = 16;
-	protected string $name;
-	protected array $used;
-	protected int $size = 20;
-	protected CacheManager $cache;
-
-	public function __construct()
-	{
-		$this->cache = new CacheManager('icons', 604800); //1 week in seconds
-//		$this->cache->invalidateGroup('icons');
-		$this->style = JVB_SITE['icons']??'regular';
-
-
-		$this->used = get_option(BASE.'used_icons', [
-			$this->style => [
-				'heart',
-				'hours',
-				'random',
-				'alphabetical',
-				'calendar',
-				'asc',
-				'desc',
-				'all',
-				'paragraph',
-				'h1',
-				'h2',
-				'h3',
-				'bold',
-				'italic',
-				'underline',
-				'strike',
-				'list-bullets',
-				'list-numbers',
-				'image',
-				'align-left',
-				'align-right',
-				'align-center',
-				'link',
-				'note',
-				'password',
-				'light',
-				'left',
-				'dark',
-				'grid',
-				'list',
-				'save',
-				'restore',
-				'edit',
-				'delete',
-				'search',
-				'add',
-				'minus',
-				'help',
-				'support',
-				'grab',
-				'share',
-				'cart',
-				'checkout',
-				'upload',
-				'download',
-				'synced',
-				'syncing',
-				'cloud',
-				'offline',
-				'error',
-				'show',
-				'publish',
-				'hide',
-				'draft',
-				'pinned',
-				'bell',
-				'bell-ringing',
-				'back',
-				'home',
-				'right',
-				'elbow-right-down',
-				'elbow-right-up',
-				'elbow-left-down',
-				'elbow-left-up',
-				'menu',
-				'submenu',
-				'close',
-				'close-square',
-				'dashboard',
-				'dash',
-				'settings',
-				'system',
-				'options',
-				'news',
-				'metrics',
-				'prev',
-				'up',
-				'down',
-				'right',
-				'copy',
-				'next',
-				'refresh',
-				'pending',
-				'clock',
-				'copy',
-				'list-heart',
-				'karma',
-				'response',
-				'reply',
-				'approval',
-				'check',
-				'hamburger',
-				'location',
-				'hours',
-				'star',
-				'star-half',
-				'exclamation-mark'
-			],
-			'fill'	=> [
-				'heart',
-				'arrow-fat-down',
-				'arrow-fat-up',
-				'star'
-			]
-		]);
-	}
-
-	protected function updateUsed():void
-	{
-		$icons = [];
-		foreach ($this->used as $style => $items) {
-			$temp = array_unique($items);
-			sort($temp);
-			$icons[$style] = $temp;
-		}
-		update_option(BASE . 'used_icons', $icons);
-	}
-
-	protected function checkMap(string $name): string
-	{
-		$result = apply_filters('jvbIconMap', (array_key_exists($name, $this->map)) ? $this->map[$name] : $name, $name);
-		return $result;
-	}
-
-	protected function checkName(string $name) {
-		$name = $this->checkMap($name);
-		$path = (str_contains($name, '.svg')) ? '/assets/icons/' : '/assets/phosphor-icons/regular/';
-		$filename = (str_contains($name, '.svg')) ? $name : $name.'.svg';
-		return file_exists(JVB_DIR . $path . $filename);
-	}
-	public function getIcon(string $name, array $options = []):?string
-	{
-		if (!$this->checkName($name)) {
-			error_log('[Icons]Icon not found for: '.print_r($name, true));
-			return '';
-		}
-		$style = (array_key_exists('style', $options) && in_array($options['style'], $this->styles)) ? $options['style'] : 'regular';
-
-		$update = false;
-		if (!array_key_exists($style, $this->used)) {
-			$this->used[$style] = [];
-			$update = true;
-		}
-		if (!in_array($name, $this->used[$style])) {
-			$this->used[$style][] = $name;
-			$update = true;
-		}
-		if ($update) {
-			$this->updateUsed();
-		}
-
-		$name = $this->checkMap($name);
-		$this->name = str_replace('.svg', '', $name);
-
-
-		// Merge options with defaults and icon config
-		$options = array_merge([
-			'title' => $options['label']??$this->getIconLabel($name),
-			'size' => $options['size'] ?? $this->size,
-			'style' => $style,
-			'class' => '',
-			'wrap'  => true,
-			'color' => 'currentColor'
-		], $options);
-
-		return $this->cache->remember(
-			array_merge($options, ['name' => $name]),
-			function () use ($name, $options) {
-				return $this->buildIcon($name, $options);
-			}
-		);
-	}
-
-	public function getIconsByGroup(string $group):array
-	{
-		if (!isset(self::ICON_GROUPS[$group])) {
-			return [];
-		}
-
-		$icons = [];
-		foreach (self::ICON_GROUPS[$group] as $name => $config) {
-			$icons[$name] = $this->getIcon($name);
-		}
-
-		return array_filter($icons);
-	}
-
-	public function getAllIcons():array
-	{
-		$icons = [];
-		foreach (self::ICON_GROUPS as $group => $groupIcons) {
-			foreach ($groupIcons as $name => $config) {
-				$icons[$name] = $this->getIcon($name);
-			}
-		}
-
-		return array_filter($icons);
-	}
-
-	public function getGroups():array
-	{
-		return array_keys(self::ICON_GROUPS);
-	}
-
-	private function findIconConfig(string $name):?array
-	{
-		foreach (self::ICON_GROUPS as $groupIcons) {
-			if (isset($groupIcons[$name])) {
-				return $groupIcons[$name];
-			}
-		}
-		return null;
-	}
-
-	private function buildIcon(string $name, array $options):?string
-	{
-
-		$filepath = $this->buildFilePath($name, $options['style']);
-		if (!file_exists($filepath)) {
-			error_log("Icon file not found: $filepath");
-			return null;
-		}
-
-		$svg = file_get_contents($filepath);
-		if ($svg === false) {
-			return null;
-		}
-
-		return $this->formatSvg($svg, $options);
-	}
-
-	private function buildFilePath(string $filename, string $style = ''):string
-	{
-		$svg = false;
-		if (str_contains($filename, '.svg')) {
-			$svg = true;
-			$nameExtra = '';
-			$path = '/assets/icons/';
-		} else {
-			$nameExtra = ($style === 'regular') ? '' : '-'.$style;
-			$path = '/assets/phosphor-icons/';
-		}
-
-		$filename = (str_contains($filename, '.svg')) ? $filename : $filename . $nameExtra . '.svg';
-		$style = ($style === '') ? '' : $style .'/';
-		$style = ($svg) ? '' : $style;
-		return JVB_DIR . $path . $style . $filename;
-	}
-
-	private function formatSvg(string $svg, array $options): string
-	{
-		// Clean up SVG
-		$svg = preg_replace("/([\n\t]+)/", ' ', $svg);
-		$svg = preg_replace('/>\s*</', '><', $svg);
-
-		// Add size attributes - FIXED: assign results back to $svg
-		if ($options['size'] !== 32) {
-			$svg = str_replace('width="32"', 'width="' . (int)$options['size'] . '"', $svg);
-			$svg = str_replace('height="32"', 'height="' . (int)$options['size'] . '"', $svg);
-		}
-
-		// Add color if provided - FIXED: assign results back to $svg
-		if ($options['color'] !== 'currentColor') {
-			$svg = str_replace('currentColor', $options['color'], $svg);
-		}
-
-		// Add title if provided
-		if (!empty($options['title'])) {
-			$svg = str_replace('<svg', '<svg title="' . esc_attr($options['title']) . '"', $svg);
-			$svg = str_replace('aria-hidden="true"', '', $svg);
-		}
-
-		$classes = trim('icon row ' . $this->name . ' ' . $options['class']);
-		if (array_key_exists('wrap', $options) && $options['wrap'] === false) {
-			return $svg;
-		}
-		return sprintf(
-			'<i class="%s">%s</i>',
-			esc_attr($classes),
-			$svg
-		);
-	}
-
-	/****************************
-	 * Converts to CSS icon
-	 ***************************/
-	public function getCSSIcon(string $name, array $options = []): ?string
-	{
-		if (!$this->checkName($name)) {
-			return '';
-		}
-		$name = $this->checkMap($name);
-		$style = (array_key_exists('style', $options) && in_array($options['style'], $this->styles)) ? $options['style'] : 'regular';
-
-		$this->name = str_replace('.svg', '', $name);
-
-		// Merge options with defaults and icon config
-		$options = array_merge([
-			'title' => $options['label']??$this->getIconLabel($name),
-			'size' => $options['size'] ?? $this->size,
-			'style' => $style,
-			'class' => '',
-			'wrap'  => true,
-			'color' => 'currentColor'
-		], $options);
-
-		$svg = $this->cache->remember(
-			array_merge($options, ['name' => $name, 'css' => true]),
-			function() use ($name, $options) {
-				return $this->buildRawSvg($name, $options);
-			}
-		);
-		// Convert to base64 data URI
-		$svg = ($svg) ? 'data:image/svg+xml;base64,' . base64_encode($svg) : null;
-		return $svg;
-	}
-
-	private function buildRawSvg(string $filename, array $options): ?string
-	{
-		$filepath = $this->buildFilePath($filename, $options['style']);
-		if (!file_exists($filepath)) {
-			error_log("Icon file not found: $filepath");
-			return null;
-		}
-
-		$svg = file_get_contents($filepath);
-		if ($svg === false) {
-			return null;
-		}
-
-		return $this->formatRawSvg($svg, $options);
-	}
-
-	private function formatRawSvg(string $svg, array $options): string
-	{
-		// Clean up SVG
-		$svg = preg_replace("/([\n\t]+)/", ' ', $svg);
-		$svg = preg_replace('/>\s*</', '><', $svg);
-
-		// Add size attributes - FIXED: assign results back to $svg
-		if ($options['size'] > 0) {
-			$svg = str_replace('width="32"', 'width="' . (int)$options['size'] . '"', $svg);
-			$svg = str_replace('height="32"', 'height="' . (int)$options['size'] . '"', $svg);
-		}
-
-		// Add color if provided and not currentColor - FIXED: assign results back to $svg
-		if ($options['color'] !== 'currentColor') {
-			$svg = str_replace('currentColor', $options['color'], $svg);
-		}
-
-		return $svg;
-	}
-
-	public function localizeIcons():array
-	{
-		if (empty($this->used)) {
-			return [];
-		}
-		$used = [];
-		foreach ($this->used as $style => $icons) {
-			foreach ($icons as $icon) {
-				$used[$icon] = $this->getIcon($icon, ['style' => $style]);
-			}
-		}
-		return $used;
-	}
-
-	public function getIconLabel(string $name): string
-	{
-		$name = str_replace('.svg', '', $name);
-		$label = (array_key_exists($name, $this->labels)) ? $this->labels[$name] : '';
-
-		$result = apply_filters('jvbIconLabel', $label, $name);
-
-		return $result;
-	}
-}
diff --git a/inc/admin/Integrations.php b/inc/admin/Integrations.php
index 2604c8b..a774548 100644
--- a/inc/admin/Integrations.php
+++ b/inc/admin/Integrations.php
@@ -247,7 +247,7 @@
 				--mt: 1rem;
 				--mb: 1rem;
 				--setMargin: var(--mt) var(--mr) var(--mb) var(--ml);
-				--insetMargin: var(--mt) calc((var(--maxWidth) - var(--narrow)) / 2 + var(--mr)) var(--mb) var(--ml);
+				--insetMargin: var(--mt) calc((var(--content) - var(--narrow)) / 2 + var(--mr)) var(--mb) var(--ml);
 				--height: 4rem;
 				--doubleHeight: 8rem;
 				--offHeight: 5rem;
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 726e78c..52b25d1 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -104,6 +104,9 @@
             // Enqueue the feed block script (it will automatically load dependencies)
             $this->localize_feedblock();
         }
+		if ($block['blockName'] === 'jvb/forms') {
+			wp_enqueue_style('jvb-form');
+		}
         return $content;
     }
 
@@ -138,6 +141,7 @@
 		if (str_contains($url[1], 'maps.apple.com')) {
 			$icon = 'apple-logo';
 		}
+
 		if ($icon !== '') {
 			return sprintf(
 				'<li%s><a href="%s" title="Find Us On %s">%s Maps</a></li>',
@@ -184,8 +188,9 @@
     protected function render_core_group(array $block):string
     {
         $tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
+
         $classes = ($tag === 'main') ?
-            $this->getClassesAndStyles($block['attrs']) :
+            '' :
             $this->getClassesAndStyles($block['attrs'], ['group']);
         return '<'.$tag.$classes.'>'.$this->innerBlocks($block).'</'.$tag.'>';
     }
@@ -282,10 +287,10 @@
                 wp_get_attachment_caption($ID) .
             '</figcaption>' :
             '<figcaption>' . $title . '</figcaption>';
-
+		$size = array_key_exists('sizeSlug', $block['attrs']) ? $block['attrs']['sizeSlug'] : 'large';
         return '<figure'.
                $this->getClassesAndStyles($block['attrs']).'>'.
-               $this->imageLink(true, $ID) .
+               $this->imageLink(true, $ID, 'tiny', $size) .
                $caption.'</figure>';
     }
 
@@ -293,7 +298,9 @@
     {
 
         $ID = $this->imageID('', $block);
-        $imgLink = ($ID) ? $this->imageLink(true, $ID) : '';
+
+		$size = array_key_exists('mediaSizeSlug', $block['attrs']) ? $block['attrs']['mediaSizeSlug'] : 'large';
+        $imgLink = ($ID) ? $this->imageLink(true, $ID, 'tiny', $size) : '';
 
         $inner = $this->innerBlocks($block);
 
@@ -513,10 +520,11 @@
             home_url($block['attrs']['url']) :
             $block['attrs']['url'];
         $current = (home_url($wp->request.'/') == $url);
-
+		$temp = $block['attrs'];
+		unset($temp['url']);
         $classes = ($current) ?
-            $this->getClassesAndStyles($block['attrs'], ['current']):
-            $this->getClassesAndStyles($block['attrs']);
+            $this->getClassesAndStyles($temp, ['current']):
+            $this->getClassesAndStyles($temp);
         $aria = '';
         if ($current) {
             $aria = ' aria-current="page"';
@@ -535,9 +543,11 @@
             $block['attrs']['url'];
         $current = (home_url($wp->request) == $url);
 
+		$temp = $block['attrs'];
+		unset($temp['url']);
         $classes = ($current) ?
-            $this->getClassesAndStyles($block['attrs'], ['has-submenu', 'current']):
-            $this->getClassesAndStyles($block['attrs'], ['has-submenu']);
+            $this->getClassesAndStyles($temp, ['has-submenu', 'current']):
+            $this->getClassesAndStyles($temp, ['has-submenu']);
 
         $aria = '';
         if ($current) {
@@ -816,15 +826,18 @@
                 $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
 				$showThemeSwitch = (bool)apply_filters('jvb_show_theme_switch', true);
                 $themeSwitch = ($showThemeSwitch) ? '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
-                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
+                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
 					jvbIcon('sun-dim', ['title'=> 'Light Mode']).
 					jvbIcon('moon', ['title'=>'Dark Mode']).
 					'</span></label>' : '';
                 $breadcrumbs = jvbBuildBreadcrumbs();
 				$afterHeader = apply_filters('jvbBelowHeader', $afterHeader);
+
 				if ($afterHeader !== '') {
 					$afterHeader = '<aside class="sub-header">'.$afterHeader.'</aside>';
 				}
+				$footerText = '<div class="scroll-progress"><div class="bar"></div>
+</div>';
             } elseif ($isFooterTemplate) {
 				$beforeHeader = apply_filters('jvbBeforeFooter', '');
 				if ($beforeHeader !== '') {
@@ -1196,9 +1209,9 @@
 				$type = 'row';
                 if (array_key_exists('type', $value)) {
 					$type = 'col';
-                    if ($value['type'] === 'constrained') {
-                        $classes[] = 'container col';
-                    }
+//                    if ($value['type'] === 'constrained') {
+//                        $classes[] = 'container col';
+//                    }
                 }
 				if (array_key_exists('orientation', $value)) {
 					$type = 'col';
@@ -1501,6 +1514,7 @@
 
             // Background URL (for cover, media blocks)
             case 'url':
+				jvbDump($value);
                 if (!empty($value) && str_starts_with($value, 'http')) {
                     $styles[] = 'background-image: url('.$value.')';
                 }
diff --git a/inc/blocks/FormBlock.php b/inc/blocks/FormBlock.php
index 640368a..b8f4ed9 100644
--- a/inc/blocks/FormBlock.php
+++ b/inc/blocks/FormBlock.php
@@ -52,7 +52,8 @@
 	public function registerBlock()
 	{
 		register_block_type($this->path, [
-			'render_callback'	=> [$this, 'render']
+			'render_callback'	=> [$this, 'render'],
+        	'style' => 'jvb-icons-forms',
 		]);
 	}
 
diff --git a/inc/blocks/GlossaryBlock.php b/inc/blocks/GlossaryBlock.php
index 5f8c394..5681ce9 100644
--- a/inc/blocks/GlossaryBlock.php
+++ b/inc/blocks/GlossaryBlock.php
@@ -38,7 +38,6 @@
 	public function render(array $attributes, string $content, WP_Block $block)
 	{
 		$cache = $this->cache->get('all');
-		$cache = false;
 		if ($cache) {
 			return $cache;
 		}
diff --git a/inc/blocks/MenuBlock.php b/inc/blocks/MenuBlock.php
index cabb11f..e52f2a2 100644
--- a/inc/blocks/MenuBlock.php
+++ b/inc/blocks/MenuBlock.php
@@ -58,7 +58,6 @@
 		}
 		$key = $this->cache->generateKey($this->params);
         $cache = $this->cache->get($key);
-        $cache = false;
         if ($cache) {
             return $cache;
         }
diff --git a/inc/blocks/SummaryBlock.php b/inc/blocks/SummaryBlock.php
index 7e92262..527e523 100644
--- a/inc/blocks/SummaryBlock.php
+++ b/inc/blocks/SummaryBlock.php
@@ -55,7 +55,6 @@
         $this->config = $this->getConfig();
         $key = $this->generateKey();
         $cache = $this->cache->get($key);
-        $cache = false;
         if ($cache) {
             return $cache;
         }
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
index 4d8e9d1..2f0bd4a 100644
--- a/inc/blocks/TimelineBlock.php
+++ b/inc/blocks/TimelineBlock.php
@@ -51,7 +51,6 @@
 		}
 		$this->parentID = $post->ID;
         $cache = $this->cache->get($this->parentID);
-        $cache = false;
         if ($cache) {
             return $cache;
         }
diff --git a/inc/blocks/VideoCoverBlock.php b/inc/blocks/VideoCoverBlock.php
index 90bda6a..cb70517 100644
--- a/inc/blocks/VideoCoverBlock.php
+++ b/inc/blocks/VideoCoverBlock.php
@@ -92,13 +92,13 @@
 			$html .= ' poster="' . esc_url($poster_url) . '"';
 		}
 
-		$html .= '>';
+		$html .= ' fetch-priority="high">';
 
 		// Add mobile sources first (lower resolution)
 		foreach ($mobile_sources as $source) {
 			if (!empty($source['url']) && !empty($source['mime'])) {
 				$html .= '<source';
-				$html .= ' src="' . esc_url($source['url']) . '"';
+				$html .= ' data-src="' . esc_url($source['url']) . '"';
 				$html .= ' type="' . esc_attr($source['mime']) . '"';
 				$html .= ' media="(max-width: 767px)"';
 				$html .= '>';
@@ -109,7 +109,7 @@
 		foreach ($video_sources as $source) {
 			if (!empty($source['url']) && !empty($source['mime'])) {
 				$html .= '<source';
-				$html .= ' src="' . esc_url($source['url']) . '"';
+				$html .= ' data-src="' . esc_url($source['url']) . '"';
 				$html .= ' type="' . esc_attr($source['mime']) . '"';
 
 				// Add media query for desktop if mobile sources exist
diff --git a/inc/helpers/all.php b/inc/helpers/all.php
index 0db6425..59ef251 100644
--- a/inc/helpers/all.php
+++ b/inc/helpers/all.php
@@ -10,7 +10,7 @@
 require(JVB_DIR . '/inc/helpers/crud.php');
 require(JVB_DIR . '/inc/helpers/dashboard.php');
 require(JVB_DIR . '/inc/helpers/directory.php');
-require(JVB_DIR . '/inc/helpers/email.php');
+//require(JVB_DIR . '/inc/helpers/email.php');
 require(JVB_DIR . '/inc/helpers/forms.php');
 require(JVB_DIR . '/inc/helpers/formatting.php');
 //require(JVB_DIR . '/inc/helpers/icons.php');
diff --git a/inc/helpers/breadcrumbs.php b/inc/helpers/breadcrumbs.php
index 81cb9b8..020a700 100644
--- a/inc/helpers/breadcrumbs.php
+++ b/inc/helpers/breadcrumbs.php
@@ -1,225 +1,128 @@
 <?php
+/**
+ * Breadcrumb Helper Functions
+ *
+ * These are backwards-compatible wrappers around BreadcrumbManager
+ * Use BreadcrumbManager directly for new code
+ */
 
-use JVBase\managers\CacheManager;
-use JVBase\utility\Features;
+use JVBase\managers\SEO\BreadcrumbManager;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
 /**
- * Outputs the breadcrumb list as an array
+ * Get breadcrumb array for current page
+ *
+ * @deprecated Use BreadcrumbManager::getInstance()->getCrumbs() instead
  * @return array
  */
-function jvbGetCrumbs():array
+function jvbGetCrumbs(): array
 {
-    $cache = CacheManager::for('breadcrumbs', MONTH_IN_SECONDS)->connectTo('all');
-    $key = get_queried_object_id();
-    $crumbs = $cache->get($key);
-	$crumbs = false;
-    if ($crumbs) {
-        return $crumbs;
-    }
-
-    $crumbs = [];
-    $crumbs[] = [
-        'name'  => 'Home',
-        'icon'  => jvbIcon('house'),
-        'url'   => get_home_url(),
-    ];
-
-    $obj = get_queried_object();
-
-    //taxonomies extra
-    if (is_tax()) {
-		$tax = jvbNoBase($obj->taxonomy);
-		$config = Features::getConfig($tax, 'term');
-		if (count($config['for_content']) === 1) {
-			$contentConfig = JVB_CONTENT[$config['for_content'][0]];
-			$crumbs[] = [
-				'name'	=> $contentConfig['breadcrumb']??$contentConfig['plural'],
-				'url'	=> get_post_type_archive_link(jvbCheckBase($config['for_content'][0])),
-			];
-			$crumbs[] = [
-				'name'	=> 'By '.$config['singular'],
-				'url'	=> false,
-			];
-		}
-		if (Features::forTaxonomy($tax)->has('directory')){
-			$directory = jvbDirectories($tax);
-			$crumbs[] = [
-				'name'  => $directory['title'],
-				'url'   => $directory['url']
-			];
-		}
-
-        $crumbs = array_merge($crumbs, jvbGetBreadcrumbTermHierarchy($obj));
-
-    }
-    if (is_singular()) {
-        $directory = jvbDirectories(jvbNoBase($obj->post_type));
-        if (!empty($directory)) {
-            $crumbs[] = [
-                'name'  => $directory['title'],
-                'url'   => $directory['url']
-            ];
-        }
-
-        if (jvbIsDirectory()) {
-            $pos = jvbGetDirectoryInfo();
-            if (!empty($pos)) {
-                $name = $pos['title'];
-
-
-                if ($name == 'Map') {
-                    $crumbs[] = array(
-                        'name'  => 'Tattoo Shops',
-                        'url'   => jvbDirectories(BASE.'shop')['url']
-                    );
-                }
-
-                $crumbs[] = array(
-                    'name'  => $name,
-                    'url'   => $pos['url']
-                );
-            }
-        } else {
-//
-//            $crumbs[] = array(
-//                'name'  => get_the_title(),
-//                'url'   => false,
-//            );
-            $crumbs = array_merge($crumbs, jvbGetBreadcrumbPostHierarchy($obj));
-        }
-
-    } elseif (is_post_type_archive() && !is_post_type_archive(BASE.'dash')) {
-		$name = jvbNoBase($obj->name);
-		$crumbs[] = array(
-			'name'	=> JVB_CONTENT[$name]['breadcrumb']??JVB_CONTENT[$name]['plural'],
-			'url'	=> false,
-		);
-	}
-    $cache->set($key, $crumbs);
-    return $crumbs;
+	return BreadcrumbManager::getInstance()->getCrumbs();
 }
 
-
 /**
+ * Build and return breadcrumb navigation HTML
+ *
+ * @deprecated Use BreadcrumbManager::getInstance()->renderNavigation() instead
  * @return string
  */
-function jvbBuildBreadcrumbs():string
+function jvbBuildBreadcrumbs(): string
 {
-    if (is_front_page()) {
-        return '';
-    }
-    $crumbs = jvbGetCrumbs();
-
-    $out = '<nav id="breadcrumbs">';
-    $out .= '<ol itemscope itemtype="https://schema.org/BreadcrumbList">';
-
-    if (!empty($crumbs)) {
-        $i = 1;
-        foreach ($crumbs as $crumb) {
-            $label = '<span itemprop="name">'.strtolower($crumb['name']).'</span>';
-            if (array_key_exists('icon', $crumb)) {
-                $label = $crumb['icon'].'<span class="screen-reader-text" itemprop="name">'.$crumb['name'].'</span>';
-            }
-            $aOpen = $aClose = '';
-            if ($crumb['url'] !== false) {
-				if (array_key_exists('id', $crumb) && $crumb['id'] === get_queried_object_id()){
-
-				} else {
-					$aOpen = '<a itemprop="item" href="'.$crumb['url'].'" title="'.$crumb['name'].'">';
-					$aClose = '</a>';
-				}
-            }
-            $out .= '<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">'.$aOpen.$label.$aClose.'<meta itemprop="position" content="'.$i.'" /></li>';
-            $i++;
-        }
-    }
-
-    $out .= '</ol>';
-    $out .= '</nav>';
-
-    return $out;
+	return BreadcrumbManager::getInstance()->renderNavigation();
 }
 
-
 /**
- * Builds a breadcrumb list of post parents, if available
+ * Build post hierarchy for breadcrumbs
+ *
+ * @deprecated Use BreadcrumbManager directly - this is now a private method
  * @param WP_Post $post
  * @param array $crumbs
- *
  * @return array
  */
-function jvbGetBreadcrumbPostHierarchy(WP_Post $post, array $crumbs = []):array
+function jvbGetBreadcrumbPostHierarchy(WP_Post $post, array $crumbs = []): array
 {
+	// This functionality is now private in BreadcrumbManager
+	// If you need this, use the full getCrumbs() method instead
+	trigger_error('jvbGetBreadcrumbPostHierarchy is deprecated. Use BreadcrumbManager::getInstance()->getCrumbs()', E_USER_DEPRECATED);
 
-    array_unshift($crumbs, [
-        'name'  => $post->post_title,
-        'url'   => get_the_permalink($post->ID),
-		'id'	=> $post->ID,
-    ]);
+	array_unshift($crumbs, [
+		'name' => $post->post_title,
+		'url'  => get_the_permalink($post->ID),
+		'id'   => $post->ID,
+	]);
 
-    if ($post->post_parent !== 0) {
-        $parent = get_post($post->post_parent);
-        if ($parent) {
-            $crumbs = jvbGetBreadcrumbPostHierarchy($parent, $crumbs);
-        }
-    }
+	if ($post->post_parent !== 0) {
+		$parent = get_post($post->post_parent);
+		if ($parent) {
+			$crumbs = jvbGetBreadcrumbPostHierarchy($parent, $crumbs);
+		}
+	}
 
-    return $crumbs;
+	return $crumbs;
 }
 
-
 /**
- * Builds a breadcrumb list of parent terms, if available
+ * Build term hierarchy for breadcrumbs
+ *
+ * @deprecated Use BreadcrumbManager directly - this is now a private method
  * @param WP_Term $term
  * @param array $crumbs
+ * @return array
+ */
+function jvbGetBreadcrumbTermHierarchy(WP_Term $term, array $crumbs = []): array
+{
+	// This functionality is now private in BreadcrumbManager
+	trigger_error('jvbGetBreadcrumbTermHierarchy is deprecated. Use BreadcrumbManager::getInstance()->getCrumbs()', E_USER_DEPRECATED);
+
+	$url = get_term_link($term->term_id);
+	array_unshift($crumbs, [
+		'name' => $term->name,
+		'url'  => $url,
+		'id'   => $term->term_id,
+	]);
+
+	if ($term->parent !== 0) {
+		$parent = get_term($term->parent, $term->taxonomy);
+		if ($parent && !is_wp_error($parent)) {
+			$crumbs = jvbGetBreadcrumbTermHierarchy($parent, $crumbs);
+		}
+	}
+
+	return $crumbs;
+}
+
+/**
+ * Get directory info (kept for now as it's not breadcrumb-specific)
  *
  * @return array
  */
-function jvbGetBreadcrumbTermHierarchy(WP_Term $term, array $crumbs=[]):array
+function jvbGetDirectoryInfo(): array
 {
-    $url = get_term_link($term->term_id);
-    array_unshift($crumbs, [
-        'name'  => $term->name,
-        'url'   => $url,
-		'id'	=> $term->term_id,
-    ]);
+	if (is_post_type_archive(BASE.'directory')) {
+		return [
+			'title' => 'Directory of Directories',
+			'url'   => get_post_type_archive_link(BASE.'directory'),
+			'slug'  => 'directory',
+			'type'  => 'directory'
+		];
+	}
 
-    if ($term->parent !== 0) {
-        $parent = get_term($term->parent, $term->taxonomy);
-        if ($parent) {
-            $crumbs = jvbGetBreadcrumbTermHierarchy($parent, $crumbs);
-        }
-    }
-    return $crumbs;
-}
+	if (is_singular(BASE.'directory')) {
+		$type = get_post_meta(get_the_ID(), BASE.'for_type_slug', true);
+		return jvbDirectories()[$type] ?? [];
+	}
 
-function jvbGetDirectoryInfo():array
-{
-    if (is_post_type_archive(BASE.'directory')) {
-        return [
-            'title' => 'Directory of Directories',
-            'url'   => get_post_type_archive_link(BASE.'directory'),
-            'slug'  => 'directory',
-            'type'  => 'directory'
-        ];
-    }
-    if (is_singular(BASE.'directory')) {
-        $type = get_post_meta(get_the_ID(), BASE.'for_type_slug', true);
+	$obj = get_queried_object();
+	$directories = jvbDirectories();
 
-        return jvbDirectories()[$type]??[];
-    }
-    $obj = get_queried_object();
+	if (is_tax()) {
+		$tax = jvbNoBase($obj->taxonomy);
+		return array_key_exists($tax, $directories) ? $directories[$tax] : [];
+	}
 
-    $directories = jvbDirectories();
-    if (is_tax()) {
-        $tax = jvbNoBase($obj->taxonomy);
-        return (array_key_exists($tax, $directories)) ? $directories[$tax] : [];
-    }
-
-    $type = jvbNoBase($obj->post_type);
-    return (array_key_exists($type, $directories)) ? $directories[$type] : [];
+	$type = jvbNoBase($obj->post_type);
+	return array_key_exists($type, $directories) ? $directories[$type] : [];
 }
diff --git a/inc/helpers/email.php b/inc/helpers/email.php
deleted file mode 100644
index 1ae7f29..0000000
--- a/inc/helpers/email.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-if (!defined('ABSPATH')) {
-	exit;
-}
-
-function jvbMail(string $to, string $subject, string $message, string $header = ''):bool
-{
-	$mailer = new JVBase\managers\EmailManager();
-	return $mailer->sendEmail($to, $subject, $message, $header);
-}
-
-function jvbSignature():string
-{
-	return '<p>&emsp; —  ♡ the edmonton.ink crew</p>';
-}
-function jvbMailButton(string $link, string $title):string
-{
-	return sprintf(
-		'<p style="text-align: center;"><a href="%s" class="button">%s</a></p>',
-		$link,
-		$title
-	);
-}
-function jvbEmailLink(string $link):string
-{
-	return sprintf(
-		'<p style="user-select:all;">%s</p>',
-		$link
-	);
-}
diff --git a/inc/helpers/members.php b/inc/helpers/members.php
index 291ecb5..c24ff84 100644
--- a/inc/helpers/members.php
+++ b/inc/helpers/members.php
@@ -212,7 +212,6 @@
 		return 'admin';
 	}
     $user = ($ID === 0) ? wp_get_current_user() : get_userdata($ID);
-	error_log('Current User: '.print_r($user, true));
     return array_values(array_intersect(
         array_keys(array_merge(JVB_USER, ['administrator'])),
         array_map(function ($role) {
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 3802bda..859d67e 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -344,7 +344,7 @@
 				</div>
 				<div class="summary">
 					<div class="result">
-						<h4></h4>
+						<h3></h3>
 						<p></p>
 					</div>
 				</div>
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index 0039a73..bef256d 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -17,7 +17,7 @@
     }
 
     ?>
-    <aside id="queue" class="left col start btw" aria-expanded="false" hidden>
+    <aside id="queue" class="left col start btw main" aria-expanded="false" hidden>
         <div class="status-actions row start nowrap">
 			<div class="refresh row btw">
                 <span class="countdown row" title="Will refresh again...">5</span>
@@ -54,9 +54,9 @@
 				?>
 			</nav>
 		</div>
-		<div class="qitems col a-start">
+		<div class="qitems col a-start nowrap">
 		</div>
-		<div class="queue-actions row btw">
+		<div class="queue-actions row btw nowrap">
 			<button class="dismiss-all">Clear Completed</button>
 			<button class="retry-all">Retry Failed</button>
 		</div>
@@ -386,7 +386,7 @@
 		}
 		$content .= '>
 			<h2>'.$config['title'].'</h2>';
-			if ( $config['description']) {
+			if ( array_key_exists('description', $config)) {
 				if (!is_array($config['description'])) {
 					$content .= apply_filters('the_content', $config['description']);
 				} else {
diff --git a/inc/importers/JaneAppClientImporter.php b/inc/importers/JaneAppClientImporter.php
index 6290c19..ea26f6d 100644
--- a/inc/importers/JaneAppClientImporter.php
+++ b/inc/importers/JaneAppClientImporter.php
@@ -309,7 +309,7 @@
 		$last_name = sanitize_text_field($data['Last Name'] ?? '');
 
 		// Generate username from email
-		$username = sanitize_user(substr($email, 0, strpos($email, '@')));
+		$username = sanitize_user($email);
 
 		// Ensure unique username
 		$base_username = $username;
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index eabff67..b820041 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -242,7 +242,7 @@
 		<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden>
 			<?= jvbIcon('shopping-cart')?><span class="abs"></span><span class="abs count"></span>
 		</button>
-		<aside id="cart">
+		<aside id="cart" class="main">
 			<form id="checkout" data-form-id="checkout" data-save="checkout">
 				<?php
 				$tabs = [
@@ -881,11 +881,10 @@
 		// Send notification
 		$user = get_user_by('ID', $user_id);
 		if ($user) {
-			wp_mail(
+			JVB()->email()->sendEmail(
 				$user->user_email,
 				'Security: Password Reset Required',
 				'For your security, please reset your password to continue accessing your account and saved payment methods.',
-				['Content-Type: text/html; charset=UTF-8']
 			);
 		}
 	}
@@ -1153,7 +1152,7 @@
 			$site_name
 		);
 
-		jvbMail(
+		JVB()->email()->sendEmail(
 			$user->user_email,
 			sprintf('[%s] Welcome! Set Your Password', $site_name),
 			$message
diff --git a/inc/integrations/PostMark.php b/inc/integrations/PostMark.php
index 5bb1744..f9e154c 100644
--- a/inc/integrations/PostMark.php
+++ b/inc/integrations/PostMark.php
@@ -21,6 +21,7 @@
 	protected string $from_name;
 	protected bool $track_open;
 	protected bool $track_links;
+	protected ?string $lastMessageId = null;
 	/**
 	 * Constructor
 	 */
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index 62bbeb4..60e4d80 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -863,7 +863,7 @@
 			return $actions;
 		}
 		$meta = new MetaForm();
-		$form = '<aside id="cart" class="right">
+		$form = '<aside id="cart" class="right main">
 			<form id="checkout" data-form-id="checkout" data-save="checkout">';
 
 				$tabs = [
@@ -1862,7 +1862,7 @@
 			$site_name
 		);
 
-		jvbMail(
+		JVB()->email()->sendEmail(
 			$user->user_email,
 			sprintf('[%s] Welcome! Set Your Password', $site_name),
 			$message
@@ -1906,11 +1906,10 @@
 		// Send notification
 		$user = get_user_by('ID', $user_id);
 		if ($user) {
-			wp_mail(
+			JVB()->email()->sendEmail(
 				$user->user_email,
 				'['.get_bloginfo('name').'] Security Code',
 				'For your security, enter this code to continue accessing your account and saved payment methods.',
-				['Content-Type: text/html; charset=UTF-8']
 			);
 		}
 	}
diff --git a/inc/managers/AdminPages.php b/inc/managers/AdminPages.php
index 0ae75d4..a2147a0 100644
--- a/inc/managers/AdminPages.php
+++ b/inc/managers/AdminPages.php
@@ -134,14 +134,15 @@
 	public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
 	{
 		$action = sanitize_text_field($request->get_param('action'));
-		$icons = \JVBase\managers\IconsManager::getInstance();
+		$source = sanitize_text_field($request->get_param('source') ?? 'icons'); // Add source param
+		$icons = \JVBase\managers\IconsManager::for($source);
 
 		switch ($action) {
 			case 'refresh-icons':
 				$icons->forceRefresh();
 				return new \WP_REST_Response([
 					'success' => true,
-					'message' => 'Icon CSS regenerated successfully'
+					'message' => "Icon CSS regenerated successfully for '{$source}'"
 				]);
 
 			case 'restore-icon-version':
@@ -582,9 +583,9 @@
             if (current_user_can($action['capability'])) {
                 ?>
                 <a data-action="<?=$action['slug']?>" class="jvb-action">
-                    <?= jvbIcon($action['icon']); ?>
+                    <?= jvbDashIcon($action['icon']); ?>
                     <span class="jvb-link-title"><?= esc_html($action['label'])?></span>
-                    <span class="loader"><?=jvbIcon('arrows-clockwise')?><?=jvbIcon('check')?></span>
+                    <span class="loader"><?=jvbDashIcon('arrows-clockwise')?><?=jvbDashIcon('check')?></span>
                 </a>
                 <?php
             }
@@ -639,7 +640,7 @@
      */
     protected function getIcon(string $icon = 'logo', bool $css = false): string
     {
-        $svg = jvbIcon($icon, ['wrap' => false]);
+        $svg = jvbDashIcon($icon, ['wrap' => false]);
         if ($css) {
             // For CSS, replace currentColor with brand color
             $svg = str_replace('currentColor', '#FF0080', $svg);
@@ -681,7 +682,7 @@
 
 			<div class="jvb-cache-actions">
 				<button type="button" class="button button-primary" data-action="flush-all">
-					<?= jvbIcon('arrows-clockwise'); ?>
+					<?= jvbDashIcon('arrows-clockwise'); ?>
 					Flush All Caches
 				</button>
 			</div>
@@ -706,7 +707,7 @@
 								<td><?= $this->formatConnections($configs); ?></td>
 								<td>
 									<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
-										<?= jvbIcon('trash'); ?> Flush
+										<?= jvbDashIcon('trash'); ?> Flush
 									</button>
 								</td>
 							</tr>
@@ -733,7 +734,7 @@
 							<td><?= $this->formatConnections($configs); ?></td>
 							<td>
 								<button type="button" class="button" data-action="flush-cache" data-group="<?= esc_attr($group); ?>">
-									<?= jvbIcon('trash'); ?> Flush
+									<?= jvbDashIcon('trash'); ?> Flush
 								</button>
 							</td>
 						</tr>
@@ -928,7 +929,14 @@
 
 	public function renderIconsPage():void
 	{
-		$icons = \JVBase\managers\IconsManager::getInstance();
+		// Get current source from query param or default to 'icons'
+		$current_source = $_GET['icon_source'] ?? 'icons';
+		$current_source = sanitize_text_field($current_source);
+
+		// Get all registered icon sources
+		$all_sources = ['icons', 'forms', 'dash']; // You could get this dynamically if needed
+
+		$icons = \JVBase\managers\IconsManager::for($current_source);
 		$versions = $icons->getVersionHistory();
 		$nonce = wp_create_nonce('wp_rest');
 
@@ -936,18 +944,30 @@
 		<div class="wrap jvb-admin-wrap">
 			<h1>Icon Management</h1>
 
+			<!-- Source Selector -->
+			<div class="jvb-icon-source-selector">
+				<label for="icon-source-select">Icon Source:</label>
+				<select id="icon-source-select" onchange="window.location.href='<?= admin_url('admin.php?page=' . BASE . 'icons&icon_source='); ?>' + this.value">
+					<?php foreach ($all_sources as $source): ?>
+						<option value="<?= esc_attr($source); ?>" <?= selected($current_source, $source, false); ?>>
+							<?= esc_html(ucfirst($source)); ?>
+						</option>
+					<?php endforeach; ?>
+				</select>
+			</div>
+
 			<div class="jvb-icon-actions">
-				<button type="button" class="button button-primary" data-action="refresh-icons">
-					<?= jvbIcon('arrows-clockwise'); ?>
+				<button type="button" class="button button-primary" data-action="refresh-icons" data-source="<?= esc_attr($current_source); ?>">
+					<?= jvbDashIcon('arrows-clockwise'); ?>
 					Force Refresh CSS
 				</button>
-				<button type="button" class="button" data-action="merge-icon-versions" id="merge-versions-btn" disabled>
-					<?= jvbIcon('git-merge'); ?>
+				<button type="button" class="button" data-action="merge-icon-versions" data-source="<?= esc_attr($current_source); ?>" id="merge-versions-btn" disabled>
+					<?= jvbDashIcon('git-merge'); ?>
 					Merge Selected Versions
 				</button>
 			</div>
 
-			<h2>Version History</h2>
+			<h2>Version History for <?= esc_html(ucfirst($current_source)); ?></h2>
 			<table class="wp-list-table widefat fixed striped">
 				<thead>
 				<tr>
@@ -977,18 +997,18 @@
 							<td>
 								<?= esc_html($version['icon_count']); ?> icons
 								<button type="button"
-										class="button-link"
-										data-action="view-icon-list"
+										class="button-link view-icon-list-btn"
 										data-timestamp="<?= esc_attr($version['timestamp']); ?>">
 									(view)
 								</button>
 							</td>
 							<td><?= esc_html($version['size_formatted']); ?></td>
 							<td>
-								<button type="button" class="button"
+								<button type="button" class="button restore-version-btn"
 										data-action="restore-icon-version"
+										data-source="<?= esc_attr($current_source); ?>"
 										data-timestamp="<?= esc_attr($version['timestamp']); ?>">
-									<?= jvbIcon('arrow-counter-clockwise'); ?> Restore
+									<?= jvbDashIcon('arrow-counter-clockwise'); ?> Restore
 								</button>
 							</td>
 						</tr>
@@ -1012,10 +1032,11 @@
 			(function() {
 				const apiUrl = '<?= esc_js(rest_url('jvb/v1/admin-icons')); ?>';
 				const nonce = '<?= esc_js($nonce); ?>';
+				const currentSource = '<?= esc_js($current_source); ?>';
 
 				// Helper function for API calls
 				function callIconAction(action, data = {}) {
-					const body = { action, ...data };
+					const body = { action, source: currentSource, ...data };
 
 					return fetch(apiUrl, {
 						method: 'POST',
@@ -1072,34 +1093,28 @@
 				});
 
 				// Force refresh button
-				const refreshBtn = document.getElementById('refresh-icons-btn');
-				if (refreshBtn) {
-					refreshBtn.addEventListener('click', function() {
-						if (confirm('Force regenerate icon CSS? This will reload the page.')) {
-							this.disabled = true;
-							callIconAction('refresh-icons');
-						}
-					});
-				}
+				document.querySelector('[data-action="refresh-icons"]')?.addEventListener('click', function() {
+					if (confirm('Force regenerate icon CSS? This will reload the page.')) {
+						this.disabled = true;
+						callIconAction('refresh-icons');
+					}
+				});
 
 				// Merge versions button
-				const mergeBtn = document.getElementById('merge-versions-btn');
-				if (mergeBtn) {
-					mergeBtn.addEventListener('click', function() {
-						const checkboxes = document.querySelectorAll('.version-checkbox:checked');
-						const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
+				document.getElementById('merge-versions-btn')?.addEventListener('click', function() {
+					const checkboxes = document.querySelectorAll('.version-checkbox:checked');
+					const timestamps = Array.from(checkboxes).map(cb => parseInt(cb.value));
 
-						if (timestamps.length < 2) {
-							alert('Please select at least 2 versions to merge');
-							return;
-						}
+					if (timestamps.length < 2) {
+						alert('Please select at least 2 versions to merge');
+						return;
+					}
 
-						if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
-							this.disabled = true;
-							callIconAction('merge-icon-versions', { timestamps: timestamps });
-						}
-					});
-				}
+					if (confirm(`Merge ${timestamps.length} versions? This will create a new CSS file with all unique icons.`)) {
+						this.disabled = true;
+						callIconAction('merge-icon-versions', { timestamps: timestamps });
+					}
+				});
 
 				// Restore version buttons
 				document.querySelectorAll('.restore-version-btn').forEach(btn => {
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index e05a0e5..49ec9c6 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -1,517 +1,241 @@
 <?php
 namespace JVBase\managers;
 
-use JVBase\managers\UserTermsManager;
-use JVBase\meta\MetaForm;
-use JVBase\meta\MetaManager;
+use JVBase\ui\CRUDSkeleton;
 use JVBase\utility\Features;
-use WP_User;
 
 if (!defined('ABSPATH')) {
-	exit; // Exit if accessed directly
+	exit;
 }
 
+/**
+ * WordPress CRUD Manager
+ * Configures CRUDSkeleton for WordPress post types
+ */
 class CRUD {
-	protected WP_User $user;
-	protected int $user_id;
+	protected CRUDSkeleton $skeleton;
+	protected CacheManager $cache;
 	protected array $config;
 	protected string $content;
-	protected string $singular;
-	protected string $plural;
-	protected array $filters;
-	protected array $bulkActions;
-	protected MetaManager $meta;
-	protected MetaForm $form;
-	protected array $taxonomies;
-	protected array $statuses;
-	protected array $fields;
-	protected array $sections;
-	protected array $stuck;
+	protected array $taxonomies = [];
+	protected int $user_id;
+	protected ?string $type = null;
+	protected ?array $constant = null;
 
-
-	//For Timeline-specific posts
-	protected bool $isTimeline = false;
-	protected array $nonTimelineFields = [];
-	protected array $timelineSharedFields = [];
-	protected array $timelineUniqueFields = [];
-
-	protected bool $userCanPublish = false;
-
-	public function __construct(string $content)
-	{
-		//If we haven't defined this content, bail early
-		if (!array_key_exists($content, JVB_CONTENT)) {
+	public function __construct(string $content) {
+		if (array_key_exists($content, JVB_CONTENT)) {
+			$this->type = 'post';
+			$this->constant = JVB_CONTENT;
+		} elseif (array_key_exists($content, JVB_TAXONOMY)) {
+			$this->type = 'term';
+			$this->constant = JVB_TAXONOMY;
+		} elseif (array_key_exists($content, JVB_USER)) {
+			$this->type = 'user';
+			$this->constant = JVB_USER;
+		} else {
 			return;
 		}
-		$this->user = wp_get_current_user();
-		$this->user_id = $this->user->ID;
-		$this->config = JVB_CONTENT[$content];
-		$this->singular = $this->config['singular'];
-		$this->plural = $this->config['plural'];
+
+		$this->user_id = get_current_user_id();
+		$this->config = $this->constant[$content];
 		$this->content = $content;
-		$this->fields = jvbGetFields($this->content, 'post');
-		$this->maybeSetupTimeline();
-		$this->sections = jvbGetSections($this->content, 'post');
-		$this->stuck = [
-			'post_title',
-			'term_name'
-		];
+		$this->cache = CacheManager::for($content);
+		// Create and configure skeleton
+		$this->skeleton = new CRUDSkeleton();
+		$this->configure();
+	}
 
-		$this->init();
+	/**
+	 * Configure CRUDSkeleton from WordPress config
+	 */
+	protected function configure(): void {
+		// Basic info
+		$this->skeleton
+			->content($this->content, $this->config['singular'], $this->config['plural'])
+			->title(
+				'Your ' . $this->config['plural'],
+				$this->config['page_description'] ?? ''
+			);
 
-		if ($this->isTimeline) {
-			$this->stuck[] = 'post_thumbnail';
+		// Initialize meta
+		$this->skeleton->initMeta($this->type, $this->content);
+
+
+
+		// Timeline if applicable
+		if (Features::forContent($this->content)->has('is_timeline')) {
+			$this->skeleton->setTimeline();
 		}
+
+		// Fields and sections
+		$this->skeleton->setFields($this->config['fields']);
+
+		$sections = array_key_exists('sections', $this->config) ? $this->config['sections'] : [];
+		foreach ($sections as $id => $config) {
+			$this->skeleton->addSection($id, $config);
+		}
+
+		// Taxonomies
+		$this->initTaxonomies();
+
+		// Statuses
+		if (Features::forContent($this->content)->has('is_calendar')) {
+			$this->skeleton->setCalendar();
+		}else {
+			$this->skeleton->setDefaultStatus();
+		}
+
+		// Views
+		$this->skeleton
+			->addViews(['grid', 'list', 'table'])
+			->defaultView('grid');
+
+		// Filters
+		$this->skeleton->addDateFilter();
+		$this->skeleton->addCustomDateRange($this->addDateRanges());
+		if (!empty($this->taxonomies)) {
+			$this->skeleton->addTaxonomyFilter(array_keys($this->taxonomies), 'user');
+		}
+
+		// Capabilities
+		$this->skeleton->addCapabilities(['view', 'edit', 'create', 'delete']);
+
+		$plural = strtolower($this->config['plural'] ?? $this->content . 's');
+		$canPublish = jvbUserIsVerified() && user_can($this->user_id, "publish_{$plural}");
+		$this->skeleton->userCanPublish($canPublish);
+
+		// Bulk actions
+		$this->skeleton->addBulkActions(['edit', 'publish', 'draft', 'trash']);
+
+		// Uploader
+		$this->setupUploader();
+
+		// Sticky fields
+		$stuck = ['post_title', 'term_name'];
+		if ($this->skeleton->get('isTimeline')) {
+			$stuck[] = 'post_thumbnail';
+		}
+		$this->skeleton->stickFields($stuck);
+
+		// Hook for create button
 		add_filter('jvbAdditionalActions', [$this, 'createItem']);
 	}
 
-	protected function init():void
-	{
-		$this->initStatuses();
-		$this->initBulkActions();
-		$this->initTaxonomies();
-		$this->initFilters();
-		$this->meta = new MetaManager(null, 'post', $this->content);
-		$this->form = new MetaForm();
-		$plural = strtolower($this->config['plural']??$this->content.'s');
-		$this->userCanPublish = (jvbUserIsVerified()) ?
-					user_can($this->user_id, "publish_{$plural}") : false;
+	/**
+	 * Setup uploader configuration
+	 */
+	protected function setupUploader(): void {
+		$isSingleImage = jvbCheck('single_image', $this->config);
 
-	}
+		$config = [
+			'type' => 'upload',
+			'subtype' => 'image',
+			'mode' => $isSingleImage ? 'direct' : 'selection',
+			'create_new' => true,
+			'label' => $this->config['upload_title'] ?? 'Bulk Upload ' . $this->config['plural'],
+			'content' => $this->content,
+			'singular' => $this->config['singular'],
+			'plural' => $this->config['plural'],
+			'multiple' => true,
+			'destination' => $isSingleImage ? 'post' : 'post_group'
+		];
 
-	protected function maybeSetupTimeline():void {
-		$this->isTimeline = Features::forContent($this->content)->has('is_timeline');
-
-		if (!$this->isTimeline) {
-			return;
+		if (!$isSingleImage) {
+			$config['upload_text'] = '<p>Drag images into groups. Each group becomes its own ' . $this->config['singular'] . '.</p>
+				<p>You can also select multiple images and click the "Add to Group" button.</p>
+				<p>If a ' . $this->config['singular'] . ' has multiple images, you can select the ' . jvbDashIcon('star') . ' to set an image as the main one.</p>
+				<p>Images left ungrouped will become individual ' . $this->config['plural'] . '</p>
+				<p>Once finished, click the \'Save Changes\' button to send to server for processing.</p>';
+		} else {
+			$config['description'] = 'Each image will become its own ' . $this->config['singular'] . '.';
 		}
-		$this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
-			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
-				return true;
-			}
-			return false;
-		}));
-		array_unshift($this->timelineSharedFields, 'post_thumbnail');
-		array_unshift($this->timelineSharedFields, 'post_title');
-		array_unshift($this->timelineSharedFields, 'post_status');
 
-		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
-			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
-				return true;
-			}
-			return false;
-		}));
-
-		$all = array_merge($this->timelineUniqueFields, $this->timelineSharedFields);
-		$this->nonTimelineFields = array_filter($this->fields, function ($field) use ($all) {
-			return !in_array($field, $all);
-		}, ARRAY_FILTER_USE_KEY);
+		$this->skeleton->addUploader($config);
 	}
 
-	protected function initTaxonomies():void
-	{
+	/**
+	 * Initialize taxonomies from WordPress config
+	 */
+	protected function initTaxonomies(): void {
 		$this->taxonomies = array_filter(JVB_TAXONOMY, function ($config) {
 			return in_array($this->content, $config['for_content']);
 		});
 	}
 
-	protected function initStatuses():void
-	{
-		$this->statuses = (array_key_exists('is_calendar', $this->config)) ?
+	/**
+	 * Get statuses - calendar or standard
+	 */
+	protected function getStatuses(): array {
+		return array_key_exists('is_calendar', $this->config) ?
 			[
-				'all'	=> [
-					'icon'  => 'calendar',
+				'all' => [
+					'icon' => 'calendar',
 					'label' => 'Everything',
 				],
-				'future'=> [
-					'label'	=> 'Upcoming',
-					'icon'	=> 'clock-clockwise',
+				'future' => [
+					'label' => 'Upcoming',
+					'icon' => 'clock-clockwise',
 				],
-				'past'	=> [
-					'label'	=> 'Past',
-					'icon'	=> 'clock-counter-clockwise',
+				'past' => [
+					'label' => 'Past',
+					'icon' => 'clock-counter-clockwise',
 				],
-				'repeat'=> [
-					'label'	=> 'Recurring',
-					'icon'	=> 'repeat',
+				'repeat' => [
+					'label' => 'Recurring',
+					'icon' => 'repeat',
 				],
-				'draft'	=> [
-					'icon'	=> 'eye-closed',
-					'label'	=> 'Hidden',
+				'draft' => [
+					'icon' => 'eye-closed',
+					'label' => 'Hidden',
 				],
-				'trash'	=> [
-					'label'	=> 'Scrapped',
-					'icon'	=> 'trash',
+				'trash' => [
+					'label' => 'Scrapped',
+					'icon' => 'trash',
 				],
 			] :
 			[
-				'all'	=> [
-					'icon'  => 'infinity',
+				'all' => [
+					'icon' => 'infinity',
 					'label' => 'Everything',
 				],
-				'publish'=> [
-					'icon'	=> 'eye',
-					'label'	=> 'Live',
+				'publish' => [
+					'icon' => 'eye',
+					'label' => 'Live',
 				],
-				'draft'	=> [
-					'icon'	=> 'eye-closed',
-					'label'	=> 'Hidden',
+				'draft' => [
+					'icon' => 'eye-closed',
+					'label' => 'Hidden',
 				],
-				'trash'	=> [
-					'label'	=> 'Scrapped',
-					'icon'	=> 'trash',
+				'trash' => [
+					'label' => 'Scrapped',
+					'icon' => 'trash',
 				],
 			];
 	}
 
-	protected function initBulkActions():void
-	{
-		$this->bulkActions = [
-			'edit'	=> 'Edit',
-			'publish'	=> 'Show',
-			'draft'	=> 'Hide',
-//			'copy'	=> 'Duplicate',
-			'trash'	=> 'Scrap'
-		];
-	}
-
-	protected function initFilters():void
-	{
-		$this->filters = [
-			'status'	=> $this->statuses,
-			'date'		=> [
-				'label'	=> 'Date',
-				'icon'	=> 'calendar'
-			]
-		];
-
-		foreach ($this->taxonomies as $taxonomy=> $config) {
-			$this->filters['taxonomy'][$taxonomy] = [
-				'label'	=> $config['singular'],
-				'icon'	=> $config['icon']??'folder'
-			];
-		}
-	}
-
-	public function render():void
-	{
-		ob_start();
-		?>
-		<div class="dashboard-page <?= esc_attr($this->content) ?>"<?=($this->isTimeline) ? ' data-timeline' : ''?>>
-			<?php
-			$this->renderHeader();
-			$this->renderContent();
-			$this->renderModals();
-			$this->renderTemplates();
-			?>
-		</div>
-		<?php
-		echo ob_get_clean();
-
-	}
-
-	protected function renderHeader():void
-	{
-		?>
-		<h1>Your <?= $this->config['plural'] ?></h1>
-		<?php
-		if (array_key_exists('page_description', $this->config)) {
-			?>
-			<p class="page-description"><?=$this->config['page_description']?></p>
-			<?php
-		}
-		$this->renderHeaderActions();
-	}
-
-	protected function renderHeaderActions():void
-	{
-		$uploadConfig = [
-			'type'			=> 'upload',
-			'subtype'		=> 'image',
-			'mode'			=> (jvbCheck('single_image', $this->config)) ? 'direct' : 'selection',
-			'create_new'	=> true,
-			'label'			=> (array_key_exists('image_title', $this->config)) ? $this->config['image_title'] : 'Upload More '.$this->config['plural'],
-			'content'		=> $this->content,
-			'singular'		=> $this->singular,
-			'plural'		=> $this->plural,
-			'multiple'		=> true,
-			'destination'	=> 'post'
-		];
-		if (!array_key_exists('single_image', $this->config) || $this->config['single_image'] === false) {
-			$uploadConfig['destination'] = 'post_group';
-		}
-		$uploadConfig['destination'] = 'post_group';
-		if (!jvbCheck('single_image', $this->config)) {
-			$uploadConfig['label'] = 'Create '.$this->config['plural'];
-			$uploadConfig['upload_text'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p>
-						<p>You can also select multiple images and click the "Add to Group" button.</p>
-						<p>If a '.$this->singular.' has multiple images, you can select the '.jvbIcon('star').' to set an image as the main one.</p>
-						<p>Images left ungrouped will become individual '.$this->plural.'</p>
-						<p>Once finished, click the \'Save Changes\' button to send to server for processing.</p>';
-		} else {
-			$uploadConfig['description'] = 'Each image will become its own '.$this->singular.'.';
-		}
-		?>
-		<details open class="uploader">
-			<summary class="row btw"><?= $this->config['upload_title'] ?? 'Bulk Upload '.$this->plural?></summary>
-			<?php
-			$this->meta->render(
-				'form',
-				'new_'.$this->content,
-				$uploadConfig
-			);
-			?>
-		</details>
-		<?php
-	}
-
-	protected function renderContent():void
-	{
-		?>
-		<section class="items-list <?=$this->content?> crud" data-content="<?= $this->content ?>">
-			<?php
-			$this->renderFilters();
-			$this->renderBulkControls();
-			?>
-			<div class="<?= $this->content ?> item-grid" role="grid"></div>
-			<div class="scroll-sentinel" aria-hidden="true"></div>
-		</section>
-		<?php
-		$state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->content);
-
-		echo '<template class="emptyState">'.$state.'</template>';
-		?>
-		<?php
-	}
-
-	protected function renderEmptyState():string
-	{
-		ob_start();
-		?>
-		<div class="empty-state">
-			<h3><?=jvbIcon($this->config['icon'])?>Nothing here<?=jvbIcon($this->config['icon'])?></h3>
-			<p>It doesn't look like you have any <?=$this->config['plural'] ?> yet.</p>
-			<p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('plus-square')?>" button to add one at a time.</small></p>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderFilters():void
-	{
-		?>
-		<div class="all-filters col start" data-ignore>
-			<div class="search row start nowrap">
-				<span class="label">Search:</span>
-				<?= jvbSearch() ?>
-			</div>
-			<div class="controls col start">
-				<?php
-				$this->renderViewFilters();
-				$this->renderStatusFilters();
-				$this->renderOrderFilters();
-				?>
-			</div>
-			<div class="filters row start">
-				<span class="label">Filters:</span>
-				<?php
-					$this->renderTaxonomyFilters();
-					$this->renderDateFilters();
-				?>
-				<button type="button" class="clear-filters row" hidden>
-					<?= jvbIcon('x', ['title'    => 'Clear']); ?>
-					Clear All Filters
-				</button>
-			</div>
-
-			<?= $this->renderColumnSelector(); ?>
-		</div>
-		<?php
-	}
-
-	protected function renderOrderFilters():void
-	{
-		?>
-		<div class="radio-options order row btw w-full">
-			<?php
-				$order = [
-					'orderby' => [
-						'date' => 'Order by date created',
-						'alphabetical' => 'Order alphabetically'
-					],
-					'order' => [
-						'sort-ascending' => 'In ascending order (Z-A, oldest to newest)',
-						'sort-descending' => 'In descending order (A-Z, newest to oldest)'
-					]
-				];
-
-				foreach ($order as $o => $option) {
-					?>
-					<div class="row start">
-						<span class="label"><?= ucfirst($o)?>:</span>
-					<?php
-					$i = 0;
-					foreach ($option as $opt => $label) {
-						$icon = $opt === 'date' ? 'calendar' : $opt;
-						?>
-						<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
-
-						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($icon)?></label>
-						<?php
-						$i++;
-					}
-					?>
-					</div>
-					<?php
-				}
-			?>
-		</div>
-		<?php
-	}
-	protected function renderStatusFilters():void
-	{
-		if (empty($this->statuses)) {
-			return;
-		}
-		?>
-		<div class="radio-options status row">
-			<span class="label">Status:</span>
-			<?php
-			$i = 1;
-			foreach ($this->statuses as $status => $config) {
-				$checked = ($i == 1) ? ' checked' : '';
-				?>
-				<input type="radio" class="btn" data-filter="status" value="<?=$status?>" name="status" id="<?=$status?>"<?=$checked?>>
-				<label for="<?=$status?>">
-					<?= jvbIcon($config['icon']) ?>
-					<span><?=$config['label']?><span class="count"></span></span>
-				</label>
-				<?php
-				$i++;
-			}
-			?>
-		</div>
-		<?php
-	}
-
-	protected function renderViewFilters():void
-	{
-		?>
-		<div class="radio-options view row">
-			<span class="label">View:</span>
-
-			<?php
-			$views = [
-				'grid' => ['icon' => 'squares-four', 'label' => 'Grid View'],
-				'list' => ['icon' => 'rows', 'label' => 'List View'],
-				'table' => ['icon' => 'table', 'label' => 'Table View']
-			];
-
-			$first = true;
-			foreach ($views as $view => $config):
-				?>
-				<input type="radio"
-					   data-view="<?= esc_attr($view) ?>"
-					   value="<?= esc_attr($view) ?>"
-					   class="btn"
-					   name="view"
-					   id="view-<?= esc_attr($view) ?>"
-					<?= $first ? 'checked' : '' ?>>
-				<label for="view-<?= esc_attr($view) ?>"
-					   title="<?= esc_attr($config['label']) ?>">
-					<?= jvbIcon($config['icon']) ?>
-					<span class="screen-reader-text"><?= esc_html($config['label']) ?></span>
-				</label>
-				<?php
-				$first = false;
-			endforeach;
-			?>
-		</div>
-		<?php
-	}
 	/**
-	 * Render column selector for table view
+	 * Add create button to dashboard actions
 	 */
-	protected function renderColumnSelector(): string {
-		ob_start();
-		?>
-		<details class="multi-select" title="Select columns" hidden>
-			<summary class="row start nowrap">
-				<?= jvbIcon('columns') ?>
-				<span class="labels">Toggle Columns</span>
-			</summary>
-			<div class="column-list">
-				<?php foreach ($this->fields as $fieldName => $config):
-					if (array_key_exists('hidden', $config)){
-						continue;
-					}
-					?>
-					<input type="checkbox"
-						   id="show-<?= esc_attr($fieldName) ?>"
-						   class="column-toggle ch"
-						   name="show-<?= esc_attr($fieldName) ?>"
-						   checked>
-					<label for="show-<?= esc_attr($fieldName) ?>">
-						<?= esc_html($config['label']) ?>
-					</label>
-				<?php endforeach; ?>
-			</div>
-		</details>
-		<?php
-		return ob_get_clean();
+	public function createItem(array $actions): array {
+		$actions[] = [
+			'button' => '<button type="button" class="create-item row" title="Create New ' . $this->config['singular'] . '">'
+				. jvbDashIcon('plus-square')
+				. '<span class="screen-reader-text">Create New ' . $this->config['singular'] . '</span></button>',
+			'content' => '', // Modal is rendered by skeleton
+		];
+
+		return $actions;
 	}
-	protected function renderTaxonomyFilters():void
+
+	protected function addDateRanges():array
 	{
-		if (empty($this->taxonomies)) {
-			return;
-		}
-		$out = '';
-		foreach ($this->taxonomies as $taxonomy => $config) {
-			$terms = $this->getCommonTerms($taxonomy);
-			if (!empty($terms)) {
-				$out .= sprintf(
-					'<div class="row nowrap"><label for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label>
-                <select id="filter-%s" class="filter %s" name="%s" data-filter="taxonomies" data-taxonomy="%s">
-                <option value="">by %s</option>',
-					$taxonomy,
-					jvbIcon($config['icon'], ['title'    => $config['plural']]),
-					esc_html($config['plural']),
-					$taxonomy,
-					$taxonomy,
-					$taxonomy,
-					$taxonomy,
-					$config['plural']
-				);
-
-
-				foreach ($terms as $term) {
-					$out .= sprintf(
-						'<option value="%s">%s</option>',
-						esc_attr($term['term_id']),
-						esc_html($term['name'])
-					);
-				}
-				$out .= '</select></div>';
-			}
-		}
-		echo $out;
-	}
-	/**
-	 * Get common terms for taxonomy
-	 * @param string $taxonomy
-	 * @return array
-	 */
-	protected function getCommonTerms(string $taxonomy):array {
-		$manager = new UserTermsManager();
-		return $manager->getUserTerms($this->user_id, $taxonomy);
-	}
-
-	protected function renderDateFilters():void
-	{
-		$postType = jvbCheckBase($this->content);
-		// Get available months
-		global $wpdb;
-		$months = $wpdb->get_results("
+		return $this->cache->remember(
+			'dateRanges',
+			function() {
+				$postType = jvbCheckBase($this->content);
+				// Get available months
+				global $wpdb;
+				$months = $wpdb->get_results("
 			SELECT DISTINCT
 				YEAR(post_date) as year,
 				MONTH(post_date) as month
@@ -520,757 +244,28 @@
 			AND post_author = '{$this->user_id}'
 			ORDER BY post_date DESC
 		");
-
-		// Quick filters
-		$out = '<div class="row nowrap">
-        <label for="filter-date">'.
-			jvbIcon('calendar',['title'=>'Date']).
-			'<span class="screen-reader-text">by Date</span>
-        </label>
-        <select id="filter-date" class="date-filter" data-filter="date">
-            <option value="">by Date</option>
-            <option value="today">Today</option>
-            <option value="week">Past Week</option>
-            <option value="month">Past Month</option>
-            <option value="year">Past Year</option>
-            <option value="custom">Custom Range...</option>
-        </select>
-    </div>';
-
-		$form = '<div class="custom-range row">
-            <label for="date-start" class="col">
-                From
-            </label>
-			<input type="date" id="date-start" class="date-start">
-            <label for="date-end" class="col">
-               To
-            </label>
-            <input type="date" id="date-end" class="date-end">
-        </div>
-        <div class="month-picker">
-            <label>
-                <span>Or select month</span>
-                <select class="month-select">
-                    <option value="">&emsp; . . . &emsp;</option>';
-
-
-		foreach ($months as $date) {
-			$month_name = date('F Y', mktime(0, 0, 0, $date->month, 1, $date->year));
-			$value = $date->year . '-' . str_pad($date->month, 2, '0', STR_PAD_LEFT);
-			$form .= sprintf(
-				'<option value="%s">%s</option>',
-				esc_attr($value),
-				esc_html($month_name)
-			);
-		}
-
-		$form .= '</select>
-            </label>
-        </div>';
-
-		// Custom date range
-		$out .= jvbNewModal(
-			'date-range',
-			'Filter Results by Date:',
-			$form
-		);
-
-		echo $out;
-	}
-
-	protected function renderBulkControls():void
-	{
-		if (empty($this->bulkActions)) {
-			return;
-		}
-		?>
-		<div class="bulk-controls row nowrap btw">
-			<div class="bulk-select">
-				<input type="checkbox" id="select-all" class="select-all">
-				<label for="select-all" class="row"><span>Select All</span><span class="selected-count" hidden></span></label>
-			</div>
-			<div class="bulk-actions row nowrap" hidden>
-				<label for="bulk-action-select" class="screen-reader-text">
-					Select what to do with this selection.
-				</label>
-				<select class="bulk-action-select" id="bulk-action-select">
-
-				</select>
-			</div>
-		</div>
-
-		<template class="notTrashOptions">
-			<select class="wrap">
-				<option value="">Bulk Actions...</option>
-				<?php
-				foreach ($this->bulkActions as $control => $label) {
-					$disabled = ($control === 'publish' && !$this->userCanPublish) ? ' disabled' : '';
-					?>
-					<option value="<?=$control?>"<?=$disabled?>><?=$label?></option>
-					<?php
+				$ranges = [];
+				foreach ($months as $date) {
+					$month_name = date('F Y', mktime(0, 0, 0, $date->month, 1, $date->year));
+					$value = $date->year . '-' . str_pad($date->month, 2, '0', STR_PAD_LEFT);
+					$ranges[$value] = $month_name;
 				}
-				foreach ($this->taxonomies as $taxonomy => $config) {
-				?>
-					<option value="tax-<?=$taxonomy?>">Add to <?= $config['singular'] ?></option>
-				<?php
-				}
-				?>
-			</select>
-
-		</template>
-		<template class="trashOptions">
-			<select class="wrap">
-				<option value="">Bulk Actions...</option>
-				<option value="restore">Restore</option>
-				<option value="delete">Permanently Delete</option>
-			</select>
-		</template>
-		<?php
-	}
-
-	protected function renderModals():void
-	{
-//		$this->renderCreateModal();
-		$this->renderEditModal();
-		$this->renderBulkEditModal();
-	}
-	protected function renderCreateModal():void
-	{
-		echo jvbNewModal(
-			'create',
-			'Creating <span class="count"></span> New '.$this->config['singular'],
-			str_replace('edit-form"', 'create-form" data-noautosave', $this->editForm())
+				return $ranges;
+			}
 		);
 	}
 
-	protected function editForm():string
-	{
-		ob_start();
-		?>
-		<form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>" data-autosave<?= ($this->isTimeline) ? ' data-timeline' : ''?>>
-			<?= jvbFormStatus() ?>
-			<input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
-			<input type="hidden" name="content" value="<?=$this->content?>" />
-			<div class="fields">
-				<div class="field-group radio-options row">
-					<span>Status:</span>
-					<?php
-					$this->getApplicableStatuses('edit');
-					?>
-				</div>
-				<?php if (!$this->userCanPublish) { ?>
-                    <p class="description">Your account needs to be verified before you can publish content.</p>
-                <?php }
-
-				if (!empty($this->sections)) {
-					$tabs = [];
-					foreach ($this->sections as $slug => $title) {
-						$tabs[$slug] = [
-							'title'	=> $title,
-							'content' => '',
-							'description' => jvbSectionDescription($slug)??'',
-						];
-						$icon = jvbSectionIcon($slug);
-						if ($icon !== '') {
-							$tabs[$slug]['icon'] = $icon;
-						}
-					}
-				} else {
-					$tabs = false;
-				}
-
-
-				$fields = $this->fields;
-				if (!$this->isTimeline) {
-					$first = ['post_thumbnail', 'post_title', 'price'];
-
-					foreach ($first as $f) {
-						if (array_key_exists($f, $fields)) {
-							if ($tabs) {
-								$tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true);
-							} else {
-								$this->meta->render('form', $f, $fields[$f]);
-							}
-
-							unset($fields[$f]);
-						}
-					}
-				}
-
-				if ($this->isTimeline) {
-					$temp = array_filter($fields, function ($field) {
-						return in_array($field, $this->timelineUniqueFields);
-					}, ARRAY_FILTER_USE_KEY);
-
-					$config = [
-						'type'		=> 'gallery',
-						'subtype'	=> 'timeline',
-						'data'		=> 'timeline',
-						'label'		=> 'Progression',
-						'fields'	=> $temp
-					];
-					$content = '';
-					foreach ($fields as $slug=> $field) {
-						if (in_array($slug, $this->timelineSharedFields)) {
-							$content .= $this->form->render($slug, null, $field, false, true);
-						}
-					}
-
-
-					$content .= $this->meta->render('form', 'timeline', $config, false,true);
-
-					$tabs['progression']['content'] = $content;
-					$fields = $this->nonTimelineFields;
-				}
-				foreach ($fields as $n => $config) {
-					if ($tabs) {
-						$section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
-						$tabs[$section]['content'] .= $this->meta->render('form', $n, $config, false, true);
-					} else {
-						$this->meta->render('form', $n, $config);
-					}
-				}
-
-				if ($tabs) {
-					jvbRenderTabs($tabs);
-				}
-				?>
-			</div>
-		</form>
-		<?php
-		return ob_get_clean();
-	}
-
-
-	protected function renderRowFields():void
-	{
-		$fields = $this->fields;
-
-		// Render priority fields first
-		$first = ['post_thumbnail', 'post_title', 'price'];
-		foreach ($first as $f) {
-			if (array_key_exists($f, $fields)) {
-				$this->meta->render('form', $f, $fields[$f]);
-				unset($fields[$f]);
-			}
-		}
-
-		// Render remaining fields
-		foreach ($fields as $name => $config) {
-			if (!array_key_exists('hidden', $config) || !$config['hidden']) {
-				$this->meta->render('form', $name, $config);
-			}
-		}
-	}
-
-	protected function getApplicableStatuses(string $prefix) {
-		foreach ($this->statuses as $status => $config) {
-			if ($status === 'all') {
-				continue;
-			}
-			if (in_array($status, ['future', 'past'])) {
-				if ($status === 'future') {
-					$status = 'publish';
-					$config = [
-						'icon'	=> 'eye',
-						'label'	=> 'Live',
-					];
-				} else {
-					continue;
-				}
-			}
-			$disabled = ($status === 'publish' && !$this->userCanPublish) ? ' disabled' : '';
-			?>
-			<input type ="radio"
-				   name="post_status"
-				   class="btn"
-				   value="<?= esc_attr($status)?>"
-				   id="<?=$prefix?>set-<?= esc_attr($status) ?>"
-				<?= $disabled?>>
-			<label for="<?=$prefix?>set-<?=esc_attr($status)?>">
-				<?= jvbIcon($config['icon'], ['title' => $config['label']]) ?>
-				<span><?= esc_html($config['label'])?></span>
-			</label>
-			<?php
-		}
-	}
-	protected function renderEditModal():void
-	{
-		echo jvbNewModal(
-			'edit',
-			'Edit your '.$this->singular,
-			$this->editForm()
-		);
-	}
-
-	protected function renderBulkEditModal():void
-	{
-		if (empty($this->bulkActions)) return;
-		ob_start();
-		?>
-		<form class="bulk-edit-form" data-save="content" data-form-id="bulk-edit-<?=$this->content?>">
-			<?= jvbFormStatus() ?>
-			<div class="selected"></div>
-			<p class="description">You can unselect items by clicking the image here.</p>
-			<p class="hint"><strong>IMPORTANT: </strong> Whatever changes you make here will be applied to all selected <?=$this->plural?>.</p>
-			<div class="fields">
-				<div class="field-group radio-options row">
-					<?php
-					$this->getApplicableStatuses('bulk-');
-                    ?>
-				</div>
-				<?php
-				if (!empty($this->taxonomies)) {
-				?>
-				<div class="taxonomies">
-					<?php
-					foreach ($this->taxonomies as $taxonomy => $config) {
-						$this->meta->render(
-							'form',
-							'bulk-edit-'.$taxonomy,
-							[
-								'type'		=> 'taxonomy',
-								'label'		=> $config['singular'],
-								'taxonomy'	=> $taxonomy,
-								'createNew'	=> jvbUserIsVerified(),
-								'multiple'	=> true,
-								'mode'		=> 'append'
-							]
-						);
-					}
-					?>
-				</div>
-				<?php
-				}
-				$fields = $this->fields;
-				$fields = array_filter($fields, function ($field) {
-					return array_key_exists('bulkEdit', $field);
-				});
-				foreach ($fields as $fieldName => $config) {
-					$this->meta->render('form', $fieldName, $config);
-				}
-				?>
-			</div>
-		</form>
-		<template class="bulkItem">
-			<label>
-				<input type="checkbox">
-				<img>
-			</label>
-		</template>
-		<?php
-		$form = ob_get_clean();
-		echo jvbNewModal(
-			'bulkEdit',
-			'Bulk Edit <span class="selected"></span> '.$this->config['plural'],
-			$form
-		);
-	}
-
-	protected function renderTemplates():void
-	{
-		$this->renderListView();
-		$this->renderGridView();
-		$this->renderTableView();
-		$this->renderTableRow();
-		if ($this->isTimeline) {
-			$temp = array_filter($this->fields, function ($field) {
-				return in_array($field, $this->timelineUniqueFields);
-			}, ARRAY_FILTER_USE_KEY);
-			$form = new MetaForm();
-			echo '<template class="timelineItem">';
-			$form->renderImagePreview(null,['fields' => $temp]);
-			echo '</template>';
-		}
-		echo jvbGetEmptyStateTemplate();
-		echo jvbGetGalleryPreviewTemplate();
-
-
-	}
-
-	protected function renderItemSelect():string
-	{
-		ob_start();
-		?>
-		<div class="item-select">
-			<input type="checkbox" class="select-item">
-			<label class="select-item-label">
-				<span class="screen-reader-text">Select this <?= $this->singular ?></span>
-			</label>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderImage():string
-	{
-		ob_start();
-		?>
-		<img loading="lazy" alt="">
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderItemActions():string
-	{
-		ob_start();
-		?>
-		<div class="item-actions">
-			<button type="button" class="action" data-action="edit" title="Edit <?= $this->singular ?>">
-				<?=jvbIcon('pencil-simple')?>
-				<span class="screen-reader-text">Edit <?= $this->singular ?></span>
-			</button>
-			<button type="button" class="action" data-action="trash" title="Scrap <?= $this->singular ?>">
-				<?=jvbIcon('trash')?>
-				<span class="screen-reader-text">Scrap <?= $this->singular ?></span>
-			</button>
-<!--			<button type="button" class="action" data-action="toggle-status">-->
-<!--				<span class="screen-reader-text">Toggle --><?php //= $this->singular ?><!-- Visibility</span>-->
-<!--			</button>-->
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderItemFields(bool $form = false):string
-	{
-		ob_start();
-		foreach ($this->fields as $name => $config) {
-			$renderMode = $form ? 'form' : 'render';
-
-			$field = $this->meta->render($renderMode, $name, $config, false, true);
-
-			// Special handling for title in grid view
-			if ($name === 'post_title' && !$form) {
-				$field = str_replace('<p', '<h3', str_replace('</p>', '</h3>', $field));
-			}
-
-			echo $field;
-		}
-		return ob_get_clean();
-	}
-
-	protected function renderGridView():void
-	{
-		?>
-		<template class="gridView">
-			<div class="item <?= $this->content ?>">
-				<input type="checkbox" class="select-item" name="select-item">
-				<label title="Select this <?= $this->singular?>" class="select-item-label">
-					<?= $this->renderImage() ?>
-				</label>
-				<?= $this->renderItemActions(); ?>
-			</div>
-		</template>
-		<?php
-	}
-
-	protected function renderListView():void
-	{
-		?>
-		<template class="listView">
-			<div class="item <?=esc_attr($this->content)?> row nowrap">
-				<?= $this->renderItemSelect()?>
-				<?=$this->renderImage() ?>
-				<div class="col start w-full">
-					<?= $this->renderItemActions()?>
-					<h3 data-field="post_title"></h3>
-					<p data-attr="date"></p>
-					<p data-field="price"></p>
-					<div data-field="post_excerpt"></div>
-				</div>
-			</div>
-		</template>
-		<?php
-	}
-
-	protected function renderTableView():void
-	{
-		if ($this->isTimeline) {
-			$this->renderTimelineTableView();
-			return;
-		}
-		?>
-		<template class="contentTable">
-			<form class="table"
-				  data-save="content"
-				  data-content="<?= esc_attr($this->content) ?>"
-				  data-form-id="content-table-<?= esc_attr($this->content) ?>">
-				<?= jvbFormStatus() ?>
-				<?= $this->renderTableActions() ?>
-
-				<table>
-					<thead>
-					<?= $this->renderTableHeader() ?>
-					</thead>
-					<tbody>
-					<!-- Rows will be inserted here -->
-					</tbody>
-					<tfoot>
-					<?= $this->renderTableHeader() ?>
-					</tfoot>
-				</table>
-			</form>
-		</template>
-		<?php
-	}
 	/**
-	 * Render table row template
+	 * Render the interface
 	 */
-	protected function renderTableRow(): void {
-		if ($this->isTimeline) {
-			$this->renderTimelineTableGroup();
-			return;
-		}
-		?>
-		<template class="tableView">
-			<tr class="item">
-				<td class="select">
-					<?= $this->renderItemSelect() ?>
-				</td>
-				<td class="status" data-field="post_status">
-					<?= $this->renderStatusRadios() ?>
-				</td>
-				<?php
-				$makeDetails = [
-					'group',
-					'repeater',
-					'checkbox',
-					'radio'
-				];
-				foreach ($this->fields as $name => $config):
-					if (array_key_exists('hidden', $config)){
-						continue;
-					}
-					$makeThisDetailed = (in_array($config['type'], $makeDetails));
-					?>
-					<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-						<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
-						<?php $this->meta->render('form', $name, $config); ?>
-						<?= $makeThisDetailed ? '</details>' : '' ?>
-					</td>
-				<?php endforeach; ?>
-			</tr>
-		</template>
-		<?php
-	}
-
-	protected function renderTimelineTableView():void
-	{
-		?>
-		<template class="contentTable">
-			<form class="table"
-				  data-save="content"
-				  data-content="<?= esc_attr($this->content) ?>"
-				  data-form-id="content-table-<?= esc_attr($this->content) ?>">
-				<?= jvbFormStatus() ?>
-				<?= $this->renderTableActions() ?>
-
-				<table>
-					<thead>
-					<?= $this->renderTimelineTableHeader() ?>
-					</thead>
-					<!-- Rows are inserted as tbody groups -->
-					<tfoot>
-					<?= $this->renderTimelineTableHeader() ?>
-					</tfoot>
-				</table>
-			</form>
-		</template>
-		<?php
-	}
-
-	protected function renderTimelineTableGroup():void
-	{
-		$makeDetails = [
-			'group',
-			'repeater',
-			'checkbox',
-			'radio'
-		];
-		?>
-		<template class="tableView">
-			<tbody class="item">
-				<tr class="shared">
-					<td class="select">
-						<?= $this->renderItemSelect() ?>
-					</td>
-					<td class="show-post_status field" data-field="post_status">
-						<?= $this->renderStatusRadios() ?>
-					</td>
-					<?php
-					foreach ($this->fields as $name => $config) {
-						if(array_key_exists('hidden', $config) || $name === 'post_status') {
-							continue;
-						}
-						if (!in_array($name, $this->timelineSharedFields)) {
-							echo '<td></td>';
-							continue;
-						}
-							$makeThisDetailed = (in_array($config['type'], $makeDetails));
-						?>
-						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
-							<?php $this->meta->render('form', $name, $config); ?>
-							<?= $makeThisDetailed ? '</details>' : '' ?>
-						</td>
-						<?php
-					}
-
-					?>
-				</tr>
-				<tr class="timeline-point">
-					<td class="select">
-						<button class="drag-handle" title="Drag to reorder" aria-label="Drag to reorder this timeline point"><?= jvbIcon('dots-six') ?></button>
-					</td>
-					<td class="show-post_status field" data-field="post_status">
-						<?= $this->renderStatusRadios() ?>
-					</td>
-					<?php
-					foreach ($this->fields as $name => $config) {
-						if(array_key_exists('hidden', $config) || $name === 'post_status') {
-							continue;
-						}
-						if (!in_array($name, $this->timelineUniqueFields)) {
-							echo '<td></td>';
-							continue;
-						}
-						$makeThisDetailed = (in_array($config['type'], $makeDetails));
-						?>
-						<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-							<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
-							<?php $this->meta->render('form', $name, $config); ?>
-							<?= $makeThisDetailed ? '</details>' : '' ?>
-						</td>
-						<?php
-					}
-					?>
-				</tr>
-			</tbody>
-		</template>
-		<?php
-	}
-	/**
-	 * Render status radio buttons
-	 */
-	protected function renderStatusRadios(): string {
-		ob_start();
-		?>
-		<div class="radio-options status-options row">
-			<?php foreach ($this->statuses as $status => $config):
-				if ($status === 'all') continue;
-
-				// Handle special cases
-				if ($status === 'future') {
-					$status = 'publish';
-					$config = [
-						'icon' => 'eye',
-						'label' => 'Live'
-					];
-				} elseif ($status === 'past') {
-					continue;
-				}
-				?>
-				<input type="radio"
-					   name="post_status"
-					   id="status-<?= esc_attr($status) ?>"
-					   value="<?= esc_attr($status) ?>">
-				<label for="status-<?= esc_attr($status) ?>">
-					<?= jvbIcon($config['icon']) ?>
-					<span class="screen-reader-text"><?= esc_html($config['label']) ?></span>
-				</label>
-			<?php endforeach; ?>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-	/**
-	 * Render table header
-	 */
-	protected function renderTableHeader(): string {
-		ob_start();
-
-		?>
-		<tr>
-			<th scope="col" class="select-header">
-				<input type="checkbox" id="select-all" name="select-all">
-				<label for="select-all">All</label>
-			</th>
-			<th scope="col" class="status-header">Status</th>
-			<?php foreach ($this->fields as $name => $config):
-				if (array_key_exists('hidden', $config)){
-					continue;
-				}
-				?>
-				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-					<?= esc_html($config['label']) ?>
-				</th>
-			<?php endforeach; ?>
-		</tr>
-		<?php
-		return ob_get_clean();
-	}
-
-	protected function renderTimelineTableHeader(): string {
-		ob_start();
-
-		?>
-		<tr>
-			<th scope="col" class="select-header">
-				<input type="checkbox" id="select-all" name="select-all">
-				<label for="select-all">All</label>
-			</th>
-			<th scope="col" class="show-post_status">
-				Status
-			</th>
-			<?php foreach ($this->fields as $name => $config):
-				if (array_key_exists('hidden', $config) || $name === 'post_status'){
-					continue;
-				}
-				?>
-				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
-					<?= esc_html($config['label']) ?>
-				</th>
-			<?php endforeach; ?>
-		</tr>
-		<?php
-		return ob_get_clean();
+	public function render(): void {
+		$this->skeleton->render();
 	}
 
 	/**
-	 * Render table action controls
+	 * Get the skeleton instance for further customization
 	 */
-	protected function renderTableActions(): string {
-		ob_start();
-		?>
-		<div class="table-actions row btw nowrap">
-			<?= jvbRenderToggleTextField(
-				'vertical',
-				'TAB NAV:',
-				'',
-				jvbIcon('caret-double-down'),
-				jvbIcon('caret-double-right')
-			) ?>
-
-			<button type="button" class="add-row" title="Add new row">
-				<?= jvbIcon('plus-square') ?>
-				<span>Add Row</span>
-			</button>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-	public function createItem(array $actions):array
-	{
-		ob_start();
-		$this->renderCreateModal();
-		$content = ob_get_clean();
-		$create = [
-			'button'	=> '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('plus-square').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>',
-			'content'	=> $content,
-		];
-		$actions[] = $create;
-		return $actions;
+	public function getSkeleton(): CRUDSkeleton {
+		return $this->skeleton;
 	}
 }
diff --git a/inc/managers/CacheManager.php b/inc/managers/CacheManager.php
index 8612385..2583431 100644
--- a/inc/managers/CacheManager.php
+++ b/inc/managers/CacheManager.php
@@ -306,7 +306,14 @@
 		$key = $this->normalizeKey($key);
 		$cache_key = $this->buildKey($key);
 
-		return wp_cache_get($cache_key, $group);
+		$value = wp_cache_get($cache_key, $group);
+
+		// Fallback to transient if no external object cache
+		if ($value === false && !wp_using_ext_object_cache()) {
+			$value = get_transient($group . '_' . $cache_key);
+		}
+
+		return $value;
 	}
 
 	/**
@@ -324,12 +331,18 @@
 		$key = $this->normalizeKey($key);
 		$cache_key = $this->buildKey($key);
 
-		// Update timestamp when setting new data
 		self::updateTimestamp($this->group);
 
-		return wp_cache_set($cache_key, $value, $group, $ttl);
-	}
+		// Try object cache first
+		$result = wp_cache_set($cache_key, $value, $group, $ttl);
 
+		// If no external object cache, also store in transient for persistence
+		if (!wp_using_ext_object_cache()) {
+			set_transient($group . '_' . $cache_key, $value, $ttl);
+		}
+
+		return $result;
+	}
 	/**
 	 * Delete a cached value
 	 * @param string|array $key The key to look up (auto-generates key from array of key=>values)
@@ -342,9 +355,17 @@
 		$key = $this->normalizeKey($key);
 		$cache_key = $this->buildKey($key);
 
-		return wp_cache_delete($cache_key, $group);
+		$result = wp_cache_delete($cache_key, $group);
+
+		// Also delete transient if no external object cache
+		if (!wp_using_ext_object_cache()) {
+			delete_transient($group . '_' . $cache_key);
+		}
+
+		return $result;
 	}
 
+
 	/**
 	 * Clear all cache for this group
 	 * @return bool
@@ -354,16 +375,40 @@
 		try {
 			if (function_exists('wp_cache_flush_group')) {
 				wp_cache_flush_group($this->group);
-				self::updateTimestamp($this->group);
-				return true;
 			}
-			return false;
+
+			// Clear transients for this group if no external object cache
+			if (!wp_using_ext_object_cache()) {
+				$this->clearGroupTransients();
+			}
+
+			self::updateTimestamp($this->group);
+			return true;
 		} catch (\Exception $e) {
 			return false;
 		}
 	}
 
 	/**
+	 * Clear all transients for this cache group
+	 */
+	private function clearGroupTransients(): void
+	{
+		global $wpdb;
+
+		$pattern = '_transient_' . $this->group . '_' . $this->prefix . '%';
+		$timeout_pattern = '_transient_timeout_' . $this->group . '_' . $this->prefix . '%';
+
+		$wpdb->query(
+			$wpdb->prepare(
+				"DELETE FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s",
+				$pattern,
+				$timeout_pattern
+			)
+		);
+	}
+
+	/**
 	 * Helper to generateKey from array if applicable
 	 * @param string|array $key
 	 * @return string
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index 69e485e..6f1f337 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -4,6 +4,7 @@
 use JVBase\forms\TaxonomySelector;use JVBase\managers\CRUD;
 use JVBase\meta\MetaManager;
 use JVBase\utility\Features;
+use JVBase\ui\Navigation;
 use WP_User;
 
 if (!defined('ABSPATH')) {
@@ -18,6 +19,7 @@
     protected WP_User $user;
     protected CacheManager $cache;
     protected string $role;
+	protected string $baseURL;
     protected int $userLink;
 
     public function __construct()
@@ -30,15 +32,30 @@
         $this->user = wp_get_current_user();
         $this->role = jvbUserRole($this->user->ID);
         $this->userLink = (int)get_user_meta($this->user->ID, BASE.'link', true);
-
+		$this->baseURL = get_home_url(null, '/dash');
 
     	add_action('template_redirect', [$this, 'handleRedirects']);
         add_action('template_include', [$this, 'dashboardTemplates']);
         add_action('admin_init', [$this, 'redirectFromAdmin']);
         add_action('wp_enqueue_scripts', [$this, 'dashboardScripts'], 50);
 		add_filter('jvbDashboardPage', [$this, 'renderIndex'], 10, 2);
+
+		add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeDashboard'], 10, 1);
     }
 
+	public function excludeDashboard(array $ids):array {
+		$cached = $this->cache->remember(
+			'dashboardIDs',
+			function() {
+				return get_posts([
+					'post_type'	=> BASE.'dash',
+					'posts_per_page' => -1,
+					'fields' => 'ids',
+				]);
+			});
+		return array_merge($ids, $cached);
+	}
+
     /**
      * Registers the custom post type that handles the dashboard
      * @return void
@@ -482,12 +499,13 @@
 		if (!is_singular(BASE.'dash') && !is_post_type_archive(BASE.'dash')) {
             return;
         }
+		wp_enqueue_style('jvb-icons-dash');
+		wp_enqueue_style('jvb-icons-forms');
 
 		wp_enqueue_script('jvb-form');
 		wp_enqueue_script('jvb-selector');
 		wp_enqueue_script('jvb-uploader');
 		wp_enqueue_script('jvb-content');
-		wp_enqueue_script('jvb-crud');
 
 		$page = $this->getCurrentPageSlug();
 
@@ -526,6 +544,12 @@
                     );
                     }
 					break;
+				case 'seo':
+					wp_enqueue_script('jvb-schema');
+					break;
+				default:
+					wp_enqueue_script('jvb-crud');
+					break;
             }
 			if (Features::forSite()->has('favourites')) {
 				 wp_enqueue_script('jvb-favourites');
@@ -629,7 +653,7 @@
             $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
             $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
             echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
-                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
+                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
 					jvbIcon('sun-dim', ['title'=> 'Light Mode']).
 					jvbIcon('moon', ['title'=>'Dark Mode']).
 					'</span></label>';
@@ -668,39 +692,133 @@
     {
         ?>
         </section>
+
+		<?php
+		$menu = new Navigation('sidebar');
+		$menuClasses = ['col', 'a-start', 'nowrap'];
+		$itemClasses = ['col'];
+		$menu->addClass('col a-start')->hasToggle()->defaultMenuClasses($menuClasses);
+		$menu->defaultItemClasses($itemClasses);
+		$pages = $this->getUserAllowedPages()?:[];
+		//Dashboard
+			//Referrals
+		$dashboard = $menu->addItem('Dashboard',jvbDashIcon('door'))
+			->url($this->baseURL);
+//			->submenu('dashboard')
+//			->defaultMenuClasses($menuClasses)
+//			->defaultItemClasses($itemClasses);
+		//notifications
+		if (in_array('Notifications', $pages)) {
+			$menu->addItem('Notifications',jvbDashIcon('bell'))
+				->url($this->baseURL.'/notifications');
+		}
+		if (in_array('Referrals', $pages)) {
+			$menu->addItem('Referrals', jvbDashIcon('hand-heart'))
+				->url($this->baseURL.'/referrals');
+		}
+		if (in_array('Favourites', $pages)) {
+			$menu->addItem('Favourites', jvbDashIcon('heart'))
+				->url($this->baseURL.'/favourites');
+		}
+
+		//Content
+			//content types
+				//Taxonomies
+		$availableContent = array_filter($pages, function($page, $key) {
+			return !is_numeric($key) && array_key_exists($key, JVB_CONTENT);
+		}, ARRAY_FILTER_USE_BOTH);
+		if (!empty ($availableContent)){
+			$content = $menu->addItem('Your Content', jvbDashIcon('book-bookmark'))
+				->submenu('content')
+				->defaultMenuClasses($menuClasses)
+				->defaultItemClasses($itemClasses);
+			foreach ($availableContent as $slug => $page) {
+				$config = JVB_CONTENT[$slug];
+				$item = $content->addItem($page, jvbDashIcon($config['icon']))
+					->url($this->baseURL.'/'.$slug);
+
+				$taxonomies = array_filter(JVB_TAXONOMY, function ($value, $key) use ($slug) {
+					return in_array($slug, $value['for_content']);
+				},1);
+				if (!empty ($taxonomies)) {
+					//TODO: If we add a dedicated 'create item' page, remove this from the empty check
+					$itemMenu = $item->submenu($slug);
+					foreach ($taxonomies as $s => $config) {
+						$itemMenu->addItem($config['plural'], $config['icon'])
+							->url($this->baseURL.'/'.$s);
+					}
+				}
+
+			}
+		}
+
+		//Settings
+		$settings = $menu->addItem('Settings', jvbDashIcon('faders'))
+			->submenu('settings')
+			->defaultItemClasses($itemClasses)
+			->defaultMenuClasses($menuClasses);
+
+			//SEO
+			if (in_array('SEO', $pages)) {
+				$settings->addItem('SEO', jvbDashIcon('robot'))
+					->url($this->baseURL.'/seo');
+			}
+			//Integrations
+			if (in_array('Integrations', $pages)) {
+				$settings->addItem('Integrations', jvbDashIcon('plugs-connected'))
+					->url($this->baseURL.'/integrations');
+			}
+		//Account
+		$account = $menu->addItem('Account', jvbDashIcon('user-circle'))
+			->url($this->baseURL.'/account')
+			->submenu('account')
+			->defaultMenuClasses($menuClasses)
+			->defaultItemClasses($itemClasses);
+		$account->addItem('Reset Password', jvbDashIcon('password'))
+			->url($this->baseURL.'/reset-password');
+			//name + contact
+			//reset password
+
+			if (in_array('notifications', $pages)) {
+				$account->addItem('Permissions', jvbDashIcon('keyhole'))
+					->url($this->baseURL.'/permissions');
+			}
+
+		echo $menu->render();
+		 ?>
+
         <footer class="col">
         	<?= jvbLoadingScreen() ?>
         	<?= TaxonomySelector::outputSelectorModal() ?>
-            <nav class="dashboard-nav">
+<!--            <nav class="dashboard-nav">-->
                 <?php
-                $current_page = $this->getCurrentPageSlug();
-                $pages = $this->getUserAllowedPages()?:[];
-
-                echo '<ul>';
-                foreach ($pages as $slug => $page) {
-					$slug = $this->getSlug($slug, $page);
-					$icon = $this->getIcon($slug, $page);
-					// Add data-page attribute for the navigator
-                    $active = ($current_page == $slug) ? ' class="current"' : '';
-                    $current = ($current_page == $slug) ? ' aria-current="page"' : '';
-
-
-					$link = ($page === 'dash') ? '/'.$page : "/dash/$slug";
-                    printf(
-                        '<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
-                        $active,
-                        get_home_url(null, $link),
-                        $current,
-                        $slug,
-                        $page,
-                        jvbIcon($icon, ['title'=> $page]),
-                        $page
-                    );
-                }
-
-                echo '</ul>';
+//                $current_page = $this->getCurrentPageSlug();
+//                $pages = $this->getUserAllowedPages()?:[];
+//                echo '<ul>';
+//                foreach ($pages as $slug => $page) {
+//					$slug = $this->getSlug($slug, $page);
+//					$icon = $this->getIcon($slug, $page);
+//					// Add data-page attribute for the navigator
+//                    $active = ($current_page == $slug) ? ' class="current"' : '';
+//                    $current = ($current_page == $slug) ? ' aria-current="page"' : '';
+//
+//
+//					$link = ($page === 'dash') ? '/'.$page : "/dash/$slug";
+//                    printf(
+//                        '<li%s><a href="%s"%s data-page="%s" data-dash title="%s">%s<span>%s</span></a></li>',
+//                        $active,
+//                        get_home_url(null, $link),
+//                        $current,
+//                        $slug,
+//                        $page,
+//                        jvbDashIcon($icon, ['title'=> $page]),
+//                        $page
+//                    );
+//                }
+//
+//                echo '</ul>';
                 ?>
-            </nav>
+<!--            </nav>-->
         </footer>
 
 
@@ -720,6 +838,14 @@
 		if ($page !== '' && $page !== 'dash') {
 			return $content;
 		}
+
+		if (Features::forSite()->has('referrals')) {
+			$whatever = JVB()->referrals()->getReferralWelcomeMessage($this->user->ID);
+			if (!empty($whatever)) {
+				return $whatever;
+			}
+		}
+
 		ob_start();
         $name = ($this->user->first_name !== '') ? $this->user->first_name : $this->user->display_name;
 
@@ -743,7 +869,7 @@
 			$icon = $this->getIcon($slug, $page);
             if ($title !== '') {
                 echo '<li><p><a href="'.get_home_url(null, '/dash/'.$slug.'/').'"
-                    data-page="'.$slug.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>';
+                    data-page="'.$slug.'" data-dash>'.jvbDashIcon($icon).ucwords($title).'</a></p></li>';
             }
 
         }
@@ -804,13 +930,13 @@
 			$out = '<nav class="integrations"><ul>';
 
 			$url = get_home_url(null, '/dash/integrations/');
-			$out .= '<li><a href="'.$url.'">'.jvbIcon('plugs-connected').'Integrations</a></li>';
+			$out .= '<li><a href="'.$url.'">'.jvbDashIcon('plugs-connected').'Integrations</a></li>';
 			foreach ($integrations as $name=> $integration) {
 				if (!JVB()->userCanConnect($name, $this->user->ID) || !$integration->hasDefaults()) {
 					continue;
 				}
 				$link = sanitize_title(str_replace('_', '-',$name));
-				$out .= '<li><a href="'.$url.$link.'">'.jvbIcon($integration->icon).$integration->getTitle().'</a></li>';
+				$out .= '<li><a href="'.$url.$link.'">'.jvbDashIcon($integration->icon).$integration->getTitle().'</a></li>';
 			}
 			$out .= '</ul></nav>';
 		}
@@ -861,16 +987,16 @@
         <div class="approvals container">
             <nav class="tabs row start" role="tablist">
                 <button type="button" class="tab active" data-tab="summary" role="tab" aria-selected="true">
-                    <h2><?= jvbIcon('infinity')?>All</h2>
+                    <h2><?= jvbDashIcon('infinity')?>All</h2>
                 </button>
                 <button type="button" class="tab" data-tab="artists" role="tab" aria-selected="false">
-                    <h2><?= jvbIcon('users-three')?>Artists</h2>
+                    <h2><?= jvbDashIcon('users-three')?>Artists</h2>
                 </button>
                 <button type="button" class="tab" data-tab="terms" role="tab" aria-selected="false">
-                    <h2><?= jvbIcon('hash')?>Terms</h2>
+                    <h2><?= jvbDashIcon('hash')?>Terms</h2>
                 </button>
                 <button type="button" class="tab" data-tab="yours" role="tab" aria-selected="false">
-                    <h2><?= jvbIcon('user')?>Yours</h2>
+                    <h2><?= jvbDashIcon('user')?>Yours</h2>
                 </button>
             </nav>
         </div>
@@ -947,7 +1073,7 @@
             $active = ($i === 1) ? ' active' : '';
             ?>
             <button type="button" class="tab<?=$active?>" data-tab="<?=$type?>" role="tab" aria-selected="<?= ($active !== '') ? 'true' : 'false'?>">
-                <h2><?=jvbIcon($settings['icon']??$key)?> <?= $settings['plural'] ?></h2>
+                <h2><?=jvbDashIcon($settings['icon']??$key)?> <?= $settings['plural'] ?></h2>
             </button>
             <?php
             $i++;
@@ -974,8 +1100,8 @@
             'vertical',
             'TAB NAV:',
             '',
-            jvbIcon('caret-double-down'),
-            jvbIcon('caret-double-right'))?>
+            jvbDashIcon('caret-double-down'),
+            jvbDashIcon('caret-double-right'))?>
 
     </div>
     <div class="items-container">
@@ -1034,18 +1160,18 @@
         <template class="<?= $type ?>Row">
             <tr>
                 <td>
-                     <?= jvbIcon('dots-six-vertical') ?>
+                     <?= jvbDashIcon('dots-six-vertical') ?>
                  </td>
                  <td data-id="actions" class="col">
                      <?= jvbRenderToggleTextField(
                          'public',
                          '',
                          '',
-                         jvbIcon('eye'),
-                         jvbIcon('eye-closed'))
+                         jvbDashIcon('eye'),
+                         jvbDashIcon('eye-closed'))
                      ?>
                      <button type="button" data-action="edit">
-                         <?= jvbIcon('pencil-simple') ?>
+                         <?= jvbDashIcon('pencil-simple') ?>
                     </button>
                 </td>
                 <?php
@@ -1104,7 +1230,7 @@
 		$pages = $this->cache->get($cacheKey);
 		if ($pages === false || JVB_TESTING) {
 			$pages = [];
-
+			$pages[] = 'SEO';
 			// Add feature-dependent pages (non-config)
 			if (Features::forSite()->has('referrals')) {
 				$pages[] = 'Referrals';
@@ -1205,9 +1331,10 @@
 			return [];
 		}
 
+
 		$cacheKey = "user_pages_{$userID}";
 		$pages = $this->cache->get($cacheKey);
-
+		$pages = false;
 		if ($pages === false || JVB_TESTING) {
 			if (user_can($userID, 'manage_options')) {
 				// Admin gets all pages as flat array
@@ -1230,7 +1357,7 @@
 					}
 					switch ($type) {
 						case 'content':
-							if (!user_can($userID, "edit_{$permission}")) {
+							if (user_can($userID, "edit_{$permission}")) {
 								$remove = false;
 							}
 							break;
@@ -1238,12 +1365,14 @@
 							$config = Features::getConfig($key, 'taxonomy');
 							if (array_key_exists('is_content', $config) && $config['is_content'] && (user_can($userID, "own_{$key}") || user_can($userID, "manage_{$key}"))) {
 								$remove = false;
+							} else if (count(array_intersect($config['for_content'], array_keys($pages))) > 0) {
+								$remove = false;
 							}
 							break;
 					}
 				} else {
 					switch ($slug) {
-						case 'integrations':
+						case 'Integrations':
 							foreach($roles as $role) {
 								if (Features::hasAnyIntegration('user', $role)) {
 									$remove = false;
@@ -1263,7 +1392,7 @@
 								}
 							}
 							break;
-						case 'approvals':
+						case 'Approvals':
 							$canApprove = false;
 							if (Features::forMembership()->has('term_approval')) {
 								if (array_key_exists('can_approve', JVB_MEMBERSHIP)) {
@@ -1313,6 +1442,8 @@
 								}
 							}
 							break;
+						case 'dash':
+						case 'Referrals':
 						case 'favourites':
 						case 'notifications':
 						case 'support':
@@ -1321,9 +1452,9 @@
 						default:
 							break;
 					}
-					if ($remove) {
-						unset($pages[$key]);
-					}
+				}
+				if ($remove) {
+					unset($pages[$key]);
 				}
 			}
 
diff --git a/inc/managers/EmailManager.php b/inc/managers/EmailManager.php
index e8084fd..c1e5202 100644
--- a/inc/managers/EmailManager.php
+++ b/inc/managers/EmailManager.php
@@ -394,8 +394,8 @@
 			<p>This password reset link is only valid for 24 hours.</p>',
 			$user->display_name,
 			$user_login,
-			jvbMailButton($reset_url,'Reset Password'),
-			jvbEmailLink($reset_url)
+			JVB()->email()->button($reset_url,'Reset Password'),
+			JVB()->email()->link($reset_url)
 		);
 		$content = apply_filters('jvbPasswordResetEmail', $content, $user_login, $user, $reset_url);
 		$content .= $this->signature;
@@ -438,7 +438,7 @@
 			$newUser['first_name'],
 			$oldUser['user_email'],
 			$newUser['user_email'],
-			jvbMailButton(wp_login_url(), 'Log In To Your Account')
+			JVB()->email()->button(wp_login_url(), 'Log In To Your Account')
 		);
 		$content = apply_filters('jvbEmailChangeRequestEmail', $content, $oldUser, $newUser);
         $content .= $this->signature;
@@ -469,8 +469,8 @@
 			%s
 			<p>Or copy and paste this link into your browser:</p>
 			%s',
-			jvbMailButton($confirm_url, 'Confirm this Email'),
-			jvbEmailLink($confirm_url)
+			JVB()->email()->button($confirm_url, 'Confirm this Email'),
+			JVB()->email()->link($confirm_url)
 		);
 
 		$content = apply_filters('jvbEmailChangedEmail', $content, $confirm_url);
@@ -499,7 +499,7 @@
 			<p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p>
 			%s',
 			$oldUser['first_name'],
-			jvbMailButton(wp_login_url(), 'Log In to Your Account')
+			JVB()->email()->button(wp_login_url(), 'Log In to Your Account')
 		);
 		$content = apply_filters('jvbPasswordChangeEmail', $content, $oldUser, $newUser);
         $content .= $this->signature;
@@ -545,8 +545,8 @@
 			<p>Or copy and paste this link into your browser:</p>
 			%s',
 			$request_name,
-			jvbMailButton($confirm_url, 'Confirm'),
-			jvbEmailLink($confirm_url)
+			JVB()->email()->button($confirm_url, 'Confirm'),
+			JVB()->email()->link($confirm_url)
 		);
 		$message = apply_filters('jvbPersonalDataExport', $message, $request_type, $confirm_url, $email_data);
 
@@ -579,8 +579,8 @@
 			%s
 			<div class="divider"></div>
 			<p><strong>Important:</strong> For privacy and security, this link will expire at %s.</p>',
-			jvbMailButton($download_url, 'Download Your Data'),
-			jvbEmailLink($download_url),
+			JVB()->email()->button($download_url, 'Download Your Data'),
+			JVB()->email()->link($download_url),
 			$expiresAt
 		);
 		$message = apply_filters('jvbPersonalDataExported', $message, $download_url, $expiresAt, $email_data);
@@ -588,6 +588,29 @@
 
         return $this->getEmailTemplate($message, 'Your Personal Data Export');
     }
+
+	public function signature():string
+	{
+		return $this->signature;
+	}
+
+	public function button(string $link, string $title):string
+	{
+		return sprintf(
+			'<p style="text-align: center;"><a href="%s" class="button">%s</a></p>',
+			$link,
+			$title
+		);
+	}
+
+	public function link(string $link):string
+	{
+		return sprintf(
+			'<p style="user-select:all;">%s</p>',
+			$link
+		);
+	}
+
 }
 
-new EmailManager();
+
diff --git a/inc/managers/ErrorHandler.php b/inc/managers/ErrorHandler.php
index add4eda..166229e 100644
--- a/inc/managers/ErrorHandler.php
+++ b/inc/managers/ErrorHandler.php
@@ -186,33 +186,61 @@
      *
      * @return bool Whether it gets logged successfully
      */
-    public function log(string $component, string $message, array $context = [], string $severity = 'error'):bool
-    {
-        try {
-            // Normal queue-based logging
-            JVB()->queue()->queueOperation(
-                'error_log',
-                get_current_user_id(),
-                [
-                    'component' => $component,
-                    'message' => $message,
-                    'context' => $context,
-                    'severity' => $severity
-                ],
-                ['priority' => 'high']
-            );
+	public function log(string $component, string $message, array $context = [], string $severity = 'error'): array
+	{
+		try {
+			$table = $this->wpdb->prefix . BASE . 'error_log';
 
+			// Validate severity
+			if (!array_key_exists($severity, $this->error_levels)) {
+				$severity = 'error';
+			}
 
-            // Immediate notification for critical errors
-            if ($severity === 'critical') {
-                $this->notifyAdmin($component, $message, $context);
-            }
-            return true;
-        } catch (Exception $e) {
-            error_log("[edmonton.ink Error] Failed to log error: " . $e->getMessage());
-            return false;
-        }
-    }
+			// Extract info
+			$error_type = sanitize_text_field($context['error_type'] ?? $component);
+			$method = isset($context['method']) ? sanitize_text_field($context['method']) : null;
+			$page_url = isset($context['url']) ? esc_url_raw($context['url']) : null;
+			$user_id = get_current_user_id();
+			$user_was_logged_in = $user_id > 0 || (!empty($context['isLoggedIn']));
+
+			// Determine source from context
+			$source = isset($context['source']) ? $context['source'] :
+				(isset($context['url']) ? 'frontend' : 'backend');
+
+			$result = $this->wpdb->insert(
+				$table,
+				[
+					'error_type' => $error_type,
+					'component' => $component,
+					'method' => $method,
+					'page_url' => $page_url,
+					'message' => sanitize_textarea_field($message),
+					'context' => json_encode($context),
+					'severity' => $severity,
+					'user_id' => $user_id ?: null,
+					'user_was_logged_in' => $user_was_logged_in ? 1 : 0,
+					'source' => $source,
+					'created_at' => current_time('mysql')
+				],
+				['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s']
+			);
+
+			if ($result === false) {
+				error_log("[ErrorHandler] Database insert failed: " . $this->wpdb->last_error);
+				return ['success' => false, 'message' => $this->wpdb->last_error];
+			}
+
+			if ($severity === 'critical') {
+				$this->checkErrorThreshold($error_type, $component);
+			}
+
+			return ['success' => true, 'id' => $this->wpdb->insert_id];
+
+		} catch (Exception $e) {
+			error_log("[ErrorHandler Exception] " . $e->getMessage());
+			return ['success' => false, 'message' => $e->getMessage()];
+		}
+	}
 
     /**
      * @param string $component What class or function logs the error
@@ -227,42 +255,96 @@
         $subject = "[edmonton.ink Critical Error] {$component}";
         $body = "Error: {$message}\n\nContext: " . print_r($context, true);
 
-        return jvbMail($admin_email, $subject, $body);
+        return JVB()->email()->sendEmail($admin_email, $subject, $body);
     }
 
     /**
      * Gather summary of the most important errors
+	 * @param ?string $start_date Defaults to today
+	 * @param ?string $end_date Defaults to today
      * @return array
      */
-    protected function gatherErrorSummary():array
-    {
-        $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
+	public function gatherErrorSummary(?string $start_date = null, ?string $end_date = null): array
+	{
+		$table = $this->wpdb->prefix . BASE . 'error_log';
 
-        // Get most frequent errors
-        $frequent_errors = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT error_type, component, message, COUNT(*) as count
-             FROM {$this->tableName}
-             WHERE created_at > %s
-             GROUP BY error_type, component, message
-             ORDER BY count DESC
-             LIMIT 20",
-            $yesterday
-        ));
+		if (!$start_date) {
+			$start_date = gmdate('Y-m-d 00:00:00', strtotime('-1 day'));
+		}
+		if (!$end_date) {
+			$end_date = gmdate('Y-m-d 23:59:59');
+		}
 
-        // Get most recent critical errors
-        $critical_errors = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT * FROM {$this->tableName}
-             WHERE severity = 'critical' AND created_at > %s
-             ORDER BY created_at DESC
-             LIMIT 5",
-            $yesterday
-        ));
+		// Most frequent error patterns (deduplicated by component/method/message)
+		$frequent = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT
+            component,
+            method,
+            error_type,
+            message,
+            severity,
+            source,
+            COUNT(*) as count,
+            SUM(CASE WHEN user_was_logged_in = 1 THEN 1 ELSE 0 END) as logged_in_count,
+            SUM(CASE WHEN user_was_logged_in = 0 THEN 1 ELSE 0 END) as logged_out_count,
+            MIN(created_at) as first_seen,
+            MAX(created_at) as last_seen
+         FROM {$table}
+         WHERE created_at BETWEEN %s AND %s
+         GROUP BY component, method, error_type, message, severity, source
+         ORDER BY count DESC, severity DESC
+         LIMIT 10",
+			$start_date,
+			$end_date
+		));
 
-        return [
-            'frequent' => $frequent_errors,
-            'critical' => $critical_errors
-        ];
-    }
+		// Critical errors
+		$critical = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT
+            component,
+            method,
+            error_type,
+            message,
+            source,
+            COUNT(*) as count,
+            SUM(CASE WHEN user_was_logged_in = 1 THEN 1 ELSE 0 END) as logged_in_count,
+            SUM(CASE WHEN user_was_logged_in = 0 THEN 1 ELSE 0 END) as logged_out_count,
+            MIN(created_at) as first_seen,
+            MAX(created_at) as last_seen
+         FROM {$table}
+         WHERE created_at BETWEEN %s AND %s AND severity = 'critical'
+         GROUP BY component, method, error_type, message, source
+         ORDER BY count DESC
+         LIMIT 5",
+			$start_date,
+			$end_date
+		));
+
+		// Overall stats
+		$stats = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT
+            COUNT(*) as total_errors,
+            COUNT(DISTINCT CONCAT(component, '-', COALESCE(method, ''), '-', error_type)) as unique_error_types,
+            SUM(CASE WHEN user_was_logged_in = 1 THEN 1 ELSE 0 END) as logged_in_errors,
+            SUM(CASE WHEN user_was_logged_in = 0 THEN 1 ELSE 0 END) as logged_out_errors,
+            SUM(CASE WHEN source = 'frontend' THEN 1 ELSE 0 END) as frontend_errors,
+            SUM(CASE WHEN source = 'backend' THEN 1 ELSE 0 END) as backend_errors,
+            SUM(CASE WHEN severity = 'critical' THEN 1 ELSE 0 END) as critical_count,
+            SUM(CASE WHEN severity = 'error' THEN 1 ELSE 0 END) as error_count,
+            SUM(CASE WHEN severity = 'warning' THEN 1 ELSE 0 END) as warning_count
+         FROM {$table}
+         WHERE created_at BETWEEN %s AND %s",
+			$start_date,
+			$end_date
+		));
+
+		return [
+			'frequent' => $frequent,
+			'critical' => $critical,
+			'stats' => $stats,
+			'date_range' => ['start' => $start_date, 'end' => $end_date]
+		];
+	}
 
     /**
      * Send daily error summary email to administrator
@@ -330,7 +412,7 @@
         $body .= "View detailed error logs in the dashboard: {$admin_url}\n\n";
 
         // Send the email
-        $sent = jvbMail($admin_email, $subject, $body, 'ERROR SUMMARY');
+        $sent = JVB()->email()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY');
 
         // Log that summary was sent
         if ($sent) {
@@ -426,6 +508,8 @@
 
     }
 
+
+
     protected function buildParams(WP_REST_Request $request):array {
         $allowedSeverity = [
             'all',
diff --git a/inc/managers/FormManager.php b/inc/managers/FormManager.php
index 963b973..5552e4d 100644
--- a/inc/managers/FormManager.php
+++ b/inc/managers/FormManager.php
@@ -394,7 +394,7 @@
         }
 
         // Send email
-        return jvbMail($to, $subject, $body, $headers);
+        return JVB()->email()->sendEmail($to, $subject, $body, $headers);
     }
 
     /**
diff --git a/inc/managers/IconsManager.php b/inc/managers/IconsManager.php
index 8278763..383e687 100644
--- a/inc/managers/IconsManager.php
+++ b/inc/managers/IconsManager.php
@@ -9,29 +9,40 @@
 
 class IconsManager
 {
-	protected static ?IconsManager $instance = null;
+	// Static array holding all source instances
+	protected static array $instances = [];
+
+	// Static storage for all custom icons across sources
+	protected static array $customIconsRegistry = [];
+
+	// Instance-specific properties
+	protected string $source;
+	protected array $icons = []; // Icons for THIS source [style => [names]]
 	protected CacheManager $cache;
 	protected string $style = 'regular';
 	protected array $styles = ['regular', 'bold', 'duotone', 'fill', 'light', 'thin'];
-	// Custom icons registered via filter
-	protected array $customIcons = [];
-	protected array $usedIcons = [];
+	protected array $customIcons = []; // Custom icons for THIS source
 	protected array $map = [];
 	protected const MAX_VERSIONS = 5;
 
 	/**
-	 * Get singleton instance
+	 * Factory method - get or create instance for a source
 	 */
-	public static function getInstance(): IconsManager
+	public static function for(string $source = 'icons'): IconsManager
 	{
-		if (self::$instance === null) {
-			self::$instance = new self();
+		if (!isset(self::$instances[$source])) {
+			self::$instances[$source] = new self($source);
 		}
-		return self::$instance;
+		return self::$instances[$source];
 	}
-	private function __construct()
+
+	/**
+	 * Constructor now takes source parameter
+	 */
+	private function __construct(string $source)
 	{
-		$this->cache = CacheManager::for('icons', WEEK_IN_SECONDS);
+		$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']
@@ -39,49 +50,106 @@
 
 		$this->addMap();
 
-		// Allow custom icon registration
-		$this->customIcons = apply_filters('jvbRegisterCustomIcons', [
-			'syncing'		=> JVB_DIR .'/assets/icons/cloud-sync-thin.svg',
-			'alphabetical'	=> JVB_DIR.'/assets/icons/alphabetical.svg'
-		]);
+		// Register custom icons only once for all sources
+		if ($source === 'icons') {
+			$this->registerCustomIcons();
+		}
 
+		// Load custom icons for THIS source
+		$this->loadCustomIconsForSource();
 
-		$this->usedIcons = get_option(BASE.'usedIcons', []);
-		$this->includeIcons();
-		// Track custom icons for CSS generation
-		$this->trackCustomIcons();
-		// Register hooks only once
-		$this->registerHooks();
+		// Load stored icons for this source
+		$this->loadStoredIcons();
+
+		if (empty($this->icons)) {
+			$this->includeIcons();
+		}
+
+		// Register global hooks only once (first instance)
+		if (count(self::$instances) === 1) {
+			$this->registerGlobalHooks();
+		}
+
+		// Register instance's hooks (every instance)
+		$this->registerInstanceHooks();
 	}
 
+
+
 	/**
-	 * Ensure custom icons are tracked for CSS generation
+	 * Register all custom icons (runs once)
 	 */
-	protected function trackCustomIcons(): void
+	protected function registerCustomIcons(): void
 	{
-		if (empty($this->customIcons)) {
-			return;
-		}
+		$icons = array_merge(apply_filters('jvbRegisterCustomIcons', []), ['syncing' => JVB_DIR . '/assets/icons/cloud-sync-thin.svg',
+			'alphabetical' => JVB_DIR . '/assets/icons/alphabetical.svg']);
 
-		foreach ($this->customIcons as $name => $path) {
-			$this->trackIconUsage($name, $this->style);
-		}
+		// Process and store in static property so all instances can access
+		self::$customIconsRegistry = $this->processCustomIconsArray($icons);
 	}
 
 	/**
-	 * Include icons via filter (for JS usage, etc.)
+	 * Process custom icons array into source-grouped format
 	 */
+	protected function processCustomIconsArray(array $icons): array
+	{
+		$out = [];
+		foreach ($icons as $name => $source) {
+			if (!file_exists($source)) {
+				error_log('[IconsManager] No file exists for custom Icon: '.$name);
+				continue;
+			}
+			$out[$name] = $source;
+		}
+
+		return $out;
+	}
+
+	/**
+	 * Load custom icons for this instance's source
+	 */
+	protected function loadCustomIconsForSource(): void
+	{
+		$this->customIcons = self::$customIconsRegistry;
+//		foreach ($this->customIcons as $name => $path) {
+//			if (!isset($this->icons[$this->style])) {
+//				$this->icons[$this->style] = [];
+//			}
+//			if (!in_array($name, $this->icons[$this->style])) {
+//				$this->icons[$this->style][] = $name;
+//			}
+//		}
+	}
+
+	/**
+	 * Load previously stored icons for this source
+	 */
+	protected function loadStoredIcons(): void
+	{
+		$allIcons = get_option(BASE.'usedIcons', []);
+		$storedIcons = $allIcons[$this->source] ?? [];
+
+		// Merge stored icons with any existing icons (like custom icons)
+		foreach ($storedIcons as $style => $names) {
+			if (!isset($this->icons[$style])) {
+				$this->icons[$style] = [];
+			}
+			$this->icons[$style] = array_unique(array_merge($this->icons[$style], $names));
+		}
+	}
+
 	protected function includeIcons():void
 	{
-		$icons = get_option(BASE.'includeIcons');
-
-		if (!$icons) {
-			$icons = [
+		$defaults = [
+			'icons' => [
+				'google-logo',
+				'apple-logo',
 				'check-circle',
 				'close-circle',
 				'cloud-slash',
 				'exclamation-mark',
 				'cloud-arrow-down',
+				'caret-down',
 				'cloud-arrow-up',
 				'cloud-check',
 				'cloud-slash',
@@ -92,9 +160,11 @@
 				'share-fat',
 				'trash',
 				'star',
+				'alphabetical',
 				['name' => 'star-half', 'style' => 'fill'],
 				['name' => 'star', 'style' => 'fill'],
-				//FORMATTING
+			],
+			'forms' => [
 				'copy',
 				'paragraph',
 				'text-h-one',
@@ -120,102 +190,186 @@
 				'file-doc',
 				'file-txt',
 				'file-xls',
-			];
+			],
+//			'dash' => [
+//
+//			]
+		];
 
-			$check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
-			foreach ($check as $constant) {
-				foreach ($constant as $key => $value) {
-					if (array_key_exists('icon', $value) && !in_array($value['icon'], $icons)) {
-						$icons[] = $value['icon'];
-					}
+		// Add icons from content/taxonomy/user configs (like old behavior)
+		$configIcons = $this->getIconsFromConfigs();
+		if (!empty($configIcons)) {
+			foreach ($configIcons as $source => $icons) {
+				if (!isset($defaults[$source])) {
+					$defaults[$source] = [];
 				}
-			}
-			$icons = apply_filters('jvbIncludeIcons', $icons);
-			$icons = $this->maybePrefixIcons($icons);
-			update_option(BASE.'includeIcons', $icons);
-		}
-
-		// Ensure icons are in the correct format (handle legacy data)
-		if (!$this->isIconsArrayPrefixed($icons)) {
-			$icons = $this->maybePrefixIcons($icons);
-			update_option(BASE.'includeIcons', $icons);
-		}
-
-		$additional = apply_filters('jvbIncludeIcons', []);
-		if (!empty($additional)) {
-			$additional = $this->maybePrefixIcons($additional);
-			$merged = $this->mergeUsedIcons($icons, $additional);
-
-			if ($icons != $merged) {
-				update_option(BASE.'includeIcons', $merged);
-				$icons = $merged;
+				$defaults[$source] = array_merge($defaults[$source], $icons);
 			}
 		}
 
-		foreach ($icons as $style => $theIcons) {
-			foreach($theIcons as $icon) {
-				$this->trackIconUsage($icon, $style);
-			}
+		// Allow filtering per source (extensibility)
+		$icons = apply_filters("jvbIncludeIcons_{$this->source}", $defaults[$this->source] ?? []);
+
+		// Also allow filtering all sources at once
+		$allIcons = apply_filters('jvbIncludeIcons', $defaults);
+		if (isset($allIcons[$this->source])) {
+			$icons = array_merge($icons, $allIcons[$this->source]);
+		}
+
+		if (!empty($icons)) {
+			$this->include($icons);
 		}
 	}
 
 	/**
-	 * Check if icons array is in the prefixed format [style => [icons]]
+	 * Get icons from JVB_CONTENT, JVB_TAXONOMY, JVB_USER configs
 	 */
-	protected function isIconsArrayPrefixed(array $icons): bool
+	protected function getIconsFromConfigs(): array
 	{
-		if (empty($icons)) {
-			return true;
+		$icons = [];
+		$check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
+
+		foreach ($check as $constant) {
+			foreach ($constant as $key => $value) {
+				if (isset($value['icon'])) {
+					// Determine source based on context (you could add 'icon_source' to configs)
+					$source = $value['icon_source'] ?? 'icons';
+
+					if (!isset($icons[$source])) {
+						$icons[$source] = [];
+					}
+					$icons[$source][] = $value['icon'];
+				}
+			}
 		}
 
-		// Check if first key is a valid style name
-		$first_key = array_key_first($icons);
-		if (!in_array($first_key, $this->styles)) {
-			return false;
-		}
-
-		// Check if first value is an array
-		return is_array($icons[$first_key]);
+		return $icons;
 	}
 
-	protected function maybePrefixIcons(array $icons):array
+	/**
+	 * Public method to include icons in this source
+	 */
+	public function include(array $icons): self
 	{
-		$out = [];
-		foreach ($icons as $icon) {
-			if (is_array($icon) && array_key_exists('style', $icon)) {
-				if (!array_key_exists($icon['style'], $out)) {
-					$out[$icon['style']] = [];
-				}
-				if (!in_array($icon['name'], $out[$icon['style']])) {
-					$out[$icon['style']][] = $icon['name'];
-				}
-			} elseif(is_array($icon)) {
-				$icon = $icon['name'];
+		$processed = $this->processIconArray($icons);
+		$changed = false;
+
+		foreach ($processed as $style => $names) {
+			if (!isset($this->icons[$style])) {
+				$this->icons[$style] = [];
 			}
-			if (!is_array($icon)) {
-				if (!array_key_exists($this->style, $out)) {
-					$out[$this->style] = [];
+
+			foreach ($names as $name) {
+				// Skip if already in this source
+				if (in_array($name, $this->icons[$style])) {
+					continue;
 				}
-				if (!in_array($icon, $out[$this->style])){
-					$out[$this->style][] = $icon;
+
+				// Skip if already in main 'icons' source
+				if ($this->iconExistsInMainSource($name, $style)) {
+					error_log("[IconsManager] Skipping '{$name}' in '{$this->source}' - already in 'icons' source");
+					continue;
 				}
+
+				$this->icons[$style][] = $name;
+				$changed = true;
 			}
 		}
+
+		// Only save if something actually changed
+		if ($changed) {
+			$this->saveIcons();
+		}
+
+		return $this;
+	}
+
+	/**
+	 * Process icon array into [style => [names]] format
+	 */
+	protected function processIconArray(array $icons): array
+	{
+		$out = [];
+
+		foreach ($icons as $icon) {
+			if (is_array($icon) && isset($icon['style'])) {
+				$style = $icon['style'];
+				$name = $icon['name'];
+			} else {
+				$style = $this->style;
+				$name = is_array($icon) ? $icon['name'] : $icon;
+			}
+
+			if (!isset($out[$style])) {
+				$out[$style] = [];
+			}
+
+			if (!in_array($name, $out[$style])) {
+				$out[$style][] = $name;
+			}
+		}
+
 		return $out;
 	}
 
-	protected function addMap():void
+	/**
+	 * Save all icons across all instances
+	 */
+	protected function saveIcons(): void
+	{
+		$allIcons = [];
+		foreach (self::$instances as $source => $instance) {
+			$allIcons[$source] = $instance->icons;
+		}
+
+		update_option(BASE.'usedIcons', $allIcons);
+
+		// Track WHICH source needs updating
+		$needsUpdate = get_option(BASE.'icons_needs_update', []);
+		if (!is_array($needsUpdate)) {
+			$needsUpdate = [];
+		}
+		$needsUpdate[$this->source] = true;
+		update_option(BASE.'icons_needs_update', $needsUpdate);
+	}
+
+	/**
+	 * Check if icon exists in other sources
+	 */
+	protected function checkDuplicateAcrossInstances(string $name, string $style): void
+	{
+		$foundIn = [];
+
+		foreach (self::$instances as $source => $instance) {
+			if (isset($instance->icons[$style]) && in_array($name, $instance->icons[$style])) {
+				$foundIn[] = $source;
+			}
+		}
+
+		if (count($foundIn) > 1) {
+			error_log(sprintf(
+				'[IconsManager] Warning: Icon "%s" (%s) is registered in multiple sources: %s. Consider consolidating to avoid duplicate CSS output.',
+				$name,
+				$style,
+				implode(', ', $foundIn)
+			));
+		}
+	}
+
+	protected function addMap(): void
 	{
 		$map = get_option(BASE.'iconMap');
 		if (!$map) {
-			$map = [];
-			if (Features::forSite()->has('referrals')){
+			$map = [
+				'seo'	=> 'robot'
+			];
+			if (Features::forSite()->has('referrals')) {
 				$map['referrals'] = 'hand-heart';
 			}
-			if (Features::forSite()->has('dashboard')){
+			if (Features::forSite()->has('dashboard')) {
 				$map['dash'] = 'door';
 			}
-			if (Features::forSite()->has('magicLink')){
+			if (Features::forSite()->has('magicLink')) {
 				$map['magicLink'] = 'magic-wand';
 			}
 			if (Features::hasAnyIntegration()) {
@@ -228,44 +382,111 @@
 	}
 
 	/**
-	 * Register WordPress hooks
+	 * Register global hooks (only once)
 	 */
-	protected function registerHooks(): void
+	protected function registerGlobalHooks(): void
 	{
-		add_action('init', [$this, 'includeIcons'], 1);
-		add_action('init', [$this, 'checkCSS'], 10);
-		add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
+		add_action('init', [$this, 'checkCSS']);
+	}
+
+	/**
+	 * Register instance-specific hooks (every instance)
+	 */
+	protected function registerInstanceHooks(): void
+	{
+		// Register this source's stylesheet
+		add_action('init', [$this, 'registerStyle'], 11);
+
+		// Auto-enqueue base icons on front-end
+		if ($this->source === 'icons') {
+			add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
+		}
+
+		// Auto-enqueue all in admin
 		add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']);
 	}
 
-	public function checkCSS():void
+	public function enqueueIconStyles():void
 	{
-//		update_option(BASE.'icons_needs_update', true);
-		if (get_option(BASE.'icons_needs_update', false)) {
-			error_log('Regenerating CSS');
+		wp_enqueue_style('jvb-icons-'.$this->source);
+	}
+
+	public function checkCSS(): void
+	{
+		$needsUpdate = get_option(BASE.'icons_needs_update', []);
+		if (!empty($needsUpdate)) {
+			error_log('Regenerating CSS for sources: ' . implode(', ', array_keys($needsUpdate)));
 			delete_option(BASE.'icons_needs_update');
-			$this->regenerateCSS();
+			self::regenerateAllCSS($needsUpdate);
 		}
 	}
 
-	protected function regenerateCSS(): void
+	protected static function regenerateAllCSS(array $sourcesToUpdate = []): void
 	{
 		error_log('[IconsManager]:regenerateCSS');
-		$css = $this->generateIconCSS();
-		$css_path = JVB_CHILD_DIR.'/assets/css/';
-		if (!file_exists($css_path)) {
-			wp_mkdir_p($css_path);
+		$css_dir = JVB_CHILD_DIR.'/assets/css/';
+
+		if (!file_exists($css_dir)) {
+			wp_mkdir_p($css_dir);
 		}
-		$css_path .= '/icons.css';
 
+		// If no specific sources provided, regenerate all
+		if (empty($sourcesToUpdate)) {
+			$sourcesToUpdate = array_fill_keys(array_keys(self::$instances), true);
+		}
 
-		// Archive current version before overwriting
-		$this->archiveCurrentVersion($css);
+		// Generate CSS only for sources that need it
+		foreach (self::$instances as $source => $instance) {
+			if (!isset($sourcesToUpdate[$source])) {
+				continue;
+			}
 
-		if (file_put_contents($css_path, $css) !== false) {
-			CacheManager::updateTimestamp('icons');
-		} else {
-			error_log('[IconsManager]Could not write css.');
+			$css = $instance->generateIconCSS();
+			$css_path = $css_dir . $source . '.css';
+
+			$instance->archiveCurrentVersion($css);
+
+			if (file_put_contents($css_path, $css) !== false) {
+				CacheManager::updateTimestamp('icons_' . $source);
+				error_log("[IconsManager] Updated {$source}.css");
+			} else {
+				error_log("[IconsManager] Could not write {$source}.css");
+			}
+		}
+	}
+
+	protected function regenerateCSS(array $sourcesToUpdate = []): void
+	{
+		error_log('[IconsManager]:regenerateCSS');
+		$css_dir = JVB_CHILD_DIR.'/assets/css/';
+
+		if (!file_exists($css_dir)) {
+			wp_mkdir_p($css_dir);
+		}
+
+		// If no specific sources provided, regenerate all
+		if (empty($sourcesToUpdate)) {
+			$sourcesToUpdate = array_fill_keys(array_keys(self::$instances), true);
+		}
+
+		// Generate CSS only for sources that need it
+		foreach (self::$instances as $source => $instance) {
+			if (!isset($sourcesToUpdate[$source])) {
+				continue; // Skip this source
+			}
+
+			$css = $instance->generateIconCSS();
+			$css_path = $css_dir . $source . '.css';
+
+			// Archive current version before overwriting
+			$instance->archiveCurrentVersion($css);
+
+			if (file_put_contents($css_path, $css) !== false) {
+				CacheManager::updateTimestamp('icons_' . $source);
+				error_log("[IconsManager] Updated {$source}.css");
+			} else {
+				error_log("[IconsManager] Could not write {$source}.css");
+			}
 		}
 	}
 
@@ -294,10 +515,10 @@
 	 *   - 'size' => 24 (for custom sizing via inline style)
 	 * @return string HTML icon element
 	 */
-	public function getIcon(string $name, array $options = []): string
+	public function get(string $name, array $options = []): string
 	{
-		$style = array_key_exists('style', $options) ? $options['style'] :$this->style;
-		$name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name;
+		$style = $options['style'] ?? $this->style;
+		$name = $this->map[$name] ?? $name;
 
 		// Validate icon exists
 		if (!$this->iconExists($name, $style)) {
@@ -305,56 +526,51 @@
 			return '';
 		}
 
+		// Track usage - only if not already tracked
+		if (!isset($this->icons[$style])) {
+			$this->icons[$style] = [];
+		}
 
+		if (!in_array($name, $this->icons[$style])) {
+			// Check if it's already in main source (for non-main sources)
+			if ($this->iconExistsInMainSource($name, $style)) {
+				// Don't add to this source, but still render the icon
+				// The CSS from icons.css will handle it
+			} else {
+				// Add to this source
+				$this->icons[$style][] = $name;
+				$this->checkDuplicateAcrossInstances($name, $style);
+				$this->saveIcons();
+			}
+		}
 
-		// Track icon usage
-		$this->trackIconUsage($name, $style);
-
-		$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
-		// Build classes
+		// Build icon HTML (same as before)
+		$styleClass = ($style !== $this->style) ? '-'.substr($style, 0, 2) : '';
 		$classes = ['icon', 'icon-' . $name.$styleClass];
-		if (!empty($options['class'])) {
+
+		if (isset($options['class'])) {
 			$classes[] = $options['class'];
 		}
 
+		$attrs = ['class' => implode(' ', $classes)];
 
-		$attrs = ['class="' . esc_attr(implode(' ', $classes)) . '"'];
-		$attrs[] = 'aria-hidden="true"';
-
-
-
-		return '<i ' . implode(' ', $attrs) . '></i>';
-	}
-
-	/**
-	 * Track icon usage for CSS generation
-	 */
-	protected function trackIconUsage(string $name, string $style): void
-	{
-		$needsUpdate = false;
-
-		if (!array_key_exists($style, $this->usedIcons)) {
-			$this->usedIcons[$style] = [];
-			$needsUpdate = true;
+		if (isset($options['label'])) {
+			$attrs['aria-label'] = esc_attr($options['label']);
+			$attrs['role'] = 'img';
+		} elseif (isset($options['decorative']) && $options['decorative']) {
+			$attrs['aria-hidden'] = 'true';
 		}
 
-		if (!in_array($name, $this->usedIcons[$style])) {
-			$this->usedIcons[$style][] = $name;
-			$needsUpdate = true;
+		if (isset($options['size'])) {
+			$attrs['style'] = sprintf('--icon-size: %dpx;', absint($options['size']));
 		}
 
-		if ($needsUpdate) {
-			// Merge with existing option to never lose icons
-			$existing = get_option(BASE.'usedIcons', []);
-			$merged = $this->mergeUsedIcons($existing, $this->usedIcons);
-			update_option(BASE.'usedIcons', $merged);
-
-			// Flag for regeneration on next init
-			update_option(BASE.'icons_needs_update', true);
-
-			// Clear cache
-			$this->cache->delete('icon_styles_css');
+		$attr_string = '';
+		foreach ($attrs as $key => $value) {
+			$attr_string .= sprintf(' %s="%s"', $key, $value);
 		}
+
+		return sprintf('<i%s></i>', $attr_string);
 	}
 
 	/**
@@ -425,17 +641,14 @@
 		return $svg;
 	}
 
-
-	/**
-	 * Enqueue icon styles via REST endpoint
-	 */
-	public function enqueueIconStyles(): void
+	public function registerStyle(): void
 	{
-		$timestamp = CacheManager::getTimestamp('icons');
+		$timestamp = CacheManager::getTimestamp('icons_' . $this->source);
+		$handle = 'jvb-icons-' . $this->source;
 
-		wp_enqueue_style(
-			'jvb-icons',
-			JVB_CHILD_URL.'assets/css/icons.css',
+		wp_register_style(
+			$handle,
+			JVB_CHILD_URL . "assets/css/{$this->source}.css",
 			[],
 			$timestamp
 		);
@@ -447,11 +660,10 @@
 	protected function generateIconCSS(): string
 	{
 		$css = '';
-		$this->mergeUsedIcons();
 
-		foreach ($this->usedIcons as $style => $icons) {
-			$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
-			foreach ($icons as $icon) {
+		foreach ($this->icons as $style => $names) {
+			$styleClass = ($style !== $this->style) ? '-'.substr($style, 0, 2) : '';
+			foreach ($names as $icon) {
 				$svg = $this->getEncodedSVG($icon, $style);
 				if ($svg !== '') {
 					$css .= ".icon-{$icon}{$styleClass}{";
@@ -460,35 +672,8 @@
 				}
 			}
 		}
-		return $this->minifyCss($css);
-	}
 
-	protected function mergeUsedIcons(array|bool $oldIcons = true, array|bool $newIcons = true):array
-	{
-		$set = false;
-		if ($oldIcons === true) {
-			$oldIcons = $this->usedIcons;
-			$set = true;
-		}
-		if ($newIcons === true) {
-			$history = $this->getVersionHistory();
-			$newIcons = (count($history) > 0) ? $history[0]['iconList'] : [];
-		}
-		foreach ($newIcons as $style => $icons) {
-			if (!isset($oldIcons[$style])) {
-				//Style  doesn't exist in previous set, add the whole thing
-				$oldIcons[$style] = $icons;
-			} else {
-				$oldIcons[$style] = array_unique(
-					array_merge($oldIcons[$style], $icons)
-				);
-			}
-		}
-		if ($set) {
-			$this->usedIcons = $oldIcons;
-			update_option(BASE.'usedIcons', $oldIcons);
-		}
-		return $oldIcons;
+		return $this->minifyCss($css);
 	}
 
 	protected function minifyCSS(string $css): string
@@ -502,7 +687,8 @@
 
 		return trim($css);
 	}
-	public function getCSSIcon(string $icon, ?string $style=null):string
+
+	public function getCSSIcon(string $icon, ?string $style = null): string
 	{
 		if (!$style) {
 			$style = $this->style;
@@ -513,20 +699,20 @@
 		}
 		return '';
 	}
-	public function getEncodedSVG(string $icon, ?string $style = null):string
+
+	public function getEncodedSVG(string $icon, ?string $style = null): string
 	{
 		if (!$style) {
 			$style = $this->style;
 		}
 		return $this->cache->remember($style.$icon,
-		function () use ($icon, $style) {
-			$svg = $this->getRawSvg($icon, $style);
-			if ($svg) {
-				return base64_encode($svg);
-			}
-			return '';
-		});
-
+			function () use ($icon, $style) {
+				$svg = $this->getRawSvg($icon, $style);
+				if ($svg) {
+					return base64_encode($svg);
+				}
+				return '';
+			});
 	}
 
 	/**
@@ -534,12 +720,15 @@
 	 */
 	public function clearIconCache(): void
 	{
-		delete_option(BASE . 'icon_usage_list'); // Clear DB option
+		delete_option(BASE . 'icon_usage_list'); // Legacy
 		delete_option(BASE.'usedIcons');
-		delete_option(BASE.'includeIcons');
 		delete_option(BASE.'iconMap');
-		$this->cache->delete('icon_styles_css');
-		CacheManager::updateTimestamp('icons');
+
+		// Clear cache for all sources
+		foreach (self::$instances as $source => $instance) {
+			$instance->cache->delete('icon_styles_css');
+			CacheManager::updateTimestamp('icons_' . $source);
+		}
 	}
 
 	protected function archiveCurrentVersion(string $css): void
@@ -547,13 +736,13 @@
 		$history = $this->getVersionHistory();
 
 		$icon_count = 0;
-		foreach ($this->usedIcons as $style => $icons) {
-			$icon_count += count($icons);
+		foreach ($this->icons as $style => $names) {
+			$icon_count += count($names);
 		}
 
 		$newEntry = [
 			'css' => $css,
-			'iconList' => $this->usedIcons,
+			'iconList' => $this->icons,
 			'timestamp' => time(),
 			'icon_count' => $icon_count,
 			'size' => strlen($css),
@@ -566,12 +755,12 @@
 			$history = array_slice($history, 0, self::MAX_VERSIONS);
 		}
 
-		update_option(BASE.'icon_css_history', $history);
+		update_option(BASE.'icon_css_history_' . $this->source, $history);
 	}
 
 	public function getVersionHistory(): array
 	{
-		return get_option(BASE.'icon_css_history', []);
+		return get_option(BASE.'icon_css_history_' . $this->source, []);
 	}
 
 	public function restoreVersion(int $timestamp): bool
@@ -580,7 +769,7 @@
 
 		foreach ($history as $entry) {
 			if ($entry['timestamp'] === $timestamp) {
-				$css_path = JVB_DIR . '/assets/css/icons.css';
+				$css_path = JVB_CHILD_DIR . '/assets/css/' . $this->source . '.css';
 
 				// Archive current before restoring
 				$current_css = file_get_contents($css_path);
@@ -590,9 +779,9 @@
 
 				// Restore the version
 				if (file_put_contents($css_path, $entry['css']) !== false) {
-					$this->usedIcons = $entry['iconList'];
-					update_option(BASE.'usedIcons', $this->usedIcons);
-					CacheManager::updateTimestamp('icons');
+					$this->icons = $entry['iconList'];
+					$this->saveIcons();
+					CacheManager::updateTimestamp('icons_' . $this->source);
 					return true;
 				}
 
@@ -600,15 +789,20 @@
 			}
 		}
 
-		error_log("[IconsManager] Version {$timestamp} not found in history");
+		error_log("[IconsManager] Version {$timestamp} not found in history for source {$this->source}");
 		return false;
 	}
 
 	public function forceRefresh(): void
 	{
 		$this->clearIconCache();
-		update_option(BASE.'icons_needs_update', true);
-		CacheManager::updateTimestamp('icons');
+		$needsUpdate = get_option(BASE.'icons_needs_update', []);
+		if (!is_array($needsUpdate)) {
+			$needsUpdate = [];
+		}
+		$needsUpdate[$this->source] = true;
+		update_option(BASE.'icons_needs_update', $needsUpdate);
+		CacheManager::updateTimestamp('icons_' . $this->source);
 	}
 
 	public function mergeVersions(array $timestamps): bool
@@ -617,8 +811,9 @@
 			return false;
 		}
 
-		$history = get_option(BASE.'icon_css_history', []);
+		$history = get_option(BASE.'icon_css_history_' . $this->source, []);
 		$merged_icons = [];
+
 		// Collect icons from selected versions
 		foreach ($history as $entry) {
 			if (in_array($entry['timestamp'], $timestamps)) {
@@ -640,18 +835,34 @@
 		}
 
 		// Archive current version
-		$current_css = file_get_contents(JVB_DIR . '/assets/css/icons.css');
+		$current_css = file_get_contents(JVB_CHILD_DIR . '/assets/css/' . $this->source . '.css');
 		if ($current_css !== false) {
 			$this->archiveCurrentVersion($current_css);
 		}
 
 		// Update used icons and regenerate
-		$this->usedIcons = $merged_icons;
-		update_option(BASE.'usedIcons', $this->usedIcons);
-
-		// Force regeneration
-		$this->regenerateCSS();
+		$this->icons = $merged_icons;
+		$this->saveIcons();
 
 		return true;
 	}
+
+	/**
+	 * Check if icon already exists in the main 'icons' source
+	 */
+	protected function iconExistsInMainSource(string $name, string $style): bool
+	{
+		// If this IS the main source, no need to check
+		if ($this->source === 'icons') {
+			return false;
+		}
+
+		// Check if main icons source exists
+		if (!isset(self::$instances['icons'])) {
+			return false;
+		}
+
+		$mainIcons = self::$instances['icons']->icons;
+		return isset($mainIcons[$style]) && in_array($name, $mainIcons[$style]);
+	}
 }
diff --git a/inc/managers/IconsManagerBackup.php b/inc/managers/IconsManagerBackup.php
new file mode 100644
index 0000000..285ec6b
--- /dev/null
+++ b/inc/managers/IconsManagerBackup.php
@@ -0,0 +1,670 @@
+<?php
+namespace JVBase\inc\managers;
+
+use JVBase\managers\CacheManager;
+use JVBase\utility\Features;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class IconsManagerBackup
+{
+	protected static ?IconsManagerBackup $instance = null;
+	protected CacheManager $cache;
+	protected string $style = 'regular';
+	protected array $styles = ['regular', 'bold', 'duotone', 'fill', 'light', 'thin'];
+	// Custom icons registered via filter
+	protected array $customIcons = [];
+	protected array $usedIcons = [];
+	protected array $map = [];
+	protected const MAX_VERSIONS = 5;
+
+	/**
+	 * Get singleton instance
+	 */
+	public static function getInstance(): IconsManagerBackup
+	{
+		if (self::$instance === null) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+	private function __construct()
+	{
+		$this->cache = CacheManager::for('icons', WEEK_IN_SECONDS);
+
+		$this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles))
+			? JVB_SITE['icons']
+			: 'regular';
+
+		$this->addMap();
+
+		// Allow custom icon registration
+		$this->customIcons = apply_filters('jvbRegisterCustomIcons', [
+			'syncing'		=> JVB_DIR .'/assets/icons/cloud-sync-thin.svg',
+			'alphabetical'	=> JVB_DIR.'/assets/icons/alphabetical.svg'
+		]);
+
+
+		$this->usedIcons = get_option(BASE.'usedIcons', []);
+		$this->includeIcons();
+		// Track custom icons for CSS generation
+		$this->trackCustomIcons();
+		// Register hooks only once
+		$this->registerHooks();
+	}
+
+	/**
+	 * Ensure custom icons are tracked for CSS generation
+	 */
+	protected function trackCustomIcons(): void
+	{
+		if (empty($this->customIcons)) {
+			return;
+		}
+
+		foreach ($this->customIcons as $name => $path) {
+			$this->trackIconUsage($name, $this->style);
+		}
+	}
+
+	/**
+	 * Include icons via filter (for JS usage, etc.)
+	 */
+	protected function includeIcons():void
+	{
+		$icons = get_option(BASE.'includeIcons');
+
+		if (!$icons) {
+			$icons = [
+				'check-circle',
+				'close-circle',
+				'cloud-slash',
+				'exclamation-mark',
+				'cloud-arrow-down',
+				'cloud-arrow-up',
+				'cloud-check',
+				'cloud-slash',
+				'cloud-warning',
+				'syncing',
+				'cloud-x',
+				'arrows-clockwise',
+				'share-fat',
+				'trash',
+				'star',
+				['name' => 'star-half', 'style' => 'fill'],
+				['name' => 'star', 'style' => 'fill'],
+				//FORMATTING
+				'copy',
+				'paragraph',
+				'text-h-one',
+				'text-h-two',
+				'text-h-three',
+				'text-h-four',
+				'text-h-five',
+				'text-h-six',
+				['name'	=>'text-b', 'style' => 'fill'],
+				'text-italic',
+				'text-underline',
+				'text-strikethrough',
+				'list-dashes',
+				'list-numbers',
+				'text-align-left',
+				'text-align-center',
+				'text-align-right',
+//			'text-align-justify',
+				'link',
+				//FILE ICONS
+				'file-pdf',
+				'file-csv',
+				'file-doc',
+				'file-txt',
+				'file-xls',
+			];
+
+			$check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
+			foreach ($check as $constant) {
+				foreach ($constant as $key => $value) {
+					if (array_key_exists('icon', $value) && !in_array($value['icon'], $icons)) {
+						$icons[] = $value['icon'];
+					}
+				}
+			}
+			$icons = apply_filters('jvbIncludeIcons', $icons);
+			$icons = $this->maybePrefixIcons($icons);
+			update_option(BASE.'includeIcons', $icons);
+		}
+
+		// Ensure icons are in the correct format (handle legacy data)
+		if (!$this->isIconsArrayPrefixed($icons)) {
+			$icons = $this->maybePrefixIcons($icons);
+			update_option(BASE.'includeIcons', $icons);
+		}
+
+		$additional = apply_filters('jvbIncludeIcons', []);
+		if (!empty($additional)) {
+			$additional = $this->maybePrefixIcons($additional);
+			$merged = $this->mergeUsedIcons($icons, $additional);
+
+			if ($icons != $merged) {
+				update_option(BASE.'includeIcons', $merged);
+				$icons = $merged;
+			}
+		}
+
+		foreach ($icons as $style => $theIcons) {
+			foreach($theIcons as $icon) {
+				$this->trackIconUsage($icon, $style);
+			}
+		}
+	}
+
+	/**
+	 * Check if icons array is in the prefixed format [style => [icons]]
+	 */
+	protected function isIconsArrayPrefixed(array $icons): bool
+	{
+		if (empty($icons)) {
+			return true;
+		}
+
+		// Check if first key is a valid style name
+		$first_key = array_key_first($icons);
+		if (!in_array($first_key, $this->styles)) {
+			return false;
+		}
+
+		// Check if first value is an array
+		return is_array($icons[$first_key]);
+	}
+
+	protected function maybePrefixIcons(array $icons):array
+	{
+		$out = [];
+		foreach ($icons as $icon) {
+			if (is_array($icon) && array_key_exists('style', $icon)) {
+				if (!array_key_exists($icon['style'], $out)) {
+					$out[$icon['style']] = [];
+				}
+				if (!in_array($icon['name'], $out[$icon['style']])) {
+					$out[$icon['style']][] = $icon['name'];
+				}
+			} elseif(is_array($icon)) {
+				$icon = $icon['name'];
+			}
+			if (!is_array($icon)) {
+				if (!array_key_exists($this->style, $out)) {
+					$out[$this->style] = [];
+				}
+				if (!in_array($icon, $out[$this->style])){
+					$out[$this->style][] = $icon;
+				}
+			}
+		}
+		return $out;
+	}
+
+	protected function addMap():void
+	{
+		$map = get_option(BASE.'iconMap');
+		if (!$map) {
+			$map = [
+				'seo'	=> 'robot'
+			];
+			if (Features::forSite()->has('referrals')){
+				$map['referrals'] = 'hand-heart';
+			}
+			if (Features::forSite()->has('dashboard')){
+				$map['dash'] = 'door';
+			}
+			if (Features::forSite()->has('magicLink')){
+				$map['magicLink'] = 'magic-wand';
+			}
+			if (Features::hasAnyIntegration()) {
+				$map['integrations'] = 'plugs-connected';
+			}
+
+
+			update_option(BASE.'iconMap', $map);
+		}
+
+		$this->map = apply_filters('jvbMapIcons', $map);
+	}
+
+	/**
+	 * Register WordPress hooks
+	 */
+	protected function registerHooks(): void
+	{
+		add_action('init', [$this, 'includeIcons'], 1);
+		add_action('init', [$this, 'checkCSS'], 10);
+		add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
+		add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']);
+	}
+
+	public function checkCSS():void
+	{
+//		update_option(BASE.'icons_needs_update', true);
+		if (get_option(BASE.'icons_needs_update', false)) {
+			error_log('Regenerating CSS');
+			delete_option(BASE.'icons_needs_update');
+			$this->regenerateCSS();
+		}
+	}
+
+	protected function regenerateCSS(): void
+	{
+		error_log('[IconsManager]:regenerateCSS');
+		$css_dir = JVB_CHILD_DIR.'/assets/css/';
+		if (!file_exists($css_dir)) {
+			wp_mkdir_p($css_dir);
+		}
+
+		// Generate CSS for each source
+		foreach ($this->usedIcons as $source => $styles) {
+			$css = $this->generateIconCSS($source);
+			$css_path = $css_dir . $source . '.css';
+
+			$this->archiveCurrentVersion($css, $source);
+
+			if (file_put_contents($css_path, $css) !== false) {
+				CacheManager::updateTimestamp('icons_' . $source);
+			} else {
+				error_log("[IconsManager] Could not write {$source}.css");
+			}
+		}
+	}
+
+	/**
+	 * Prevent cloning
+	 */
+	private function __clone() {}
+
+	/**
+	 * Prevent unserialization
+	 */
+	public function __wakeup()
+	{
+		throw new \Exception("Cannot unserialize singleton");
+	}
+
+	/**
+	 * Get an icon element
+	 *
+	 * @param string $name Icon name (e.g., 'heart', 'calendar')
+	 * @param array $options Options array:
+	 *   - 'style' => 'regular'|'bold'|'fill'|etc.
+	 *   - 'label' => 'Accessible label' (for standalone icons)
+	 *   - 'decorative' => true (for icons next to text)
+	 *   - 'class' => 'additional classes'
+	 *   - 'size' => 24 (for custom sizing via inline style)
+	 * @return string HTML icon element
+	 */
+	public function getIcon(string $name, array $options = []): string
+	{
+		$style = array_key_exists('style', $options) ? $options['style'] :$this->style;
+		$source = $options['source'] ?? 'icons';
+		$name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name;
+
+		// Validate icon exists
+		if (!$this->iconExists($name, $style)) {
+			error_log('[IconsManager] Icon not found: ' . $name);
+			return '';
+		}
+
+
+
+		// Track icon usage
+		$this->trackIconUsage($name, $style, $source);
+
+		$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
+		// Build classes
+		$classes = ['icon', 'icon-' . $name.$styleClass];
+		if (!empty($options['class'])) {
+			$classes[] = $options['class'];
+		}
+
+
+		$attrs = ['class="' . esc_attr(implode(' ', $classes)) . '"'];
+		$attrs[] = 'aria-hidden="true"';
+
+
+
+		return '<i ' . implode(' ', $attrs) . '></i>';
+	}
+
+	/**
+	 * Track icon usage for CSS generation
+	 */
+	protected function trackIconUsage(string $name, string $style, string $source = 'icons'): void
+	{
+		// Initialize source array if needed
+		if (!isset($this->usedIcons[$source])) {
+			$this->usedIcons[$source] = [];
+		}
+
+		// Initialize style array if needed
+		if (!isset($this->usedIcons[$source][$style])) {
+			$this->usedIcons[$source][$style] = [];
+		}
+
+		// Add icon if not already tracked
+		if (!in_array($name, $this->usedIcons[$source][$style])) {
+			$this->usedIcons[$source][$style][] = $name;
+			$needsUpdate = true;
+		}
+
+		if ($needsUpdate) {
+			$existing = get_option(BASE.'usedIcons', []);
+			$merged = $this->mergeUsedIcons($existing, $this->usedIcons);
+			update_option(BASE.'usedIcons', $merged);
+			update_option(BASE.'icons_needs_update', true);
+			$this->cache->delete('icon_styles_css');
+		}
+	}
+
+	/**
+	 * Check if icon file exists
+	 */
+	protected function iconExists(string $name, ?string $style = null): bool
+	{
+		if (!$style) {
+			$style = $this->style;
+		}
+		// Check custom icons first
+		if (array_key_exists($name, $this->customIcons)) {
+			return file_exists($this->customIcons[$name]);
+		}
+
+		// Check standard icons
+		$filepath = $this->buildFilePath($name, $style);
+		return file_exists($filepath);
+	}
+
+	/**
+	 * Build file path for icon
+	 */
+	protected function buildFilePath(string $name, ?string $style = null): string
+	{
+		if (!$style) {
+			$style = $this->style;
+		}
+		// Custom icons (absolute path provided)
+		if (array_key_exists($name, $this->customIcons)) {
+			return $this->customIcons[$name];
+		}
+
+		// Standard SVG icons in /assets/icons/
+		if (str_ends_with($name, '.svg')) {
+			return JVB_DIR . '/assets/icons/' . $name;
+		}
+		$name = ($style === 'regular') ? $name : $name . '-' . $style;
+
+		// Phosphor icons with style variants
+		return JVB_DIR . '/assets/phosphor-icons/' . $style . '/' . $name . '.svg';
+	}
+
+	/**
+	 * Get raw SVG content for CSS mask-image
+	 */
+	protected function getRawSvg(string $name, ?string $style = null): ?string
+	{
+		if (!$style) {
+			$style = $this->style;
+		}
+		$filepath = $this->buildFilePath($name, $style);
+
+		if (!file_exists($filepath)) {
+			return null;
+		}
+
+		$svg = file_get_contents($filepath);
+		if ($svg === false) {
+			return null;
+		}
+
+		// Clean up SVG for CSS usage
+		$svg = preg_replace("/([\n\t]+)/", ' ', $svg);
+		$svg = preg_replace('/>\s*</', '><', $svg);
+		$svg = trim($svg);
+
+		return $svg;
+	}
+
+
+	/**
+	 * Enqueue icon styles via REST endpoint
+	 */
+	public function enqueueIconStyles(): void
+	{
+		$timestamp = CacheManager::getTimestamp('icons');
+
+		wp_enqueue_style(
+			'jvb-icons',
+			JVB_CHILD_URL.'assets/css/icons.css',
+			[],
+			$timestamp
+		);
+	}
+
+	/**
+	 * Generate CSS from icon list
+	 */
+	protected function generateIconCSS(string $source = 'icons'): string
+	{
+		$css = '';
+
+		if (!isset($this->usedIcons[$source])) {
+			return $css;
+		}
+
+		foreach ($this->usedIcons[$source] as $style => $icons) {
+			$styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
+			foreach ($icons as $icon) {
+				$svg = $this->getEncodedSVG($icon, $style);
+				if ($svg !== '') {
+					$css .= ".icon-{$icon}{$styleClass}{";
+					$css .= "--icon:url('data:image/svg+xml;base64,{$svg}');";
+					$css .= "}";
+				}
+			}
+		}
+		return $this->minifyCss($css);
+	}
+
+	protected function mergeUsedIcons(array|bool $oldIcons = true, array|bool $newIcons = true):array
+	{
+		$set = false;
+		if ($oldIcons === true) {
+			$oldIcons = $this->usedIcons;
+			$set = true;
+		}
+		if ($newIcons === true) {
+			$history = $this->getVersionHistory();
+			$newIcons = (count($history) > 0) ? $history[0]['iconList'] : [];
+		}
+		foreach ($newIcons as $style => $icons) {
+			if (!isset($oldIcons[$style])) {
+				//Style  doesn't exist in previous set, add the whole thing
+				$oldIcons[$style] = $icons;
+			} else {
+				$oldIcons[$style] = array_unique(
+					array_merge($oldIcons[$style], $icons)
+				);
+			}
+		}
+		if ($set) {
+			$this->usedIcons = $oldIcons;
+			update_option(BASE.'usedIcons', $oldIcons);
+		}
+		return $oldIcons;
+	}
+
+	protected function minifyCSS(string $css): string
+	{
+		// Remove comments
+		$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
+		// Remove whitespace
+		$css = preg_replace('/\s+/', ' ', $css);
+		// Remove spaces around specific characters
+		$css = preg_replace('/\s*([:;{}])\s*/', '$1', $css);
+
+		return trim($css);
+	}
+	public function getCSSIcon(string $icon, ?string $style=null):string
+	{
+		if (!$style) {
+			$style = $this->style;
+		}
+		$svg = $this->getEncodedSVG($icon, $style);
+		if ($svg !== '') {
+			return "data:image/svg+xml;base64,{$svg}";
+		}
+		return '';
+	}
+	public function getEncodedSVG(string $icon, ?string $style = null):string
+	{
+		if (!$style) {
+			$style = $this->style;
+		}
+		return $this->cache->remember($style.$icon,
+		function () use ($icon, $style) {
+			$svg = $this->getRawSvg($icon, $style);
+			if ($svg) {
+				return base64_encode($svg);
+			}
+			return '';
+		});
+
+	}
+
+	/**
+	 * Clear icon cache (useful for development/debugging)
+	 */
+	public function clearIconCache(): void
+	{
+		delete_option(BASE . 'icon_usage_list'); // Clear DB option
+		delete_option(BASE.'usedIcons');
+		delete_option(BASE.'includeIcons');
+		delete_option(BASE.'iconMap');
+		$this->cache->delete('icon_styles_css');
+		CacheManager::updateTimestamp('icons');
+	}
+
+	protected function archiveCurrentVersion(string $css, string $source = 'icons'): void
+	{
+		$history = $this->getVersionHistory($source);
+
+		$icon_count = 0;
+		if (isset($this->usedIcons[$source])) {
+			foreach ($this->usedIcons[$source] as $style => $icons) {
+				$icon_count += count($icons);
+			}
+		}
+
+		$newEntry = [
+			'css' => $css,
+			'iconList' => $this->usedIcons[$source] ?? [],
+			'timestamp' => time(),
+			'icon_count' => $icon_count,
+			'size' => strlen($css),
+			'size_formatted' => size_format(strlen($css), 2)
+		];
+
+		array_unshift($history, $newEntry);
+
+		if (count($history) > self::MAX_VERSIONS) {
+			$history = array_slice($history, 0, self::MAX_VERSIONS);
+		}
+
+		update_option(BASE.'icon_css_history_' . $source, $history);
+	}
+
+	public function getVersionHistory(string $source = 'icons'): array
+	{
+		return get_option(BASE.'icon_css_history_' . $source, []);
+	}
+
+
+	public function restoreVersion(int $timestamp): bool
+	{
+		$history = $this->getVersionHistory();
+
+		foreach ($history as $entry) {
+			if ($entry['timestamp'] === $timestamp) {
+				$css_path = JVB_DIR . '/assets/css/icons.css';
+
+				// Archive current before restoring
+				$current_css = file_get_contents($css_path);
+				if ($current_css !== false) {
+					$this->archiveCurrentVersion($current_css);
+				}
+
+				// Restore the version
+				if (file_put_contents($css_path, $entry['css']) !== false) {
+					$this->usedIcons = $entry['iconList'];
+					update_option(BASE.'usedIcons', $this->usedIcons);
+					CacheManager::updateTimestamp('icons');
+					return true;
+				}
+
+				return false;
+			}
+		}
+
+		error_log("[IconsManager] Version {$timestamp} not found in history");
+		return false;
+	}
+
+	public function forceRefresh(): void
+	{
+		$this->clearIconCache();
+		update_option(BASE.'icons_needs_update', true);
+		CacheManager::updateTimestamp('icons');
+	}
+
+	public function mergeVersions(array $timestamps): bool
+	{
+		if (empty($timestamps)) {
+			return false;
+		}
+
+		$history = get_option(BASE.'icon_css_history', []);
+		$merged_icons = [];
+		// Collect icons from selected versions
+		foreach ($history as $entry) {
+			if (in_array($entry['timestamp'], $timestamps)) {
+				foreach ($entry['iconList'] as $style => $icons) {
+					if (!isset($merged_icons[$style])) {
+						$merged_icons[$style] = [];
+					}
+					// Merge and keep unique
+					$merged_icons[$style] = array_unique(
+						array_merge($merged_icons[$style], $icons)
+					);
+				}
+			}
+		}
+
+		if (empty($merged_icons)) {
+			error_log('[IconsManager] No icons found in selected versions');
+			return false;
+		}
+
+		// Archive current version
+		$current_css = file_get_contents(JVB_DIR . '/assets/css/icons.css');
+		if ($current_css !== false) {
+			$this->archiveCurrentVersion($current_css);
+		}
+
+		// Update used icons and regenerate
+		$this->usedIcons = $merged_icons;
+		update_option(BASE.'usedIcons', $this->usedIcons);
+
+		// Force regeneration
+		$this->regenerateCSS();
+
+		return true;
+	}
+}
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 525a53f..f336e23 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -17,10 +17,7 @@
 class LoginManager
 {
 	protected Features $siteFeatures;
-	protected ?MagicLinkManager $magicLink = null;
 	protected ?MetaForm $metaForm = null;
-	protected EmailManager $emailManager;
-	protected AjaxRateLimiter $rateLimiter;
 	protected CacheManager $cache;
 
 
@@ -44,7 +41,6 @@
 	public function __construct()
 	{
 		$this->siteFeatures = Features::forSite();
-		$this->emailManager = new EmailManager();
 
 
 		$this->cache = CacheManager::for('login');
@@ -67,8 +63,10 @@
 		// Login success handling
 		add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
 
+		add_filter( 'login_url', [$this, 'loginUrl'], 10, 3 );
 		// Allow other features to register handlers
 		do_action('jvbLoginManagerInit', $this);
+		add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
 	}
 
 	/**************************************************************************
@@ -90,7 +88,7 @@
 			return;
 		}
 		// Build custom login URL with all query args
-		$custom_login_page = home_url('/login');
+		$custom_login_page = home_url('/login/');
 		$query_args = $_GET;
 
 		// Remove WordPress internal args
@@ -287,6 +285,18 @@
 			}
 		}
 	}
+	public function loginUrl(string $login_url, string $redirect, bool $force_reauth):string
+	{
+		// This will append /custom-login/ to you main site URL as configured in general settings (ie https://domain.com/custom-login/)
+		$login_url = site_url( '/login/', 'login' );
+		if ( ! empty( $redirect ) ) {
+			$login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url );
+		}
+		if ( $force_reauth ) {
+			$login_url = add_query_arg( 'reauth', '1', $login_url );
+		}
+		return $login_url;
+	}
 	public function getLoginPage():int|false
 	{
 		return (int)get_option(BASE.'login_page');
@@ -308,7 +318,6 @@
 		if (!Features::forSite()->has('magicLink')) {
 			return;
 		}
-		$this->magicLink = new MagicLinkManager();
 	}
 
 	/*********************************************************************
@@ -597,7 +606,8 @@
             $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
             $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
             echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
-                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
+    				<span class="screen-reader-text">Toggle dark mode</span>
+                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
 					jvbIcon('sun-dim', ['title'=> 'Light Mode']).
 					jvbIcon('moon', ['title'=>'Dark Mode']).
 					'</span></label>';
@@ -812,7 +822,7 @@
 
 	protected function maybeMagicLink(): void
 	{
-		if (!$this->magicLink || !in_array($this->action, ['login', 'lostpassword'])) {
+		if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) {
 			return;
 		}
 		?>
@@ -883,7 +893,7 @@
 					method: 'POST',
 					headers: {
                 		'Content-Type': 'application/json',
-						'X-WP-Nonce': jvbSettings.nonce
+						'X-WP-Nonce': window.auth.getNonce()
 					},
 					body: JSON.stringify(realFormData)
 				});
@@ -905,11 +915,16 @@
 						window.LoginController.handleFormSuccess(form, result);
 					}
 
+					if (window.auth && typeof window.auth.handleLogin === 'function' && Object.hasOwn(result, 'auth')) {
+						console.log('Awaiting Auth...');
+						await window.auth.handleLogin(result.auth); // Pass the full result
+					}
+
 					// Handle redirect
 					if (result.redirect) {
 						setTimeout(() => {
 							window.location.href = result.redirect;
-						}, 500); // Brief delay to show success message
+						}, 200); // Brief delay to show success message
 					}
 
 				} catch (error) {
@@ -962,6 +977,11 @@
 		wp_safe_redirect($login_url);
 		exit;
 	}
+
+	public function saveRegistrationFields(int $user_id, array $userdata):void
+	{
+
+	}
 }
 
 // Initialize the login manager
diff --git a/inc/managers/MagicLinkManager.php b/inc/managers/MagicLinkManager.php
index 466d3b9..880fe9c 100644
--- a/inc/managers/MagicLinkManager.php
+++ b/inc/managers/MagicLinkManager.php
@@ -17,7 +17,7 @@
 class MagicLinkManager
 {
 	protected CacheManager $cache;
-	protected EmailManager $email;
+	protected CacheManager $referral_cache;
 
 	// Token settings
 	protected int $token_expiry = 900; // 15 minutes in seconds
@@ -33,7 +33,7 @@
 	public function __construct()
 	{
 		$this->cache = CacheManager::for('magic_links', $this->token_expiry);
-		$this->email = new EmailManager();
+		$this->referral_cache = CacheManager::for('referral_magic_links', 14 * DAY_IN_SECONDS);
 
 		// Hook into WordPress auth flow
 		add_action('template_redirect', [$this, 'handleMagicLinkClick']);
@@ -95,7 +95,12 @@
 			'created' => time()
 		], $data);
 
-		$this->cache->set($token, $token_data);
+		// Use longer expiry for referral tokens
+		if ($type === self::TYPE_REFERRAL) {
+			$this->referral_cache->set($token, $token_data);
+		} else {
+			$this->cache->set($token, $token_data);
+		}
 
 		return $token;
 	}
@@ -105,9 +110,15 @@
 	 */
 	public function verifyToken(string $token, string $email): array|WP_Error
 	{
+		// Try regular cache first, then referral cache
 		$token_data = $this->cache->get($token);
 
 		if (!$token_data) {
+			$token_data = $this->referral_cache->get($token);
+		}
+
+		if (!$token_data) {
+			error_log('Token not found. Checking cache stats...');
 			return new WP_Error('invalid_token', 'Invalid or expired token');
 		}
 
@@ -116,7 +127,12 @@
 		}
 
 		// Delete token after verification (single use)
-		$this->cache->delete($token);
+		// Check which cache it's in and delete from the correct one
+		if ($token_data['type'] === 'referral') {
+			$this->referral_cache->delete($token);
+		} else {
+			$this->cache->delete($token);
+		}
 
 		return $token_data;
 	}
@@ -180,7 +196,7 @@
 		$subject = 'Sign in to ' . get_bloginfo('name');
 		$message = $this->getLoginEmailTemplate($user->display_name, $magic_url);
 
-		$sent = $this->email->sendEmail($email, $subject, $message, 'Log in to '. get_bloginfo('name'));
+		$sent = JVB()->email()->sendEmail($email, $subject, $message, 'Log in to '. get_bloginfo('name'));
 
 		return $sent ? true : new WP_Error('email_failed', 'Failed to send magic link');
 	}
@@ -212,7 +228,7 @@
 		$subject = 'Complete your ' . get_bloginfo('name') . ' registration';
 		$message = $this->getSignupEmailTemplate($context['name'] ?? '', $magic_url);
 
-		$sent = $this->email->sendEmail($email, $subject, $message, 'Complete Registration');
+		$sent = JVB()->email()->sendEmail($email, $subject, $message, 'Complete Registration');
 
 		return $sent ? true : new WP_Error('email_failed', 'Failed to send signup link');
 	}
@@ -229,7 +245,8 @@
 		$token_data = [
 			'referral_code' => $context['referral_code'],
 			'name' => $context['name'] ?? '',
-			'role' => $context['role'] ?? 'subscriber'
+			'role' => $context['role'] ?? 'subscriber',
+			'email'	=> $email
 		];
 
 		$token = $this->generateToken($email, self::TYPE_REFERRAL, $token_data);
@@ -243,10 +260,10 @@
 		$referrer_name = $context['referrer_name'] ?? 'A friend';
 		$reward_text = $context['reward_text'] ?? '';
 
-		$subject = $referrer_name . ' invited you to join ' . get_bloginfo('name');
-		$message = $this->getReferralEmailTemplate($context['name'] ?? '', $referrer_name, $magic_url, $reward_text);
+		$subject = (array_key_exists('subject', $context) && $context['subject'] !== '') ? $context['subject'] : $referrer_name . ' invited you to join ' . get_bloginfo('name');
+		$message = $this->getReferralEmailTemplate($context['name'] ?? '', $referrer_name, $magic_url, $reward_text, $context);
 
-		$sent = $this->email->sendEmail($email, $subject, $message, 'Accept Invitation');
+		$sent = JVB()->email()->sendEmail($email, $subject, $message, 'Accept Invitation');
 
 		return $sent ? true : new WP_Error('email_failed', 'Failed to send referral link');
 	}
@@ -274,7 +291,7 @@
 		$subject = 'Reset your password';
 		$message = $this->getResetEmailTemplate($user->display_name, $magic_url);
 
-		$sent = $this->email->sendEmail($email, $subject, $message, 'Reset Password');
+		$sent = JVB()->email()->sendEmail($email, $subject, $message, 'Reset Password');
 
 		return $sent ? true : new WP_Error('email_failed', 'Failed to send reset link');
 	}
@@ -290,7 +307,7 @@
 
 		$action = sanitize_text_field($_GET['action']);
 		$token = sanitize_text_field($_GET['magic_token']);
-		$email = sanitize_email($_GET['email']);
+		$email = sanitize_email(rawurldecode($_GET['email']));
 
 		if (!in_array($action, ['magic_login', 'magic_signup', 'magic_referral', 'magic_reset'])) {
 			return;
@@ -350,6 +367,10 @@
 	 */
 	protected function processSignup(array $token_data): void
 	{
+		if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) {
+			JVB()->error()->log('[MagicLinkManager]Could not process Signup');
+			return;
+		}
 		$user_id = wp_create_user(
 			$token_data['email'],
 			wp_generate_password(20, true, true),
@@ -390,47 +411,43 @@
 	/**
 	 * Process referral signup via magic link
 	 */
+	/**
+	 * Process referral signup via magic link
+	 */
 	protected function processReferralSignup(array $token_data): void
 	{
-		$user_id = wp_create_user(
-			$token_data['email'],
-			wp_generate_password(20, true, true),
-			$token_data['email']
-		);
-
-		if (is_wp_error($user_id)) {
-			wp_die('Failed to create account: ' . $user_id->get_error_message());
+		if (!array_key_exists('email', $token_data) || !array_key_exists('name', $token_data)) {
+			JVB()->error()->log('[MagicLinkManager]Could not process Referral Signup');
+			return;
 		}
 
-		if (!empty($token_data['name'])) {
-			wp_update_user([
-				'ID' => $user_id,
-				'display_name' => $token_data['name'],
-				'first_name' => $token_data['name']
-			]);
+		$email = sanitize_email($token_data['email']);
+		if (email_exists($email)) {
+			wp_die('Looks like you already have an account!');
 		}
-
-		// Store referral code for ReferralManager
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
+		$role = JVB()->referrals()->getRole();
+		$pass = wp_generate_password(20, true, true);
+		$name = sanitize_text_field($token_data['name']);
+		$user_id = wp_insert_user([
+			'user_login' 	=> $email,
+			'user_email' 	=> $email,
+			'user_pass'		=> $pass,
+			'display_name'	=> $name,
+			'role'			=> $role
+		]);
+		if (!is_wp_error($user_id)) {
+			$response = JVB()->routes('login')->login($email, $pass, true);
+			if ($response) {
+				wp_safe_redirect(home_url('/dash?welcome=1&referral=1'));
+				exit;
+			}
+		} else {
+			JVB()->error()->log(
+				'[MagicLinkManager]',
+				$user_id->get_error_message(),
+				$token_data
+			);
 		}
-		$_SESSION[BASE . 'referral_code'] = $token_data['referral_code'];
-		setcookie(
-			BASE . 'referral_code',
-			$token_data['referral_code'],
-			time() + (86400 * 30),
-			'/'
-		);
-
-		$user = get_user_by('ID', $user_id);
-		wp_set_current_user($user_id);
-		wp_set_auth_cookie($user_id, true);
-
-		do_action('user_register', $user_id);
-		do_action('wp_login', $user->user_login, $user);
-
-		wp_safe_redirect(home_url('/dash?welcome=1&referral=1'));
-		exit;
 	}
 
 	/**
@@ -492,10 +509,11 @@
 	{
 		$content = '<h2>Hey ' . esc_html($name) . '!</h2>';
 		$content .= '<p>Click the button below to sign in to your account. This link expires in 15 minutes.</p>';
-		$content .= '<p style="text-align: center; margin: 30px 0;">';
-		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Sign In</a>';
-		$content .= '</p>';
-		$content .= '<p style="color: #666; font-size: 14px;">If you didn\'t request this, you can safely ignore this email.</p>';
+		$content .= JVB()->email()->button($magic_url, 'Sign In');
+		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
+		$content .= JVB()->email()->link($magic_url);
+		$content .= '<p>If you didn\'t request this, you can safely ignore this email. The link will expire in 15 minutes.</p>';
+		$content .= JVB()->email()->signature();
 
 		return $content;
 	}
@@ -504,27 +522,32 @@
 	{
 		$content = '<h2>Welcome' . ($name ? ', ' . esc_html($name) : '') . '!</h2>';
 		$content .= '<p>You\'re almost there! Click the button below to complete your registration and access your account.</p>';
-		$content .= '<p style="text-align: center; margin: 30px 0;">';
-		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Complete Registration</a>';
-		$content .= '</p>';
-		$content .= '<p style="color: #666; font-size: 14px;">This link expires in 15 minutes.</p>';
+		$content .= JVB()->email()->button($magic_url, 'Complete Registration');
+		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
+		$content .= JVB()->email()->link($magic_url);
+		$content .= '<p>This link expires in 15 minutes.</p>';
+		$content .= JVB()->email()->signature();
 
 		return $content;
 	}
 
-	protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text): string
+	protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text, array $context): string
 	{
 		$content = '<h2>Hey' . ($name ? ' ' . esc_html($name) : '') . '!</h2>';
 		$content .= '<p><strong>' . esc_html($referrer_name) . '</strong> thinks you\'d love ' . get_bloginfo('name') . '!</p>';
 
+		if (array_key_exists('message', $context) && $context['message']!== '') {
+			$content .= wpautop($context['message']);
+		}
 		if ($reward_text) {
 			$content .= '<p>' . esc_html($reward_text) . '</p>';
 		}
 
-		$content .= '<p style="text-align: center; margin: 30px 0;">';
-		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Join Now</a>';
-		$content .= '</p>';
-		$content .= '<p style="color: #666; font-size: 14px;">This link expires in 15 minutes.</p>';
+		$content .= JVB()->email()->button($magic_url, 'Join Now');
+		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
+		$content .= JVB()->email()->link($magic_url);
+		$content .= '<p>This link expires in 14 days.</p>';
+		$content .= JVB()->email()->signature();
 
 		return $content;
 	}
@@ -533,13 +556,11 @@
 	{
 		$content = '<h2>Hey ' . esc_html($name) . '!</h2>';
 		$content .= '<p>We received a request to reset your password. Click the button below to sign in and update your password.</p>';
-		$content .= '<p style="text-align: center; margin: 30px 0;">';
-		$content .= '<a href="' . $magic_url . '" style="background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Reset Password</a>';
-		$content .= '</p>';
-		$content .= '<p style="color: #666; font-size: 14px;">If you didn\'t request this, you can safely ignore this email. This link expires in 15 minutes.</p>';
-
+		$content .= JVB()->email()->button($magic_url, 'Reset Password');
+		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
+		$content .= JVB()->email()->link($magic_url);
+		$content .= '<p>If you didn\'t request this, you can safely ignore this email. This link expires in 15 minutes.</p>';
+		$content .= JVB()->email()->signature();
 		return $content;
 	}
 }
-
-new MagicLinkManager();
diff --git a/inc/managers/NotificationManager.php b/inc/managers/NotificationManager.php
index db4a598..1554ad6 100644
--- a/inc/managers/NotificationManager.php
+++ b/inc/managers/NotificationManager.php
@@ -1030,7 +1030,7 @@
         };
 
         // Send the email
-        return jvbMail($user->user_email, $subject, $content, $header);
+        return JVB()->email()->sendEmail($user->user_email, $subject, $content, $header);
     }
 
     /**
diff --git a/inc/managers/OperationQueue.php b/inc/managers/OperationQueue.php
index 4e07c6f..1d7d67c 100644
--- a/inc/managers/OperationQueue.php
+++ b/inc/managers/OperationQueue.php
@@ -1180,7 +1180,7 @@
 
         $message .= "Please check the error logs for more details.";
 
-        return jvbMail($admin_email, $subject, $message);
+        return JVB()->email()->sendEmail($admin_email, $subject, $message);
     }
 
     /**
@@ -1767,7 +1767,7 @@
         $message .= "This is an automated report. Please check the admin dashboard for more details.";
 
         // Send email
-        jvbMail($admin_email, $subject, $message);
+		JVB()->email()->sendEmail($admin_email, $subject, $message);
     }
 
     /**
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 3f000e4..2f7468a 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -4,6 +4,8 @@
 use JVBase\managers\MagicLinkManager;
 use JVBase\integrations\Cloudflare;
 use JVBase\meta\MetaForm;
+use JVBase\ui\CRUDSkeleton;
+use JVBase\ui\Tabs;
 use JVBase\utility\Features;
 use WP_User;
 use WP_Error;
@@ -34,9 +36,12 @@
 		'referrer_reward_type'	=> 'fixed',
 		'referee_reward_type' => 'percentage',  // 'percentage' or 'fixed'
 		'referee_reward_amount' => 20,  // 20% or $20
-		'referee_reward_applies_to' => 'first_order'  // 'first_order' or 'all_orders'
+		'referee_reward_applies_to' => 'first_order',  // 'first_order' or 'all_orders'
+		'referral_role'	=> BASE.'client'
 	];
 
+	protected string $role = BASE.'client';
+
 	protected array $settings;
 
 	public function __construct()
@@ -46,7 +51,6 @@
 		$this->cache = CacheManager::for('referrals', WEEK_IN_SECONDS);
 		$this->referrals_table = $wpdb->prefix . BASE . 'referrals';
 		$this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards';
-		$this->magic_link = new MagicLinkManager();
 
 		$this->referralPage = $this->getReferralPageId();
 		$this->settings = $this->getRewardSettings();
@@ -88,6 +92,14 @@
 		add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3);
 	}
 
+	public function getSettings():array
+	{
+		return $this->settings;
+	}
+	public function getRole():string
+	{
+		return $this->role;
+	}
 	public function addLoginInputs(string $action):void
 	{
 		if (array_key_exists('ref', $_GET)) {
@@ -126,11 +138,16 @@
 			'jvb-a11y',
 			'jvb-popup',
 			'jvb-tabs',
+			'jvb-data-store',
 		];
 
 		if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) {
 			$requirements[] = 'cloudflare-turnstile';
 		}
+		if (is_singular(BASE.'dash')) {
+			$requirements[] = 'jvb-form';
+			$requirements[] = 'jvb-view';
+		}
 		wp_enqueue_script(
 			'jvb-referral',
 			JVB_URL . 'assets/js/min/referral.min.js',
@@ -267,16 +284,15 @@
 	 * Track a new referral when user registers
 	 *
 	 * @param int $user_id
+	 * @return bool;
 	 */
-	public function processReferral(int $user_id, string $email, array $data): void
+	public function processReferral(int $user_id): bool
 	{
-		// Check if user was created via referral magic link
-		// Try to get code from multiple sources
-    	$referral_code = $data['referral_code'] ??
-		get_user_meta($user_id, BASE . 'pending_referral_code', true);
+		// Try to get code from user meta first (set during registration)
+		$referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true);
 
-		// Check session/cookie if not in data
 		if (empty($referral_code)) {
+			// Check session/cookie if not in meta
 			if (session_status() === PHP_SESSION_NONE) {
 				session_start();
 			}
@@ -284,34 +300,63 @@
 		}
 
 		if (empty($referral_code)) {
-			return;
+			return false; // No referral code - regular registration
 		}
 
 		// Find the referrer
 		$referrer = $this->getUserByReferralCode($referral_code);
-
 		if (!$referrer) {
 			delete_user_meta($user_id, BASE . 'pending_referral_code');
-			return;
+			return false;
 		}
 
-		// Check for duplicates
-		$existing = $this->getReferralByReferee($user_id);
-		if ($existing) {
-			delete_user_meta($user_id, BASE . 'pending_referral_code');
-			return;
+		$user = get_userdata($user_id);
+
+		// Check if referral already exists for this user
+		$existing = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table}
+		WHERE referrer_id = %d AND (referee_email = %s OR referee_id = %d)",
+			$referrer->ID,
+			$user->user_email,
+			$user_id
+		));
+
+		if (!$existing) {
+			// Create new referral record - referred_at captures registration time
+			$this->wpdb->insert(
+				$this->referrals_table,
+				[
+					'referrer_id' => $referrer->ID,
+					'referee_id' => $user_id,
+					'referee_name' => $user->display_name,
+					'referee_email' => $user->user_email,
+					'referee_phone' => get_user_meta($user_id, BASE . 'phone', true) ?: '',
+					'referral_code' => $referral_code,
+					'status' => 'pending', // pending first treatment
+					'referred_at' => current_time('mysql') // When they registered
+				],
+				['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
+			);
 		}
 
-		// Create referral record
-		$result = $this->createReferral($referrer->ID, $user_id, $referral_code);
-
-		if ($result) {
-			// Clean up temp meta
-			delete_user_meta($user_id, BASE . 'pending_referral_code');
-
-			// Fire action for tracking
-			do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code);
+		// Clean up temp data
+		delete_user_meta($user_id, BASE . 'pending_referral_code');
+		if (isset($_SESSION[BASE . 'referral_code'])) {
+			unset($_SESSION[BASE . 'referral_code']);
 		}
+		if (isset($_COOKIE[BASE . 'referral_code'])) {
+			setcookie(BASE . 'referral_code', '', time() - 3600, '/');
+		}
+
+		// Clear caches
+		$this->cache->clear();
+
+		// Fire action for tracking
+		do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code);
+
+		// Send notification to referrer
+		$this->sendReferrerNotification($referrer->ID, $user->display_name);
+		return true;
 	}
 
 	/**
@@ -468,28 +513,52 @@
 	 */
 	public function getUserReferrals(int $user_id, array $args = []): array
 	{
-		$defaults = [
-			'status' => 'all',
-			'limit' => 100,
-			'offset' => 0,
-			'orderby' => 'referred_at',
-			'order' => 'DESC'
-		];
+		return $this->cache->remember(
+			$user_id,
+			function() use ($user_id, $args) {
+				$defaults = [
+					'status' => 'all',
+					'limit' => 100,
+					'offset' => 0,
+					'orderby' => 'referred_at',
+					'order' => 'DESC'
+				];
 
-		$args = wp_parse_args($args, $defaults);
+				$args = wp_parse_args($args, $defaults);
 
-		$where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id);
+				$where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id);
 
-		if ($args['status'] !== 'all') {
-			$where .= $this->wpdb->prepare(" AND status = %s", $args['status']);
-		}
+				if ($args['status'] !== 'all') {
+					$where .= $this->wpdb->prepare(" AND status = %s", $args['status']);
+				}
 
-		$query = "SELECT * FROM {$this->referrals_table}
+				$query = "SELECT * FROM {$this->referrals_table}
                   {$where}
                   ORDER BY {$args['orderby']} {$args['order']}
                   LIMIT {$args['limit']} OFFSET {$args['offset']}";
 
-		return $this->wpdb->get_results($query);
+				$results =  $this->wpdb->get_results($query);
+
+				return array_map(function($referral) {
+					$last_invite = get_transient('referral_last_invite_' . md5($referral->referee_email));
+					$can_resend = !$last_invite || (time() - $last_invite) > WEEK_IN_SECONDS;
+					$status = match($referral->status) {
+						'consulted' => 'Awaiting Treatment',
+						'treated'	=> 'Rewarded!',
+						default => 'Pending',
+					};
+					return [
+						'id'			=> $referral->id,
+						'referee_name'	=> $referral->referee_name,
+						'referee_email'	=> $referral->referee_email,
+						'referred_at'	=> JVB()->routes('referral')->formatTimestamp($referral->referred_at),
+						'referral_status'=> $status,
+						'can_resend'	=> $can_resend
+					];
+				}, $results);
+			}
+		);
+
 	}
 
 	/**
@@ -509,26 +578,25 @@
 
 		$stats = $this->wpdb->get_row($this->wpdb->prepare(
 			"SELECT
-                COUNT(*) as total_referrals,
-                SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count,
-                SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count
-            FROM {$this->referrals_table}
-            WHERE referrer_id = %d",
+			COUNT(*) as code_used,
+			SUM(CASE WHEN status IN ('consulted', 'treated') THEN 1 ELSE 0 END) as consultations,
+			SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treatments,
+			SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending
+		FROM {$this->referrals_table}
+		WHERE referrer_id = %d",
 			$user_id
 		), ARRAY_A);
 
-		// Get total rewards
-		$rewards = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT
-                SUM(CASE WHEN status = 'available' THEN amount ELSE 0 END) as available_rewards,
-                SUM(CASE WHEN status = 'redeemed' THEN amount ELSE 0 END) as redeemed_rewards
-            FROM {$this->rewards_table}
-            WHERE user_id = %d AND reward_type = 'referrer'",
+		// Get total rewards earned (available + redeemed)
+		$rewards = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT SUM(amount)
+		FROM {$this->rewards_table}
+		WHERE user_id = %d AND reward_type = 'referrer'",
 			$user_id
-		), ARRAY_A);
+		));
 
-		$stats = array_merge($stats, $rewards);
-
+		$stats['total_rewards'] = floatval($rewards ?? 0);
+		$stats['user_id'] = $user_id;
 		$this->cache->set($cache_key, $stats, HOUR_IN_SECONDS);
 
 		return $stats;
@@ -638,7 +706,7 @@
 			count($new_referrals) !== 1 ? 's' : '');
 
 
-		jvbMail($to, $subject, $content);
+		JVB()->email()->sendEmail($to, $subject, $content);
 	}
 
 	/**
@@ -661,7 +729,7 @@
 
 		$message = $this->generateWeeklyReportEmail($top_referrers, $total_referrals);
 
-		wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8']);
+		JVB()->email()->sendEmail($to, $subject, $message);
 	}
 
 	/**
@@ -883,6 +951,7 @@
 		</table>
 	<?php endif; ?>
 
+		<?php /**
 		<script>
 			function markReferralTreated(referralId) {
 				if (!confirm('Mark this referral as treated? This will create reward records.')) {
@@ -907,6 +976,7 @@
 			}
 		</script>
 		<?php
+	 */
 	}
 
 	/**
@@ -963,7 +1033,7 @@
 	{
 
 		$user_id = get_current_user_id();
-		$content = '<aside class="jvb-referral right">';
+		$content = '<aside class="main referral right">';
 		if (!$user_id) {
 			$content .= $this->getUnloggedInReferral();
 		} else {
@@ -1007,7 +1077,9 @@
 		' . ($referrer_name ? '<p>' . esc_html($referrer_name) . ' invited you to join us</p>' : '') . '
 	</div>
 	<form id="referral-code-form">
-				'.jvbFormStatus().$meta->return('referral_name', null, [
+				'.jvbFormStatus(). '
+    <input type="hidden" name="user_select" value="' . esc_attr(get_option(BASE.'referral_role','client')) . '">
+    ' .$meta->return('referral_name', null, [
 				'required'	=> true,
 				'type'		=> 'text',
 				'label'		=> 'Your Name',
@@ -1158,20 +1230,24 @@
 
 		<div class="copy-section">
 			<h4>Your Referral Link</h4>
-			<div class="copy-group">
+			<div class="copy-group row btw nowrap">
 				<code id="referral-link" class="copy-target"><?= esc_url($share_url) ?></code>
 				<button type="button" class="copy-btn" data-target="referral-link" aria-label="Copy referral link">
 					<?php echo jvbIcon('copy', ['size' => 16]); ?>
 				</button>
 			</div>
+			<p class="hint">Quickest and easiest: autofills your code.</p>
+
 
 			<h4>Your Code</h4>
-			<div class="copy-group">
+			<div class="copy-group row btw nowrap">
 				<code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code>
 				<button type="button" class="copy-btn" data-target="referral-code" aria-label="Copy referral code">
 					<?php echo jvbIcon('copy', ['size' => 16]); ?>
 				</button>
 			</div>
+			<p class="hint">Manually copy and paste the code</p>
+
 		</div>
 
 		<div class="recent-referrals-section">
@@ -1203,6 +1279,7 @@
 		<a href="<?= get_home_url(null, '/dash/referrals')?>" class="view-dashboard-btn">
 			Dashboard <?= jvbIcon('arrow-right', ['size' => 16]); ?>
 		</a>
+		<p class="hint">Bulk-invite your friends via email - the link will pre-fill their name, email, and code!</p>
 
 		<?php
 		return ob_get_clean();
@@ -1244,8 +1321,8 @@
 			<p>Or click the button below:</p>
 			%s
 			</div>',
-				jvbEmailLink($code),
-				jvbMailButton($share_url, 'Share Your Code')
+				JVB()->email()->link($code),
+				JVB()->email()->button($share_url, 'Share Your Code')
 			);
 		}
 
@@ -1257,10 +1334,9 @@
 	{
 		return add_query_arg(
 			[
-				'ref' => $code,
-				'action'	=> 'register'
+				'ref' => $code
 			],
-			wp_login_url()
+			get_home_url()
 		);
 	}
 
@@ -1270,15 +1346,12 @@
 	 * @param int $user_id Referrer's user ID
 	 * @param string $invitee_email Email of person to invite
 	 * @param string $invitee_name Name of person to invite
+	 * @param string $subject
+	 * @param string $message
 	 * @return array|WP_Error Result with success/error
 	 */
-	public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name):array|WP_Error
+	public function sendReferralInvitation(int $user_id, string $invitee_email, string $invitee_name, string $subject, string $message):array|WP_Error
 	{
-		// Verify user exists
-		if (!$this->checkUser($user_id)) {
-			return new WP_Error('invalid_user', 'Invalid user ID');
-		}
-
 		// Check email rate limit (15/hour)
 		$rate_check = $this->checkEmailRateLimit($user_id);
 		if ($rate_check !== true) {
@@ -1291,11 +1364,7 @@
 			return new WP_Error('invalid_email', 'Invalid email address');
 		}
 
-		// Check if this email has already been invited or registered
-		if ($this->isEmailInvited($invitee_email)) {
-			return new WP_Error('already_invited', 'This person has already been invited');
-		}
-
+		// Check if already registered
 		if (email_exists($invitee_email)) {
 			return new WP_Error('user_exists', 'This person already has an account');
 		}
@@ -1308,29 +1377,56 @@
 			return $referral_code;
 		}
 
-		// Get reward text for email
-		$reward_text = $this->settings['referee_reward_type'] === 'percentage'
-			? "Get {$this->settings['referee_reward_amount']}% off your first treatment!"
-			: "Get \${$this->settings['referee_reward_amount']} off your first treatment!";
-
-		// Record the invitation attempt (for tracking)
+		// Record the invitation attempt (for rate limiting only)
 		$this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name);
 
-		// Send magic link via MagicLinkManager
-		$result = $this->magic_link->sendMagicLink(
+		// Create registration URL with token (opens sidebar with prefilled form)
+		$token_data = [
+			'name' => sanitize_text_field($invitee_name),
+			'email' => $invitee_email,
+			'expires' => time() + (30 * DAY_IN_SECONDS)
+		];
+
+		// Encode the token
+		$token = base64_encode(json_encode($token_data));
+		$registration_url = add_query_arg([
+			'ref' => $referral_code,
+			'rname' => sanitize_text_field($invitee_name),
+			'remail'=> rawurlencode($invitee_email),
+		], home_url('/'));
+
+		// Get reward text for email
+		$reward_text = $this->settings['referee_reward_type'] === 'percentage'
+			? "{$this->settings['referee_reward_amount']}% off"
+			: "\${$this->settings['referee_reward_amount']} off";
+
+		// Build email content
+		$email_content =
+			sprintf(
+				'<h2>%s invited you to %s!</h2>
+			<p>%s</p>
+			<div class="callout">
+				<h3>Get %s your first treatment!</h3>
+			</div>
+			<p>Click the button below to register and claim your reward:</p>
+			%s
+			<p><small>This invitation expires in 30 days.</small></p>',
+				esc_html($referrer->display_name),
+				esc_html(get_bloginfo('name')),
+				nl2br(esc_html($message)),
+				esc_html($reward_text),
+				JVB()->email()->button($registration_url, 'Register & Get Your Reward')
+			);
+
+		// Send email
+		$sent = JVB()->email()->sendEmail(
 			$invitee_email,
-			MagicLinkManager::TYPE_REFERRAL,
-			[
-				'name' => sanitize_text_field($invitee_name),
-				'referral_code' => $referral_code,
-				'referrer_id' => $user_id,
-				'referrer_name' => $referrer->display_name,
-				'reward_text' => $reward_text
-			]
+			$subject,
+			$email_content
 		);
 
-		if (is_wp_error($result)) {
-			return $result;
+		if (!$sent) {
+			return new WP_Error('email_failed', 'Failed to send invitation email');
 		}
 
 		return [
@@ -1348,7 +1444,7 @@
 	 * @param array $invitations Array of ['email' => '', 'name' => '']
 	 * @return array Results with success/failed arrays
 	 */
-	public function sendBatchReferralInvitations(int $user_id, array $invitations): array
+	public function sendBatchReferralInvitations(int $user_id, array $invitations, string $subject, string $message): array
 	{
 		$results = [
 			'success' => [],
@@ -1368,7 +1464,7 @@
 				continue;
 			}
 
-			$result = $this->sendReferralInvitation($user_id, $email, $name);
+			$result = $this->sendReferralInvitation($user_id, $email, $name, $subject, $message);
 
 			if (is_wp_error($result)) {
 				$results['failed'][] = [
@@ -1389,7 +1485,7 @@
 
 		return [
 			'success' => !empty($results['success']),
-			'results' => $results,
+			'result' => $results,
 			'summary' => sprintf(
 				'Sent %d invitations, %d failed',
 				count($results['success']),
@@ -1576,6 +1672,7 @@
 
 	/**
 	 * Add referral settings subpage to admin menu
+	 * Add referral settings subpage to admin menu
 	 *
 	 * @param array $subpages
 	 * @return array
@@ -1730,7 +1827,7 @@
 			<!-- Settings Section -->
 			<?= $this->renderAdminHTML() ?>
 		</div>
-
+<?php /**
 		<style>
 			.jvb-upload-box {
 				padding: 20px;
@@ -1785,11 +1882,12 @@
 				margin: 10px 0;
 			}
 		</style>
-
+*/
+		if (is_admin()) {
+?>
 		<script>
 			jQuery(document).ready(function($) {
 				// Client upload
-				// Client upload
 				$('#client-upload-form').on('submit', function(e) {
 					e.preventDefault();
 					const formData = new FormData(this);
@@ -1897,12 +1995,12 @@
 					const search = $('#referral-search').val();
 
 					$.ajax({
-						url: '<?= rest_url('jvb/v1/referrals/list') ?>',
+						url: '<?= rest_url('jvb/v1/referrals') ?>',
 						method: 'GET',
 						data: {
-							page: page,
-							per_page: 20,
-							status: status,
+							offset: page -1,
+							limit: 20,
+							status: status === '' ? 'all' : status,
 							search: search
 						},
 						beforeSend: function(xhr) {
@@ -1930,10 +2028,10 @@
 					html += '<th>Actions</th>';
 					html += '</tr></thead><tbody>';
 
-					if (data.referrals.length === 0) {
+					if (data.items.length === 0) {
 						html += '<tr><td colspan="7" style="text-align: center;">No referrals found</td></tr>';
 					} else {
-						data.referrals.forEach(function(ref) {
+						data.items.forEach(function(ref) {
 							html += '<tr>';
 							html += '<td>' + (ref.referrer_name || 'Unknown') + '</td>';
 							html += '<td>' + (ref.referee_display_name || ref.referee_name) + '</td>';
@@ -1976,9 +2074,12 @@
 					if (!confirm('Mark this referral as consulted? This will create the consultation reward.')) return;
 
 					$.ajax({
-						url: '<?= rest_url('jvb/v1/referrals/mark-consulted') ?>',
+						url: '<?= rest_url('jvb/v1/referrals') ?>', // Changed from /mark-consulted
 						method: 'POST',
-						data: JSON.stringify({ referral_id: id }),
+						data: JSON.stringify({
+							action: 'consulted',  // Added action parameter
+							referral_id: id
+						}),
 						contentType: 'application/json',
 						beforeSend: function(xhr) {
 							xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
@@ -1999,9 +2100,12 @@
 					if (!confirm('Mark this referral as treated? This will create rewards for both parties.')) return;
 
 					$.ajax({
-						url: '<?= rest_url('jvb/v1/referrals/mark-treated') ?>',
+						url: '<?= rest_url('jvb/v1/referrals') ?>', // Changed from /mark-treated
 						method: 'POST',
-						data: JSON.stringify({ referral_id: id }),
+						data: JSON.stringify({
+							action: 'treated',  // Added action parameter
+							referral_id: id
+						}),
 						contentType: 'application/json',
 						beforeSend: function(xhr) {
 							xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
@@ -2039,6 +2143,7 @@
 			});
 		</script>
 		<?php
+		}
 	}
 
 	protected function renderAdminHTML():string
@@ -2166,14 +2271,14 @@
 						</tr>
 						<tr>
 							<th scope="row">
-								<label for="<?= BASE ?>client_import_role">Client Import Role</label>
+								<label for="<?= BASE ?>referral_role">Client Import Role</label>
 							</th>
 							<td>
 								<?php
-								$selected_role = get_option(BASE . 'client_import_role', '');
+								$selected_role = get_option(BASE . 'referral_role', '');
 								$roles = wp_roles()->get_names();
 								?>
-								<select name="<?= BASE ?>client_import_role" id="<?= BASE ?>client_import_role">
+								<select name="<?= BASE ?>referral_role" id="<?= BASE ?>referral_role">
 									<?php foreach ($roles as $role_value => $role_name): ?>
 										<option value="<?= esc_attr($role_value) ?>" <?php selected($selected_role, $role_value); ?>>
 											<?= esc_html($role_name) ?>
@@ -2339,116 +2444,195 @@
 			$referral_code = $this->getUserReferralCode($user_id);
 		}
 
-		$stats = $this->getUserStats($user_id);
 		$referrals = $this->getUserReferrals($user_id, ['limit' => 20]);
 
 		ob_start();
+
+		$tabs = new Tabs();
+		$tabs->addTab('share')
+			->title('Share')
+			->icon('share-fat')
+			->description('Share your code and earn rewards when your referrals complete their first treatment!')
+			->content($this->shareDashboard($user_id, $referral_code));
+		$tabs->addTab('referrals')
+			->title('Your Referrals')
+			->icon('hand-heart')
+			->content($this->referralCRUD($user_id));
+
 		?>
 		<div class="referral-dashboard">
-			<div class="referral-header">
-				<h2>Your Referrals</h2>
-				<p>Share your code and earn rewards when your referrals complete their first treatment!</p>
-			</div>
+			<?= $tabs->render(true);?>
+		</div>
 
-			<?php $this->getShareButtons($user_id); ?>
+		<?php
+		return ob_get_clean();
+	}
 
-			<!-- Referral Code Card -->
-			<div class="referral-code-card">
-				<h3>Your Referral Code</h3>
-				<div class="code-display">
-					<span class="code"><?= esc_html($referral_code) ?></span>
-					<button class="button copy-code" data-code="<?= esc_attr($referral_code) ?>">
-						Copy Code
-					</button>
-				</div>
-				<p class="share-link">
-					Share link: <input type="text" readonly value="<?= home_url('/?ref=' . $referral_code) ?>"
-									   onclick="this.select()" style="width: 100%; margin-top: 5px;" />
-				</p>
+	protected function shareDashboard(int $user_id, string $referral_code):string
+	{
+		ob_start();
+		?>
+		<?php $this->getShareButtons($user_id); ?>
+
+		<!-- Referral Code Card -->
+		<div class="card">
+			<h3>Share Code</h3>
+			<div class="row btw nowrap">
+				<code class="code"><?= esc_html($referral_code) ?></code>
+				<button class="button copy-btn" data-code="<?= esc_attr($referral_code) ?>">
+					Copy Code
+				</button>
 			</div>
-			<form class="invite">
-				<?php
-				$meta = new MetaForm();
-				$field = [
-					'type'	=> 'repeater',
-					'label'	=> 'Invite Your Friends',
-					'fields'	=> [
-						'name'	=> [
-							'type'	=> 'text',
-							'label'	=> 'name',
-						],
-						'email'	=> [
-							'type'	=> 'email',
-							'label'	=> 'email',
-						]
+			<h3>Share Link</h3>
+			<div class="row btw nowrap">
+				<code class="share-link">
+					<?= home_url('/?ref=' . $referral_code) ?>
+				</code>
+				<button class="button copy-btn" data-code="<?= home_url('/?ref=' . $referral_code) ?>">
+					Copy Link
+				</button>
+			</div>
+		</div>
+		<form class="invite">
+			<h2>Invite your Friends</h2>
+			<p>Or, if you prefer, enter your friends name(s) and email(s), and we'll send off some emails.</p>
+			<p><small>(No data is stored. Your friends will get an email from our email.)</small></p>
+			<?php
+			$meta = new MetaForm();
+			$invite = [
+				'type' => 'tag_list',
+				'label' => 'Invite Your Friends',
+				'hint' => 'Add friends to send them a referral link',
+				'add_label' => 'Add Invite',
+				'tag_format' => '{name} ({email})', // or 'first_field', 'all_fields', 'email', etc.
+				'fields' => [
+					'name' => [
+						'type' => 'text',
+						'label' => 'Name',
+						'placeholder' => 'Full Name',
+						'required' => true
+					],
+					'email' => [
+						'type' => 'email',
+						'label' => 'Email',
+						'placeholder' => 'email@example.com',
+						'required' => true
 					]
-				];
-				$meta->render('invite', [], $field);
+				]
+			];
+			$fields = [
+				'subject'	=> [
+					'type'	=> 'text',
+					'label'	=> 'Email Subject',
+					'value'	=> 'Try Legacy for Tattoo Removal',
+				],
+				'message'	=> [
+					'type'		=> 'textarea',
+					'label'		=> 'Customize message',
+					'value'	=> 'I had a great experience at Legacy Tattoo Removal!
+
+If you click the link below, you can get 20% off your first treatment with them.',
+					'hint'		=> 'We\'ll add your code and a link automatically.'
+				]
+			];
+			$meta->render('invite', [], $invite);
+			?>
+			<details>
+				<summary class="icon icon-caret-down">Customize Message</summary>
+				<?php
+				foreach ($fields as $fieldName => $field) {
+					$value = (array_key_exists('value', $field)) ? $field['value'] : [];
+					$meta->render($fieldName, $value, $field);
+				}
 				?>
-			</form>
+			</details>
 
-			<!-- Stats Grid -->
-			<div class="stats-grid">
-				<div class="stat-card">
-					<h4>Total Referrals</h4>
-					<span class="stat-number"><?= esc_html($stats['total_referrals'] ?? 0) ?></span>
-				</div>
-				<div class="stat-card">
-					<h4>Completed Treatments</h4>
-					<span class="stat-number"><?= esc_html($stats['treated_count'] ?? 0) ?></span>
-				</div>
-				<div class="stat-card">
-					<h4>Pending</h4>
-					<span class="stat-number"><?= esc_html($stats['pending_count'] ?? 0) ?></span>
-				</div>
-				<div class="stat-card highlight">
-					<h4>Available Rewards</h4>
-					<span class="stat-number">$<?= number_format($stats['available_rewards'] ?? 0, 2) ?></span>
-				</div>
+			<button type="submit"><?=jvbIcon('envelope')?>Send Invites</button>
+		</form>
+		<?php
+		return ob_get_clean();
+	}
+
+	protected function referralCRUD(int $user_id):string
+	{
+		$stats = $this->getUserStats($user_id);
+		ob_start();
+		?>
+		<!-- Stats Grid with Updated Labels -->
+		<div class="item-grid stats">
+			<div class="card">
+				<h4>Code Used</h4>
+				<span class="stat-number" data-stat="code_used"><?= esc_html($stats['code_used'] ?? 0) ?></span>
+				<p class="hint">People who used your code</p>
 			</div>
-
-			<!-- Referrals List -->
-			<div class="referrals-list-card">
-				<h3>Your Referrals</h3>
-				<?php if (empty($referrals)): ?>
-					<p>You haven't referred anyone yet. Share your code to get started!</p>
-				<?php else: ?>
-					<table class="referrals-table">
-						<thead>
-						<tr>
-							<th>Name</th>
-							<th>Email</th>
-							<th>Status</th>
-							<th>Referred Date</th>
-						</tr>
-						</thead>
-						<tbody>
-						<?php foreach ($referrals as $ref): ?>
-							<tr>
-								<td><?= esc_html($ref->referee_name) ?></td>
-								<td><?= esc_html($ref->referee_email) ?></td>
-								<td><span class="status-badge <?= esc_attr($ref->status) ?>"><?= esc_html(ucfirst($ref->status)) ?></span></td>
-								<td><?= date('M j, Y', strtotime($ref->referred_at)) ?></td>
-							</tr>
-						<?php endforeach; ?>
-						</tbody>
-					</table>
-				<?php endif; ?>
+			<div class="card">
+				<h4>Treatments</h4>
+				<span class="stat-number" data-stat="treatments"><?= esc_html($stats['treatments'] ?? 0) ?></span>
+				<p class="hint">Completed first treatment</p>
+			</div>
+			<div class="card highlight">
+				<h4>Total Rewards</h4>
+				<span class="stat-number" data-stat="total_rewards">$<?= number_format($stats['total_rewards'] ?? 0, 2) ?></span>
+				<p class="hint">Earned from referrals</p>
 			</div>
 		</div>
 
-
-		<script>
-			jQuery(document).ready(function($) {
-				$('.copy-code').on('click', function() {
-					const code = $(this).data('code');
-					navigator.clipboard.writeText(code).then(function() {
-						alert('Code copied to clipboard!');
-					});
-				});
-			});
-		</script>
 		<?php
+		// Configure CRUDSkeleton for referrals
+		$crud = new CRUDSkeleton();
+		$crud->title('Your Referrals', 'Track friends you\'ve invited and rewards earned')
+			->content('referral', 'Referral', 'Referrals')
+			->initMeta('custom', 'referral')
+			->setFields([
+				'referee_name' => [
+					'label' => 'Name',
+					'type' => 'text',
+				],
+				'referee_email' => [
+					'label' => 'Email',
+					'type' => 'text',
+				],
+				'referred_at' => [
+					'label' => 'Code Used',
+					'type' => 'date',
+				],
+				'referral_status' => [
+					'label' => 'Status',
+					'type' => 'text',
+				]
+			])
+			->setStatuses(['all', 'unused', 'registered', 'consulted', 'completed'])
+			->addViews(['table', 'list'])
+			->defaultView('table')
+			->addCapabilities(['view'])
+			->addDateFilter('referred_at')
+			->showBulkControls(false)
+			->showFilters(false)
+			->useCRUDjs(false); // We'll use our custom Referral.js with DataStore
+
+		// Add custom template for actions column
+		$crud->addItemActions(['resend', 'trash']);
+		$crud->defineItemAction('resend', [
+			'title'	=> 'Resend Invitation',
+			'icon'	=> 'paper-plane-tilt'
+		]);
+		$crud->defineItemAction('trash', [
+			'title'	=> 'Remove from List'
+		]);
+
+		// Custom empty state
+		$crud->addTemplate('empty', '
+		<template class="emptyState">
+			<div class="empty-state">
+				<h3>' . jvbDashIcon('hand-heart') . 'Nothing Yet' . jvbDashIcon('hand-heart') . '</h3>
+				<p>Start sharing your referral code to earn rewards!</p>
+				<p><small><i>Share your code using the "Share" tab below.</i></small></p>
+			</div>
+		</template>
+	');
+
+		$crud->render();
+
 		return ob_get_clean();
 	}
 
@@ -2473,8 +2657,8 @@
 			update_option(BASE . 'referral_page_id', $page_id);
 
 			// Save client import role
-			$import_role = sanitize_text_field($post_data[BASE . 'client_import_role'] ?? JVB_USER);
-			update_option(BASE . 'client_import_role', $import_role);
+			$import_role = sanitize_text_field($post_data[BASE . 'referral_role'] ?? JVB_USER);
+			update_option(BASE . 'referral_role', $import_role);
 
 			// Save reward settings
 			$settings = [
@@ -2573,5 +2757,98 @@
 		</nav>
 	<?php
 	}
+
+	/**
+	 * Send notification to referrer when someone registers
+	 *
+	 * @param int $referrer_id
+	 * @param string $referee_name
+	 */
+	protected function sendReferrerNotification(int $referrer_id, string $referee_name): void
+	{
+		$referrer = get_userdata($referrer_id);
+		if (!$referrer) {
+			return;
+		}
+
+		$subject = sprintf('%s signed up with your referral code!', $referee_name);
+		$message = sprintf(
+			"Great news! %s just signed up using your referral code.\n\n" .
+			"View your referrals: %s",
+			$referee_name,
+			home_url('/dash/referrals')
+		);
+
+		JVB()->email()->sendEmail(
+			$referrer->user_email,
+			$subject,
+			$message
+		);
+	}
+
+	/**
+	 * Get welcome message for newly referred user
+	 *
+	 * @param int $user_id
+	 * @return string HTML content for welcome message
+	 */
+	public function getReferralWelcomeMessage(int $user_id): string
+	{
+		// Check if user was referred
+		$referral = $this->getReferralByReferee($user_id);
+
+		if (!$referral || $referral->status !== 'pending') {
+			return '';
+		}
+
+		// Only show for recent registrations (within 7 days)
+		$registered_time = strtotime($referral->referred_at);
+		if ((time() - $registered_time) > (7 * DAY_IN_SECONDS)) {
+			return '';
+		}
+
+		// Get referrer name
+		$referrer = get_userdata($referral->referrer_id);
+		$referrer_first_name = $referrer ? strtok($referrer->display_name, ' ') : 'Your friend';
+
+		// Get reward text
+		$reward_text = $this->getRewardText(false); // Just "20% off" or "$25 off"
+
+		$booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact'));
+		$estimate_url = apply_filters('jvb_referral_estimate_url', home_url('/estimate'));
+
+		ob_start();
+		?>
+		<div class="welcome-banner referral-welcome">
+			<div class="banner-content">
+				<h3><?= jvbIcon('confetti') ?>Welcome! <small><b><?= esc_html($referrer_first_name) ?></b> invited you to save <b><?= esc_html($reward_text) ?></b>!</small></h3>
+				<p>But we're not done yet! Here's what happens next:</p>
+				<div class="callout">
+					<ol>
+						<li>Book your <b>free consultation</b></li>
+						<li>Come in and we'll assess your tattoo</li>
+						<li>Get <?= esc_html($reward_text) ?> your first treatment!</li>
+					</ol>
+				</div>
+				<p class="hint">
+					<strong>Important:</strong> If you book with a different email than
+					<strong><?= esc_html(wp_get_current_user()->user_email) ?></strong>,
+					please let us know so we can apply your reward!
+				</p>
+				<ul class="buttons">
+					<li><a href="<?= esc_url($estimate_url) ?>" class="button-secondary">
+						<?= jvbIcon('calculator') ?> Get an Estimate First
+					</a></li>
+					<li><a href="<?= esc_url($booking_url) ?>" class="button-primary">
+						<?= jvbIcon('calendar') ?> Book Free Consult
+					</a></li>
+				</ul>
+
+
+			</div>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
 }
 
diff --git a/inc/managers/RoleManager.php b/inc/managers/RoleManager.php
index 417c2ee..4913bc4 100644
--- a/inc/managers/RoleManager.php
+++ b/inc/managers/RoleManager.php
@@ -19,8 +19,24 @@
 	   $this->content = array_map(function($content) {
 		   return strtolower($content['plural']);
 	   },JVB_CONTENT);
+	   add_action('set_user_role', [$this, 'updateRoles'], 10, 3);
     }
 
+	public function updateRoles(int $userID, string $role, array $oldRoles):void
+	{
+		if (doing_action('set_user_role') > 1) {
+			return;
+		}
+		$temp = jvbNoBase($role);
+		if (array_key_exists($temp, JVB_USER)) {
+			$user = get_userdata($userID);
+			if (!$user) {
+				return;
+			}
+			$this->reset($user);
+			$this->setUserAs($user, $temp);
+		}
+	}
 
     /**
      * @param WP_User $user
@@ -140,7 +156,6 @@
     /**
      * @param WP_User $user
      * @param string $type
-     * @param bool $add
      *
      * @return void
      */
@@ -410,7 +425,7 @@
 		if (empty($capsMap)){
 			$capsMap = [
 				$content,
-				str_replace('-', '_',sanitize_title(strtolower(JVB_CONTENT[$content]['plural'])))
+				str_replace('-', '_',sanitize_title(strtolower(JVB_CONTENT[$content]['plural']??JVB_TAXONOMY[$content]['plural'])))
 			];
 			return $capsMap[1];
 		}
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
new file mode 100644
index 0000000..529dd76
--- /dev/null
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -0,0 +1,327 @@
+<?php
+namespace JVBase\managers\SEO;
+
+use JVBase\managers\CacheManager;
+use JVBase\utility\Features;
+use WP_Post;
+use WP_Term;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Breadcrumb Manager
+ *
+ * Generates breadcrumb navigation arrays and HTML output
+ * Integrates with SchemaOutputManager for structured data
+ */
+class BreadcrumbManager
+{
+	private CacheManager $cache;
+	private static ?self $instance = null;
+
+	private function __construct()
+	{
+		$this->cache = CacheManager::for('breadcrumbs', MONTH_IN_SECONDS)->connectTo('all');
+	}
+
+	public static function getInstance(): self
+	{
+		if (self::$instance === null) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+
+	/**
+	 * Get breadcrumb array for current page
+	 *
+	 * @return array Array of breadcrumb items with 'name', 'url', optional 'icon' and 'id'
+	 */
+	public function getCrumbs(): array
+	{
+		if (is_front_page()) {
+			return [];
+		}
+
+		$key = get_queried_object_id() ?: 'home';
+		$crumbs = $this->cache->get($key);
+
+		if ($crumbs !== false) {
+			return $crumbs;
+		}
+
+		$crumbs = $this->buildCrumbs();
+		$this->cache->set($key, $crumbs);
+
+		return $crumbs;
+	}
+
+	/**
+	 * Build breadcrumb array based on current page context
+	 */
+	private function buildCrumbs(): array
+	{
+		$crumbs = [];
+
+		// Always start with home
+		$crumbs[] = [
+			'name' => 'Home',
+			'icon' => jvbIcon('house'),
+			'url'  => get_home_url(),
+		];
+
+		$obj = get_queried_object();
+
+		if (is_tax()) {
+			$crumbs = $this->addTaxonomyCrumbs($crumbs, $obj);
+		} elseif (is_singular()) {
+			$crumbs = $this->addArchiveCrumbs($crumbs, $obj);
+			$crumbs = $this->addSingularCrumbs($crumbs, $obj);
+		} elseif (is_post_type_archive() && !is_post_type_archive(BASE.'dash')) {
+			$crumbs = $this->addArchiveCrumbs($crumbs, $obj);
+		}
+
+		return $crumbs;
+	}
+
+	/**
+	 * Add taxonomy-specific breadcrumbs
+	 */
+	private function addTaxonomyCrumbs(array $crumbs, WP_Term $term): array
+	{
+		$tax = jvbNoBase($term->taxonomy);
+		$config = Features::getConfig($tax, 'term');
+
+		// Add parent content archive if taxonomy is for single content type
+		if (count($config['for_content']) === 1) {
+			$contentConfig = JVB_CONTENT[$config['for_content'][0]];
+			$crumbs[] = [
+				'name' => $contentConfig['breadcrumb'] ?? $contentConfig['plural'],
+				'url'  => get_post_type_archive_link(jvbCheckBase($config['for_content'][0])),
+			];
+			$crumbs[] = [
+				'name' => 'By ' . $config['singular'],
+				'url'  => false,
+			];
+		}
+
+		// Add directory if exists
+		if (Features::forTaxonomy($tax)->has('directory')) {
+			$directory = jvbDirectories($tax);
+			$crumbs[] = [
+				'name' => $directory['title'],
+				'url'  => $directory['url']
+			];
+		}
+
+		// Add term hierarchy
+		$crumbs = array_merge($crumbs, $this->buildTermHierarchy($term));
+
+		return $crumbs;
+	}
+
+	/**
+	 * Add singular post breadcrumbs
+	 */
+	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']
+			];
+		}
+
+		// Handle directory posts specially
+		if (jvbIsDirectory()) {
+			$pos = jvbGetDirectoryInfo();
+			if (!empty($pos)) {
+				// Special case for map
+				if ($pos['title'] == 'Map') {
+					$crumbs[] = [
+						'name' => 'Tattoo Shops',
+						'url'  => jvbDirectories(BASE.'shop')['url']
+					];
+				}
+
+				$crumbs[] = [
+					'name' => $pos['title'],
+					'url'  => $pos['url']
+				];
+			}
+		} else {
+			// Add post hierarchy
+			$crumbs = array_merge($crumbs, $this->buildPostHierarchy($post));
+		}
+
+		return $crumbs;
+	}
+
+	/**
+	 * Add archive breadcrumbs
+	 */
+	private function addArchiveCrumbs(array $crumbs, object $obj): array
+	{
+		$type = is_singular() ? $obj->post_type : $obj -> name;
+		$name = jvbNoBase($type);
+		if (array_key_exists($name, JVB_CONTENT)) {
+			$crumbs[] = [
+				'name' => JVB_CONTENT[$name]['breadcrumb'] ?? JVB_CONTENT[$name]['plural'],
+				'url'  => get_post_type_archive_link($type),
+			];
+		}
+
+		return $crumbs;
+	}
+
+	/**
+	 * Build term hierarchy recursively
+	 */
+	private function buildTermHierarchy(WP_Term $term, array $crumbs = []): array
+	{
+		$url = get_term_link($term->term_id);
+		array_unshift($crumbs, [
+			'name' => $term->name,
+			'url'  => $url,
+			'id'   => $term->term_id,
+		]);
+
+		if ($term->parent !== 0) {
+			$parent = get_term($term->parent, $term->taxonomy);
+			if ($parent && !is_wp_error($parent)) {
+				$crumbs = $this->buildTermHierarchy($parent, $crumbs);
+			}
+		}
+
+		return $crumbs;
+	}
+
+	/**
+	 * Build post hierarchy recursively
+	 */
+	private function buildPostHierarchy(WP_Post $post, array $crumbs = []): array
+	{
+		array_unshift($crumbs, [
+			'name' => $post->post_title,
+			'url'  => get_the_permalink($post->ID),
+			'id'   => $post->ID,
+		]);
+
+		if ($post->post_parent !== 0) {
+			$parent = get_post($post->post_parent);
+			if ($parent) {
+				$crumbs = $this->buildPostHierarchy($parent, $crumbs);
+			}
+		}
+
+		return $crumbs;
+	}
+
+	/**
+	 * Render breadcrumb navigation HTML
+	 *
+	 * @return string HTML breadcrumb navigation
+	 */
+	public function renderNavigation(): string
+	{
+		if (is_front_page()) {
+			return '';
+		}
+
+		$crumbs = $this->getCrumbs();
+		if (empty($crumbs)) {
+			return '';
+		}
+
+		$out = '<nav id="breadcrumbs">';
+		$out .= '<ol itemscope itemtype="https://schema.org/BreadcrumbList">';
+
+		$position = 1;
+		foreach ($crumbs as $crumb) {
+			$label = '<span itemprop="name">' . strtolower($crumb['name']) . '</span>';
+
+			// Replace label with icon if present
+			if (isset($crumb['icon'])) {
+				$label = $crumb['icon'] . '<span class="screen-reader-text" itemprop="name">' . $crumb['name'] . '</span>';
+			}
+
+			$aOpen = $aClose = '';
+
+			// Add link if URL exists and not current page
+			if ($crumb['url'] !== false) {
+				$isCurrent = isset($crumb['id']) && $crumb['id'] === get_queried_object_id();
+				if (!$isCurrent) {
+					$aOpen = '<a itemprop="item" href="' . esc_url($crumb['url']) . '" title="' . esc_attr($crumb['name']) . '">';
+					$aClose = '</a>';
+				}
+			}
+
+			$out .= '<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">';
+			$out .= $aOpen . $label . $aClose;
+			$out .= '<meta itemprop="position" content="' . $position . '" />';
+			$out .= '</li>';
+
+			$position++;
+		}
+
+		$out .= '</ol>';
+		$out .= '</nav>';
+
+		return $out;
+	}
+
+	/**
+	 * Convert breadcrumb array to schema.org format
+	 * Used by SchemaOutputManager
+	 *
+	 * @return array Schema.org BreadcrumbList
+	 */
+	public function toSchema(): array
+	{
+		$crumbs = $this->getCrumbs();
+		if (empty($crumbs)) {
+			return [];
+		}
+
+		$items = [];
+		$position = 1;
+
+		foreach ($crumbs as $crumb) {
+			// Schema requires a URL
+			if ($crumb['url'] === false) {
+				$crumb['url'] = get_permalink();
+			}
+
+			$items[] = [
+				'@type'    => 'ListItem',
+				'position' => $position,
+				'name'     => $crumb['name'],
+				'item'     => $crumb['url'],
+			];
+
+			$position++;
+		}
+
+		return [
+			'@type'           => 'BreadcrumbList',
+			'@id'             => get_permalink() . '/#breadcrumbs',
+			'itemListElement' => $items
+		];
+	}
+
+	/**
+	 * Invalidate breadcrumb cache for specific object
+	 */
+	public function invalidateCache(?int $objectId = null): void
+	{
+		if ($objectId) {
+			$this->cache->delete($objectId);
+		} else {
+			$this->cache->clear();
+		}
+	}
+}
diff --git a/inc/managers/SEO/ConfigManager.php b/inc/managers/SEO/ConfigManager.php
new file mode 100644
index 0000000..c66bba3
--- /dev/null
+++ b/inc/managers/SEO/ConfigManager.php
@@ -0,0 +1,390 @@
+<?php
+namespace JVBase\managers\SEO;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Interface for options for schema and meta, defaulting to what is defined in the constants
+ */
+class ConfigManager
+{
+	private ?string $type = null;
+	private ?string $metaKey = null;
+	private ?string $schemaKey = null;
+	private ?string $archiveKey = null;
+	protected bool $hasArchive = false;
+	private static array $instances = [];
+	protected ?array $schema = null;
+	protected ?array $meta = null;
+	protected ?array $archive = null;
+	protected SchemaBuilder $registry;
+
+	/**
+	 * Private constructor; use for() factory method instead
+	 */
+	private function __construct(string $type) {
+		$this->type = $type;
+		$this->schemaKey = BASE.'schema_for_'.$type;
+		$this->metaKey = BASE.'meta_for_'.$type;
+		$this->registry = SchemaBuilder::getInstance();
+		$this->schema = $this->getConfigFor($type);
+		$this->meta = $this->getMetaFor($type);
+	}
+
+	/**
+	 * Factory method - returns singleton instance per type
+	 */
+	public static function for(string $type): self
+	{
+		$key = jvbNoBase($type);
+		if (!isset(self::$instances[$key])) {
+			self::$instances[$key] = new self($type);
+		}
+		return self::$instances[$key];
+	}
+
+	public function meta():array
+	{
+		return $this->meta ?? [];
+	}
+	public function schema():array
+	{
+		return $this->schema ?? [];
+	}
+
+	public function archive(): array
+	{
+		return $this->archive ?? [];
+	}
+
+	public function setupArchive()
+	{
+		$this->hasArchive = true;
+		$this->archiveKey = BASE.'archive_for_'.$this->type;
+		$this->archive = $this->getArchiveFor($this->type);
+	}
+
+	/**
+	 * Get default meta configuration for a type
+	 */
+	protected function getMetaFor(string $type): array
+	{
+		$default = $this->registry->getDefaultMetaValues();
+		return get_option($this->metaKey, $default);
+	}
+
+	/**
+	 * Get default schema configuration for a type
+	 */
+	protected function getConfigFor(string $type): array
+	{
+		$default = $this->getDefaultConfig($type, 'schema');
+		return get_option($this->schemaKey, $default);
+	}
+
+	/**
+	 * Get default schema configuration for a type
+	 */
+	protected function getArchiveFor(string $type): array
+	{
+		$default = $this->getDefaultConfig($type, 'archive');
+		return get_option($this->archiveKey, $default);
+	}
+
+	/**
+	 * Get default configuration from constants
+	 */
+	private function getDefaultConfig(string $type, string $configType): array
+	{
+		switch ($type) {
+			case 'website':
+				// Try actual schema type first, then semantic key
+				if (defined('JVB_SCHEMA')) {
+					if (array_key_exists('website', JVB_SCHEMA)) {
+						return JVB_SCHEMA['website'];
+					}
+				}
+				return [];
+			case 'organization':
+
+				// Try actual schema types first, then semantic keys
+				if (defined('JVB_SCHEMA')) {
+					if (array_key_exists('organization', JVB_SCHEMA)) {
+						return JVB_SCHEMA['organization'];
+					}
+				}
+				return [];
+
+			default:
+				// Try to find in content, taxonomy, or user configs
+				$config = $this->findInConstants($type);
+				if (array_key_exists('seo', $config) && is_array($config['seo'])) {
+					$config = $config['seo'];
+				}
+
+				// If asking for archive config and none exists, provide default
+				if ($configType === 'archive' && !isset($config['archive'])) {
+					return [
+						'type' => 'CollectionPage',
+						'name' => '{{archive_title}}',
+						'description' => '{{archive_description}}',
+						'url' => '{{archive_url}}'
+					];
+				}
+				return $config[$configType] ?? [];
+		}
+	}
+	/**
+	 * Find configuration in JVB constants
+	 */
+	private function findInConstants(string $type): array
+	{
+		if (defined('JVB_CONTENT') && isset(JVB_CONTENT[$type])) {
+			return JVB_CONTENT[$type];
+		}
+		if (defined('JVB_TAXONOMY') && isset(JVB_TAXONOMY[$type])) {
+			return JVB_TAXONOMY[$type];
+		}
+		if (defined('JVB_USER') && isset(JVB_USER[$type])) {
+			return JVB_USER[$type];
+		}
+		return [];
+	}
+
+	public function resetConfig(): bool
+	{
+		$result = delete_option($this->schemaKey);
+		if ($result) {
+			$this->schema = $this->getConfigFor($this->type);
+		}
+		return $result;
+	}
+	/**
+	 * Reset meta configuration to defaults
+	 */
+	public function resetMeta(): bool
+	{
+		$result = delete_option($this->metaKey);
+		if ($result) {
+			$this->meta = $this->getMetaFor($this->type);
+		}
+		return $result;
+	}
+
+	public function resetArchive():bool
+	{
+		$result = delete_option($this->archiveKey);
+		if ($result) {
+			$this->archive = $this->getArchiveFor($this->type);
+		}
+		return $result;
+	}
+
+	/**
+	 * Reset both configurations to defaults
+	 */
+	public function resetAll(): bool
+	{
+		return !($this->resetConfig() && $this->resetMeta() && ($this->hasArchive)) || $this->resetArchive();
+	}
+	/**
+	 * Validate and update schema configuration
+	 *
+	 * @param array $config Schema configuration to save
+	 * @return bool|\WP_Error True on success, WP_Error on failure
+	 */
+	public function updateConfig(array $config): bool|\WP_Error
+	{
+		// Validate type is provided
+		if (!isset($config['type'])) {
+			return new \WP_Error('missing_type', 'Schema type is required');
+		}
+
+		// Validate type exists in registry
+		if (!$this->registry->getTypeDefinition($config['type'])) {
+			return new \WP_Error('invalid_type', sprintf('Schema type "%s" is not registered', $config['type']));
+		}
+
+		// Get allowed fields for this type
+		$allowedFields = $this->registry->getFieldsForType($config['type']);
+
+		// Filter to only allowed fields
+		$validated = array_filter($config, function($key) use ($allowedFields) {
+			return in_array($key, $allowedFields);
+		}, ARRAY_FILTER_USE_KEY);
+
+		// Validate template syntax for field values
+		$fieldErrors = [];
+		foreach ($validated as $field => $value) {
+			if (is_string($value) && $field !== 'type') {
+				$validationResult = $this->validateTemplate($value, $field);
+				if (is_wp_error($validationResult)) {
+					$fieldErrors[$field] = $validationResult->get_error_message();
+				}
+			}
+		}
+
+		if (!empty($fieldErrors)) {
+			return new \WP_Error('validation_failed', 'Template validation failed', $fieldErrors);
+		}
+
+		// Remove completely empty values (but keep false/0)
+		$validated = array_filter($validated, function($value) {
+			return $value !== '' && $value !== null && $value !== [];
+		});
+
+		// Update option
+		$result = update_option($this->schemaKey, $validated);
+
+		if ($result) {
+			// Update instance cache
+			$this->schema = $validated;
+		}
+
+		return $result;
+	}
+	/**
+	 * Validate and update meta configuration
+	 *
+	 * @param array $meta Meta configuration to save
+	 * @return bool|\WP_Error True on success, WP_Error on failure
+	 */
+	public function updateMeta(array $meta): bool|\WP_Error
+	{
+		// Validate template syntax
+		$errors = [];
+		foreach ($meta as $field => $value) {
+			if (is_string($value)) {
+				$validationResult = $this->validateTemplate($value, $field);
+				if (is_wp_error($validationResult)) {
+					$errors[$field] = $validationResult->get_error_message();
+				}
+			}
+		}
+
+		if (!empty($errors)) {
+			return new \WP_Error('validation_failed', 'Template validation failed', $errors);
+		}
+
+		// Update option
+		$result = update_option($this->metaKey, $meta);
+
+		if ($result) {
+			// Update instance cache
+			$this->meta = $meta;
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Validate and update archive configuration
+	 *
+	 * @param array $archive Archive configuration to save
+	 * @return bool|\WP_Error True on success, WP_Error on failure
+	 */
+	public function updateArchive(array $archive): bool|\WP_Error
+	{
+		if (!$this->hasArchive) {
+			return new \WP_Error('no_archive', 'This type does not support archives');
+		}
+
+		// Validate type is provided
+		if (!isset($archive['type'])) {
+			return new \WP_Error('missing_type', 'Schema type is required');
+		}
+
+		// Validate type exists in registry
+		if (!$this->registry->getTypeDefinition($archive['type'])) {
+			return new \WP_Error('invalid_type', sprintf('Schema type "%s" is not registered', $archive['type']));
+		}
+
+		// Get allowed fields for this type
+		$allowedFields = $this->registry->getFieldsForType($archive['type']);
+
+		// Filter to only allowed fields
+		$validated = array_filter($archive, function($key) use ($allowedFields) {
+			return in_array($key, $allowedFields);
+		}, ARRAY_FILTER_USE_KEY);
+
+		// Validate template syntax
+		$fieldErrors = [];
+		foreach ($validated as $field => $value) {
+			if (is_string($value) && $field !== 'type') {
+				$validationResult = $this->validateTemplate($value, $field);
+				if (is_wp_error($validationResult)) {
+					$fieldErrors[$field] = $validationResult->get_error_message();
+				}
+			}
+		}
+
+		if (!empty($fieldErrors)) {
+			return new \WP_Error('validation_failed', 'Template validation failed', $fieldErrors);
+		}
+
+		// Remove completely empty values
+		$validated = array_filter($validated, function($value) {
+			return $value !== '' && $value !== null && $value !== [];
+		});
+
+		// Update option
+		$result = update_option($this->archiveKey, $validated);
+
+		if ($result) {
+			$this->archive = $validated;
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Validate template syntax
+	 *
+	 * @param string $template Template string to validate
+	 * @param string $field Field name (for error messages)
+	 * @return bool|\WP_Error True if valid, WP_Error if invalid
+	 */
+	private function validateTemplate(string $template, string $field): bool|\WP_Error
+	{
+		// Check for unclosed template tags
+		$openCount = substr_count($template, '{{');
+		$closeCount = substr_count($template, '}}');
+
+		if ($openCount !== $closeCount) {
+			return new \WP_Error(
+				'malformed_template',
+				sprintf('Unclosed template tag in field "%s"', $field)
+			);
+		}
+
+		// Extract all template variables
+		preg_match_all('/\{\{([^}]+)\}\}/', $template, $matches);
+
+		if (!empty($matches[1])) {
+			foreach ($matches[1] as $variable) {
+				$variable = trim($variable);
+
+				// Check for empty variables
+				if (empty($variable)) {
+					return new \WP_Error(
+						'empty_variable',
+						sprintf('Empty template variable in field "%s"', $field)
+					);
+				}
+
+				// Check for invalid characters (basic validation)
+				// Allows: field_name, field_name|filter, nested.field
+				if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_.]*(?:\|[a-zA-Z_][a-zA-Z0-9_]*)*$/', $variable)) {
+					return new \WP_Error(
+						'invalid_variable',
+						sprintf('Invalid template variable "%s" in field "%s"', $variable, $field)
+					);
+				}
+			}
+		}
+
+		return true;
+	}
+}
diff --git a/inc/managers/SEO/FieldBuilder.php b/inc/managers/SEO/FieldBuilder.php
new file mode 100644
index 0000000..8d46316
--- /dev/null
+++ b/inc/managers/SEO/FieldBuilder.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace JVBase\managers\SEO;
+
+
+/**
+ * Field Builder - Fluent API for field definitions
+ */
+class FieldBuilder
+{
+	private SchemaBuilder $schema;
+	private string $name;
+	private array $definition = [];
+
+	public function __construct(SchemaBuilder $schema, string $name, array $baseDefinition = [])
+	{
+		$this->schema = $schema;
+		$this->name = $name;
+		$this->definition = $baseDefinition;
+	}
+
+	public function type(string $type): self
+	{
+		$this->definition['type'] = $type;
+		return $this;
+	}
+
+	public function label(string $label): self
+	{
+		$this->definition['label'] = $label;
+		return $this;
+	}
+
+	public function description(string $description): self
+	{
+		$this->definition['description'] = $description;
+		return $this;
+	}
+
+	public function transformer(string $transformer): self
+	{
+		$this->definition['transformer'] = $transformer;
+		return $this;
+	}
+
+	public function required(bool $required = true): self
+	{
+		$this->definition['required'] = $required;
+		return $this;
+	}
+
+	public function repeater(bool $repeater = true): self
+	{
+		$this->definition['repeater'] = $repeater;
+		return $this;
+	}
+
+	public function options(array $options): self
+	{
+		$this->definition['options'] = $options;
+		return $this;
+	}
+
+	public function placeholder(string $placeholder): self
+	{
+		$this->definition['placeholder'] = $placeholder;
+		return $this;
+	}
+
+	public function fields(array $fields): self
+	{
+		$this->definition['fields'] = $fields;
+		return $this;
+	}
+
+	public function default($default): self
+	{
+		$this->definition['default'] = $default;
+		return $this;
+	}
+
+	/**
+	 * Finish building and register the field
+	 */
+	public function __destruct()
+	{
+		$this->schema->registerField($this->name, $this->definition);
+	}
+}
diff --git a/inc/managers/SEO/FieldOverrideBuilder.php b/inc/managers/SEO/FieldOverrideBuilder.php
new file mode 100644
index 0000000..2737fcc
--- /dev/null
+++ b/inc/managers/SEO/FieldOverrideBuilder.php
@@ -0,0 +1,44 @@
+<?php
+namespace JVBase\managers\SEO;
+
+
+
+/**
+ * Field Override Builder - For customizing fields within a specific type
+ */
+class FieldOverrideBuilder
+{
+	private TypeBuilder $typeBuilder;
+	private string $fieldName;
+	private array $overrides = [];
+
+	public function __construct(TypeBuilder $typeBuilder, string $fieldName)
+	{
+		$this->typeBuilder = $typeBuilder;
+		$this->fieldName = $fieldName;
+	}
+
+	public function label(string $label): TypeBuilder
+	{
+		$this->overrides['label'] = $label;
+		return $this->finish();
+	}
+
+	public function description(string $description): TypeBuilder
+	{
+		$this->overrides['description'] = $description;
+		return $this->finish();
+	}
+
+	public function required(bool $required = true): TypeBuilder
+	{
+		$this->overrides['required'] = $required;
+		return $this->finish();
+	}
+
+	private function finish(): TypeBuilder
+	{
+		$this->typeBuilder->setFieldOverride($this->fieldName, $this->overrides);
+		return $this->typeBuilder;
+	}
+}
diff --git a/inc/managers/SEO/SEOAdminPage.php b/inc/managers/SEO/SEOAdminPage.php
new file mode 100644
index 0000000..53ea618
--- /dev/null
+++ b/inc/managers/SEO/SEOAdminPage.php
@@ -0,0 +1,281 @@
+<?php
+namespace JVBase\managers\SEO;
+
+use JVBase\managers\AdminPages;
+use JVBase\managers\CacheManager;
+use JVBase\meta\MetaForm;
+use JVBase\ui\Tabs;
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+/**
+ * Admin interface for SEO configuration
+ *
+ * Provides UI for configuring meta tags and schema for content types.
+ * Now includes live schema preview functionality.
+ *
+ */
+class SEOAdminPage
+{
+    private ConfigManager $config;
+    private SchemaBuilder $registry;
+    private MetaForm $form;
+
+    public function __construct()
+    {
+        $this->registry = SchemaBuilder::getInstance();
+        $this->form = new MetaForm();
+
+
+        // Add to JVB dashboard
+        add_filter('jvbDashboardPage', [$this, 'addDashboardSection'], 20, 2);
+		add_action('admin_enqueue_scripts', [$this, 'enqueueScripts']);
+    }
+
+	public function enqueueScripts():void
+	{
+		global $_GET;
+		if (array_key_exists('page', $_GET) && $_GET['page'] === BASE.'seo') {
+			wp_enqueue_script('jvb-form');
+			wp_enqueue_script('jvb-schema');
+		}
+	}
+
+    public static function addSubpage():void
+    {
+        $subpage = [
+            'page_title' => 'SEO Settings',
+            'menu_title' => 'SEO',
+            'capability' => 'manage_options',
+            'menu_slug' => BASE . 'seo',
+            'callback' => [self::class, 'renderAdminPageStatic']
+        ];
+        AdminPages::addSubPage(BASE.'seo', $subpage);
+    }
+
+    public static function renderAdminPageStatic():void
+    {
+        JVB()->seoAdmin()->renderAdminPage();
+    }
+
+    /**
+     * Add section to JVB dashboard
+     */
+    public function addDashboardSection(string $content, string $page): string
+    {
+		if ($page !== 'SEO') {
+			return $content;
+		}
+        ob_start();
+        $this->renderAdminPage();
+        return ob_get_clean();
+    }
+
+    /**
+     * Render admin page
+     */
+	public function renderAdminPage(bool $outputScripts = true): void
+	{
+		?>
+		<div class="wrap jvb-seo-admin">
+			<h1><?= jvbDashIcon('magnifying-glass'); ?> SEO Configuration</h1>
+
+			<?php
+			$tabs = new Tabs();
+
+			$tabs->addTab('main')
+				->title('Website & Business')
+				->icon('storefront')
+				->content($this->renderMain());
+
+			$tabs->addTab('content')
+				->title('Content')
+				->icon('note')
+				->content($this->renderConfig('content'));
+
+			$tabs->addTab('taxonomies')
+				->title('Taxonomies')
+				->icon('tag')
+				->content($this->renderConfig('taxonomy'));
+
+			echo $tabs->render();
+			?>
+		</div>
+
+		<?php
+		$this->renderTemplates();
+		if ($outputScripts) {
+			$this->renderStyles();
+		}
+	}
+
+    protected function renderForm(string $key, ?string $type = null, string $configType = 'schema'):string
+    {
+		if (!in_array($configType, ['meta', 'schema', 'archive'])) {
+			return '';
+		}
+
+		$this->config = ConfigManager::for($key);
+		// Setup archive if needed
+		if ($configType === 'archive') {
+			$this->config->setupArchive();
+			$config = $this->config->archive();
+		} elseif ($configType === 'schema') {
+			$config = $this->config->schema();
+		} else { // meta
+			$config = $this->config->meta();
+		}
+
+		if (!$type) {
+			$type = (array_key_exists('type', $config)) ? $config['type'] : 'WebPage';
+		}
+		$fields = ($configType === 'meta') ? $this->registry->getMetaFields() : $this->registry->getFieldsForType($type);
+		$registry = $this->registry->getTypeDefinition($type);
+        ob_start();
+		?>
+		<form data-save="seo" data-content="<?=$key?>">
+			<input type="hidden" name="context" value="<?=$key?>">
+			<fieldset>
+				<legend><?= $this->registry->getTypeDefinition($type)['label']??ucwords($key) ?></legend>
+				<?php
+				$exclude = ['creator'];
+				foreach ($fields as $index => $fieldName) {
+					if (in_array($fieldName, $exclude) ) {
+						continue;
+					}
+					if ($index === 0 && $fieldName !== 'type') {
+						echo '<div class="seo-'.$type.'">';
+					}
+					$fieldConfig = $this->registry->getFieldDefinition($fieldName);
+
+					$this->form->render($fieldName, $config[$fieldName]??'', $fieldConfig);
+					if ($index === 0 && $fieldName === 'type') {
+						echo '<div class="seo-'.$type.'">';
+					}
+				}
+				?>
+			</div>
+			</fieldset>
+			<div class="row nowrap">
+				<button type="button" data-action="reset" style="width:max-content"><?= jvbDashIcon('arrow-counter-clockwise')?> Reset</button>
+				<button type="submit"><?=jvbDashIcon('floppy-disk') ?> Save <?=$registry['label']??ucwords($key)?></button>
+			</div>
+		</form>
+		<?php
+        return ob_get_clean();
+    }
+
+	protected function renderMain():string
+	{
+		$business = ConfigManager::for('organization');
+		$savedBusiness = $business->schema()['type'] ?? 'Organization';
+
+		$tabs = new Tabs();
+
+		$tabs->addTab('website')
+			->title('WebSite Schema')
+			->icon('globe-simple')
+			->description('This is the main definition for your website')
+			->content($this->renderForm('website', 'WebSite'));
+
+		$tabs->addTab('organization')
+			->title('Organization Schema')
+			->icon('storefront')
+			->description('Define your organization or local business here.')
+			->content($this->renderForm('organization', $savedBusiness));
+
+		return $tabs->render();
+	}
+
+	protected function renderConfig(string $type):string
+	{
+		$types = ['meta', 'schema'];
+
+		switch ($type) {
+			case 'content':
+				$config = JVB_CONTENT;
+				$types[] = 'archive';
+				break;
+			case 'taxonomy':
+			case 'taxonomies':
+				$config = JVB_TAXONOMY;
+				break;
+			case 'user':
+				$config = JVB_USER;
+				break;
+			default:
+				error_log('[SEOAdminPage]:renderConfig --- no config found for '.$type);
+				return '';
+		}
+
+		$mainTabs = new Tabs();
+
+		foreach ($config as $c => $opt) {
+			$subTabs = new Tabs();
+
+			foreach ($types as $t) {
+				$tab = $subTabs->addTab($c.'_'.$t);
+
+				switch ($t) {
+					case 'meta':
+						$tab->title('Meta')
+							->icon('folders')
+							->description('The title and description are used when a link is shared to social media and a preview shows, or in the search engine result for this page.')
+							->content($this->renderForm($c, null, $t));
+						break;
+
+					case 'schema':
+						$tab->title('Schema')
+							->icon('robot')
+							->description('Defining the schema helps search engines understand what the content of this page is about.')
+							->content($this->renderForm($c, null, $t));
+						break;
+
+					case 'archive':
+						$tab->title('Archive')
+							->icon('hard-drives')
+							->description('The archive is similar to the per-post schema for this content, but is generally a CollectionPage of some sort.')
+							->content($this->renderForm($c, null, $t));
+						break;
+				}
+			}
+
+			$mainTabs->addTab($c)
+				->title($opt['plural'])
+				->icon($opt['icon'])
+				->content($subTabs->render());
+		}
+
+		return $mainTabs->render();
+	}
+
+    /**
+     * Render admin styles
+     */
+    private function renderStyles(): void
+    {
+		jvbInlineStyles('forms');
+    }
+
+	protected function renderTemplates():void
+	{
+		$types = array_keys($this->registry->schemaTypes);
+		foreach ($types as $type) {
+			?>
+			<template class="seo-<?=$type?>">
+				<div class="seo-<?=$type?>">
+					<?php
+					$fields = $this->registry->getFieldsForType($type);
+					foreach ($fields as $fieldName) {
+						$config = $this->registry->getFieldDefinition($fieldName);
+						$this->form->render($fieldName, '', $config);
+					}
+					?>
+				</div>
+			</template>
+			<?php
+		}
+	}
+}
diff --git a/inc/managers/SEO/SchemaBuilder.php b/inc/managers/SEO/SchemaBuilder.php
new file mode 100644
index 0000000..12bf00f
--- /dev/null
+++ b/inc/managers/SEO/SchemaBuilder.php
@@ -0,0 +1,1735 @@
+<?php
+namespace JVBase\managers\SEO;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Schema.org Builder - Fluent API for field and type definitions
+ *
+ * Usage:
+ * - Define fields: $schema->field('custom_name')->type('text')->label('Custom Label')
+ * - Use presets: $schema->preset('name')->label('Override Label')
+ * - Define types: $schema->type('WebSite')->fields(['name', 'url', 'description'])
+ */
+class SchemaBuilder
+{
+	private static ?self $instance = null;
+	private array $fieldDefinitions = [];
+	private array $typeDefinitions = [];
+	private array $typeGroups = [];
+
+	private ?FieldBuilder $currentField = null;
+	private ?TypeBuilder $currentType = null;
+
+	public array $schemaTypes = [
+		'WebSite'			=> 'Web Site',
+		'Organization' 		=> 'Organization',
+		'LocalBusiness' 	=> '  - Local Business',
+		'TattooParlor' 		=> '    - - Tattoo Shop',
+		'HealthBusiness' 	=> '    - - Health Business',
+		'FoodEstablishment' => '    - - Restaurant',
+		'WebPage' 			=> 'Web Page',
+		'CollectionPage'	=> '  - Collection Page',
+		'DefinedTermSet'    => '  - Glossary/Collection',
+		'FAQPage'			=> '  - FAQ Page',
+		'Person'			=> 'Person',
+		'CreativeWork'		=> 'Creative Work',
+		'DefinedTerm'		=> '  - Defined Term',
+		'VisualArtwork'		=> '  - Visual Artwork',
+		'Tattoo'			=> '    - - Tattoo',
+		'BeforeAfter'		=> '  - Before & After',
+		'Product'			=> 'Product',
+		'Event'				=> 'Event',
+	];
+
+	private array $metaFields = ['metaTitle', 'metaDescription', 'socialPreviewImage', 'twitterImage'];
+
+	private array $defaultMetaValues = [
+		'title' => '{{post_title}} | {{site_name}}',
+		'description' => '{{post_excerpt}}',
+		'image' => '{{featured_image}}',
+		'twitter_image' => ''
+	];
+
+	public static function getInstance(): self
+	{
+		if (self::$instance === null) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+
+	private function __construct()
+	{
+		$this->registerPresetFields();
+		$this->registerTypes();
+		$this->registerTypeGroups();
+
+		do_action(BASE . 'schema_builder_loaded', $this);
+	}
+
+	/**
+	 * Start defining a custom field
+	 */
+	public function field(string $name): FieldBuilder
+	{
+		$this->currentField = new FieldBuilder($this, $name);
+		return $this->currentField;
+	}
+
+	/**
+	 * Start with a preset field (can be customized)
+	 */
+	public function preset(string $name): FieldBuilder
+	{
+		$presets = $this->getPresetDefinitions();
+
+		if (!isset($presets[$name])) {
+			throw new \InvalidArgumentException("Unknown preset field: {$name}");
+		}
+
+		$this->currentField = new FieldBuilder($this, $name, $presets[$name]);
+		return $this->currentField;
+	}
+
+	/**
+	 * Start defining a schema type
+	 */
+	public function type(string $typeName): TypeBuilder
+	{
+		$this->currentType = new TypeBuilder($this, $typeName);
+		return $this->currentType;
+	}
+
+	/**
+	 * Register a custom field definition
+	 */
+	public function registerField(string $fieldName, array $config): void
+	{
+		$this->fieldDefinitions[$fieldName] = $config;
+	}
+
+	/**
+	 * Register a custom type definition
+	 */
+	public function registerType(string $typeName, array $config): void
+	{
+		$this->typeDefinitions[$typeName] = $config;
+	}
+
+	/**
+	 * Get field definition
+	 */
+	public function getFieldDefinition(string $fieldName): ?array
+	{
+		$definitions = $this->getFieldDefinitions();
+		return $definitions[$fieldName] ?? null;
+	}
+
+	/**
+	 * Get all field definitions
+	 */
+	public function getFieldDefinitions(): array
+	{
+		return apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions);
+	}
+
+	/**
+	 * Get type definition
+	 */
+	public function getTypeDefinition(string $type): ?array
+	{
+		$definitions = $this->getTypeDefinitions();
+		return $definitions[$type] ?? null;
+	}
+
+	/**
+	 * Get all type definitions
+	 */
+	public function getTypeDefinitions(): array
+	{
+		return apply_filters(BASE . 'schema_type_definitions', $this->typeDefinitions);
+	}
+
+	public function getTypeGroups(): array
+	{
+		return $this->typeGroups;
+	}
+
+	public function getMetaFields(): array
+	{
+		return $this->metaFields;
+	}
+
+	public function getDefaultMetaValues(): array
+	{
+		return $this->defaultMetaValues;
+	}
+
+	/**
+	 * Get all fields for a specific type (with inheritance)
+	 */
+	public function getFieldsForType(string $type): array
+	{
+		$fields = [];
+
+		$typeDefinition = $this->getTypeDefinition($type);
+		if (!$typeDefinition) {
+			return $fields;
+		}
+
+		$fields = array_merge($fields, $typeDefinition['fields'] ?? []);
+
+		// Handle inheritance
+		if (!empty($typeDefinition['extends'])) {
+			$parentFields = $this->getFieldsForType($typeDefinition['extends']);
+			$fields = array_unique(array_merge($parentFields, $fields));
+		}
+
+		return $fields;
+	}
+
+	/**
+	 * Get MetaManager configuration for a schema type
+	 * This creates the form fields for the selected @type
+	 */
+	public function getMetaConfigForType(string $type): array
+	{
+		$fields = $this->getFieldsForType($type);
+		$config = [];
+
+		foreach ($fields as $fieldName) {
+			$fieldDef = $this->getFieldDefinition($fieldName);
+			if ($fieldDef) {
+				// Use the field name as the key (this IS the schema property)
+				$config[$fieldName] = $fieldDef;
+			}
+		}
+
+		return $config;
+	}
+
+	/**
+	 * Get types organized by group for UI display
+	 */
+	public function getTypesByGroup(): array
+	{
+		$types = $this->getTypeDefinitions();
+		$grouped = [];
+
+		foreach ($types as $typeName => $config) {
+			$group = $config['group'] ?? 'general';
+
+			if (!isset($grouped[$group])) {
+				$grouped[$group] = [
+					'label' => $this->typeGroups[$group] ?? ucfirst($group),
+					'types' => []
+				];
+			}
+
+			$grouped[$group]['types'][$typeName] = $config['label'] ?? $typeName;
+		}
+
+		return $grouped;
+	}
+
+	/**
+	 * Register a type group
+	 */
+	public function registerGroup(string $key, string $label): void
+	{
+		$this->typeGroups[$key] = $label;
+	}
+
+	/**
+	 * Get post types for select options
+	 */
+	public static function getContentPostTypes(): array
+	{
+		$options = ['' => '-- Select Post Type --'];
+
+		if (defined('JVB_CONTENT')) {
+			foreach (JVB_CONTENT as $key => $config) {
+				$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
+			}
+		}
+
+		return $options;
+	}
+
+	/**
+	 * Get taxonomies for select options
+	 */
+	public static function getContentTaxonomies(): array
+	{
+		$options = ['' => '-- Select Taxonomy --'];
+
+		if (defined('JVB_TAXONOMY')) {
+			foreach (JVB_TAXONOMY as $key => $config) {
+				$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
+			}
+		}
+
+		return $options;
+	}
+
+	/**
+	 * Define preset fields that can be reused
+	 */
+	private function registerPresetFields(): void
+	{
+		// Special type selector field
+		$this->field('type')
+			->type('select')
+			->label('Type')
+			->options(array_merge(['' => '-- Content Type'], $this->schemaTypes));
+
+		/**************************************************************
+		 * META FIELDS
+		 **************************************************************/
+		$this->field('metaTitle')
+			->type('text')
+			->label('Meta Title')
+			->description('Used in search results and when shared on social media. Leave blank to use default.');
+
+		$this->field('metaDescription')
+			->type('textarea')
+			->label('Meta Description')
+			->description('Brief description shown in search results and social previews.');
+
+		$this->field('socialPreviewImage')
+			->type('group')
+			->label('Social Preview Image')
+			->description('Image shown when shared on social media. Recommended: 1200x630px.')
+			->transformer('image_url_with_fallback')
+			->fields([
+				'source_field' => [
+					'type' => 'text',
+					'label' => 'Image Source Field',
+					'description' => 'Template field to get image from (e.g., {{post_thumbnail}}, {{custom_image_field}})',
+					'placeholder' => '{{post_thumbnail}}'
+				],
+				'fallback' => [
+					'type' => 'upload',
+					'label' => 'Fallback Image',
+					'description' => 'Used when source field returns no image'
+				]
+			]);
+
+		$this->field('twitterImage')
+			->type('group')
+			->label('Twitter Card Image')
+			->description('Separate image for Twitter. Falls back to main social image if empty.')
+			->transformer('image_url_with_fallback')
+			->fields([
+				'source_field' => [
+					'type' => 'text',
+					'label' => 'Image Source Field',
+					'placeholder' => '{{twitter_specific_image}}'
+				],
+				'fallback' => [
+					'type' => 'upload',
+					'label' => 'Fallback Image'
+				]
+			]);
+
+		/**************************************************************
+		 * QA FIELD FAQ
+		**************************************************************/
+		$this->field('question')
+			->type('text')
+			->label('Question')
+			->description('Template for the question (e.g., {{post_title}})')
+			->default('{{post_title}}')
+			->transformer('text');
+
+		$this->field('answer')
+			->type('textarea')
+			->label('Answer')
+			->description('Template for the answer (e.g., {{post_content}})')
+			->default('{{post_content}}')
+			->transformer('text');
+		/**************************************************************
+		 * CORE IDENTITY FIELDS
+		 **************************************************************/
+		$this->field('name')
+			->type('text')
+			->label('Name')
+			->description('The name of the item')
+			->transformer('text');
+
+		$this->field('alternateName')
+			->type('repeater')
+			->label('Alternate Name(s)')
+			->description('Alternative names or nicknames')
+			->transformer('text_array')
+			->fields([
+				'name' => [
+					'type' => 'text',
+					'label' => 'Name'
+				]
+			]);
+
+		$this->field('legalName')
+			->type('text')
+			->label('Legal Name')
+			->description('The official legal name')
+			->transformer('text');
+
+		$this->field('description')
+			->type('textarea')
+			->label('Description')
+			->description('A description of the item')
+			->transformer('text');
+
+		$this->field('disambiguatingDescription')
+			->type('textarea')
+			->label('Disambiguating Description')
+			->description('Brief clarification to distinguish from similar items')
+			->transformer('text');
+
+		$this->field('url')
+			->type('url')
+			->label('URL')
+			->description('Website URL')
+			->transformer('url');
+
+		$this->field('slogan')
+			->type('text')
+			->label('Slogan')
+			->description('A slogan or tagline')
+			->transformer('text');
+
+		/**************************************************************
+		 * BEFORE/AFTER FIELDS
+		 **************************************************************/
+		$this->field('about')
+			->type('reference')
+			->label('About (Service/Topic)')
+			->transformer('reference');
+
+		$this->field('temporalCoverage')
+			->type('text')
+			->label('Time Period')
+			->description('ISO 8601 format: 2024-01-10/2024-09-01')
+			->transformer('text');
+
+		$this->field('associatedMedia')
+			->type('repeater')
+			->label('Associated Media')
+			->transformer('image_object_array')
+			->fields([
+				'image' => ['type' => 'image', 'label' => 'Image'],
+				'caption' => ['type' => 'text', 'label' => 'Caption'],
+				'position' => ['type' => 'number', 'label' => 'Position'],
+			]);
+
+		$this->field('additionalProperty')
+			->type('repeater')
+			->label('Additional Properties')
+			->transformer('property_value_array')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Property Name'],
+				'value' => ['type' => 'text', 'label' => 'Value'],
+			]);
+
+		/**************************************************************
+		 * IMAGE FIELDS
+		 **************************************************************/
+		$this->field('image')
+			->type('upload')
+			->label('Image')
+			->description('Primary image')
+			->transformer('image_object');
+
+		$this->field('logo')
+			->type('upload')
+			->label('Logo')
+			->transformer('image_object');
+
+		$this->field('photo')
+			->type('upload')
+			->label('Photo of Location')
+			->transformer('image_object');
+
+		$this->field('video')
+			->type('upload')
+			->label('Video')
+			->transformer('video_object');
+
+		/**************************************************************
+		 * LOCATION & CONTACT FIELDS
+		 **************************************************************/
+		$this->field('location')
+			->type('location')
+			->label('Location')
+			->description('Physical location with address and coordinates')
+			->transformer('location_complex');
+
+		$this->field('address')
+			->type('location')
+			->label('Address')
+			->description('Postal address')
+			->transformer('postal_address');
+
+		$this->field('geo')
+			->type('group')
+			->label('Geographic Coordinates')
+			->description('Latitude and longitude')
+			->transformer('geo_coordinates')
+			->fields([
+				'latitude' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Latitude',
+				],
+				'longitude' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Longitude',
+				]
+			]);
+
+		$this->field('telephone')
+			->type('text')
+			->label('Telephone')
+			->description('Phone number')
+			->transformer('text');
+
+		$this->field('faxNumber')
+			->type('text')
+			->label('Fax Number')
+			->transformer('text');
+
+		$this->field('email')
+			->type('email')
+			->label('Email')
+			->description('Email address')
+			->transformer('email');
+
+		$this->field('contactPoint')
+			->type('repeater')
+			->label('Contact Points')
+			->description('Additional contact methods')
+			->transformer('contact_point_array')
+			->fields([
+				'contactType' => [
+					'type' => 'text',
+					'label' => 'Contact Type',
+					'description' => 'e.g., customer service, sales',
+				],
+				'telephone' => [
+					'type' => 'text',
+					'label' => 'Phone',
+				],
+				'email' => [
+					'type' => 'email',
+					'label' => 'Email',
+				]
+			]);
+
+		$this->field('potentialAction')
+			->type('repeater')
+			->label('Potential Actions')
+			->transformer('potential_action_array')
+			->fields([
+				'action' => [
+					'type' => 'radio',
+					'label' => 'Action',
+					'options' => [
+						'searchAction' => 'Search Action',
+						'communicateAction' => 'Contact Action',
+						'scheduleAction' => 'Reserve Action',
+						'applyAction' => 'Estimate Action'
+					]
+				],
+				'name' => [
+					'type' => 'text',
+					'label' => 'Name',
+				],
+				'target' => [
+					'type' => 'url',
+					'label' => 'Action URL',
+				],
+				'description' => [
+					'type' => 'textarea',
+					'label' => 'Description'
+				]
+			])
+			->default([
+				[
+					'action' => 'searchAction',
+					'target' => get_home_url(null, '/search/?s={query}')
+				]
+			]);
+
+		/**************************************************************
+		 * HOURS & OPERATIONAL FIELDS
+		 **************************************************************/
+		$this->field('openingHours')
+			->type('group')
+			->label('Opening Hours')
+			->description('Business hours specification')
+			->transformer('opening_hours_specification')
+			->fields([
+				'monday' => [
+					'type' => 'group',
+					'label' => 'Monday',
+					'fields' => [
+						'opens' => ['type' => 'time', 'label' => 'Opens'],
+						'closes' => ['type' => 'time', 'label' => 'Closes']
+					]
+				],
+				'tuesday' => [
+					'type' => 'group',
+					'label' => 'Tuesday',
+					'fields' => [
+						'opens' => ['type' => 'time', 'label' => 'Opens'],
+						'closes' => ['type' => 'time', 'label' => 'Closes']
+					]
+				],
+				'wednesday' => [
+					'type' => 'group',
+					'label' => 'Wednesday',
+					'fields' => [
+						'opens' => ['type' => 'time', 'label' => 'Opens'],
+						'closes' => ['type' => 'time', 'label' => 'Closes']
+					]
+				],
+				'thursday' => [
+					'type' => 'group',
+					'label' => 'Thursday',
+					'fields' => [
+						'opens' => ['type' => 'time', 'label' => 'Opens'],
+						'closes' => ['type' => 'time', 'label' => 'Closes']
+					]
+				],
+				'friday' => [
+					'type' => 'group',
+					'label' => 'Friday',
+					'fields' => [
+						'opens' => ['type' => 'time', 'label' => 'Opens'],
+						'closes' => ['type' => 'time', 'label' => 'Closes']
+					]
+				],
+				'saturday' => [
+					'type' => 'group',
+					'label' => 'Saturday',
+					'fields' => [
+						'opens' => ['type' => 'time', 'label' => 'Opens'],
+						'closes' => ['type' => 'time', 'label' => 'Closes']
+					]
+				],
+				'sunday' => [
+					'type' => 'group',
+					'label' => 'Sunday',
+					'fields' => [
+						'opens' => ['type' => 'time', 'label' => 'Opens'],
+						'closes' => ['type' => 'time', 'label' => 'Closes']
+					]
+				],
+			]);
+
+		$this->field('hasPart')
+			->type('repeater')
+			->label('Site Navigation')
+			->description('Main navigation menu items')
+			->transformer('navigation_array')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Link Text'],
+				'url' => ['type' => 'url', 'label' => 'URL'],
+				'description' => ['type' => 'textarea', 'label' => 'Description (optional)'],
+			]);
+
+		$this->field('priceRange')
+			->type('text')
+			->label('Price Range')
+			->description('e.g., $$, $100-$500')
+			->transformer('text');
+
+		$this->field('currenciesAccepted')
+			->type('checkbox')
+			->label('Currencies Accepted')
+			->options(['CAD' => 'CAD', 'USD' => 'USD'])
+			->transformer('text_array');
+
+		$this->field('paymentAccepted')
+			->type('checkbox')
+			->label('Payment Methods')
+			->options([
+				'Cash' => 'Cash',
+				'Credit Card' => 'Credit Card',
+				'Debit' => 'Debit',
+				'Google Pay' => 'Google Pay',
+				'Apple Pay' => 'Apple Pay',
+				'PayPal' => 'PayPal',
+				'Interac' => 'Interac',
+				'AMEX' => 'AMEX',
+			])
+			->transformer('text_array');
+
+		/**************************************************************
+		 * ORGANIZATION & BUSINESS FIELDS
+		 **************************************************************/
+		$this->field('foundingDate')
+			->type('date')
+			->label('Founding Date')
+			->description('Date the organization was founded')
+			->transformer('date');
+
+		$this->field('dissolutionDate')
+			->type('date')
+			->label('Dissolution Date')
+			->description('Date the organization closed')
+			->transformer('date');
+
+		$this->field('founders')
+			->type('repeater')
+			->label('Founders')
+			->description('Name of founder(s)')
+			->transformer('person_array')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Name'],
+				'url' => ['type' => 'url', 'label' => 'URL'],
+			]);
+
+		$this->field('numberOfEmployees')
+			->type('text')
+			->label('Number of Employees')
+			->transformer('number');
+
+		$this->field('taxID')
+			->type('text')
+			->label('Tax ID')
+			->description('Tax identification number')
+			->transformer('text');
+
+		$this->field('vatID')
+			->type('text')
+			->label('VAT ID')
+			->description('VAT registration number')
+			->transformer('text');
+
+		$this->field('duns')
+			->type('text')
+			->label('D-U-N-S Number')
+			->description('Dun & Bradstreet number')
+			->transformer('text');
+
+		/**************************************************************
+		 * SOCIAL & LINKS
+		 **************************************************************/
+		$this->field('sameAs')
+			->type('repeater')
+			->label('Social Media & Links')
+			->description('URLs to social profiles and related pages')
+			->transformer('url_array')
+			->fields([
+				'url' => ['type' => 'url', 'label' => 'URL']
+			]);
+
+		/**************************************************************
+		 * AREA & GEOGRAPHY
+		 **************************************************************/
+		$this->field('areaServed')
+			->type('repeater')
+			->label('Area Served')
+			->description('Geographic areas served')
+			->transformer('text_array')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Location Name'],
+				'url' => ['type' => 'url', 'label' => 'Wikipedia Page'],
+			]);
+
+		$this->field('hasMap')
+			->type('url')
+			->label('Map URL')
+			->description('Link to a map (e.g., Google Maps)')
+			->transformer('url');
+
+		/**************************************************************
+		 * AMENITIES & FEATURES
+		 **************************************************************/
+		$this->field('amenityFeature')
+			->type('checkbox')
+			->label('Amenity Features')
+			->description('Available facilities and features')
+			->transformer('text')
+			->options([
+				'Wheelchair Accessible' => 'Wheelchair Accessible',
+				'Free Parking' => 'Free Parking',
+				'Private Rooms' => 'Private Rooms',
+				'Air Conditioning' => 'Air Conditioning',
+				'WiFi' => 'WiFi',
+				'Gender Neutral Restroom' => 'Gender Neutral Restroom',
+				'LGBTQ+ Friendly' => 'LGBTQ+ Friendly',
+				'Sterilization Room' => 'Sterilization Room',
+				'Refreshments Available' => 'Refreshments Available',
+				'Street Level Access' => 'Street Level Access',
+				'Single Use Needles' => 'Single Use Needles',
+				'Consultation Room' => 'Consultation Room',
+				'Aftercare Products Available' => 'Aftercare Products Available',
+				'Walk-Ins Welcome' => 'Walk-Ins Welcome',
+				'By Appointment' => 'By Appointment Only',
+			]);
+
+		/**************************************************************
+		 * LANGUAGES
+		 **************************************************************/
+		$this->field('availableLanguage')
+			->type('repeater')
+			->label('Languages Available')
+			->description('Languages spoken or supported')
+			->transformer('language_array')
+			->fields([
+				'language' => ['type' => 'text', 'label' => 'Language']
+			]);
+
+		$this->field('knowsLanguage')
+			->type('repeater')
+			->label('Languages Known')
+			->description('Languages the person knows')
+			->transformer('language_array')
+			->fields([
+				'language' => ['type' => 'text', 'label' => 'Language']
+			]);
+
+		$this->field('inLanguage')
+			->type('radio')
+			->label('In Language')
+			->options([
+				'en-CA' => 'English, Canadian',
+				'en-US' => 'English, American',
+				'fr-CA' => 'French, Canadian'
+			])
+			->transformer('text');
+
+		/**************************************************************
+		 * RATINGS & REVIEWS
+		 **************************************************************/
+		$this->field('aggregateRating')
+			->type('group')
+			->label('Aggregate Rating')
+			->description('Overall rating and review count')
+			->transformer('aggregate_rating')
+			->fields([
+				'ratingValue' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Rating Value',
+					'description' => 'Average rating (e.g., 4.5)',
+				],
+				'bestRating' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Best Rating',
+					'default' => 5,
+					'description' => 'Highest possible rating (e.g., 5)',
+				],
+				'worstRating' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Worst Rating',
+					'default' => 1,
+					'description' => 'Lowest possible rating (e.g., 1)',
+				],
+				'ratingCount' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Rating Count',
+					'description' => 'Total number of ratings',
+				],
+				'reviewCount' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Review Count',
+					'description' => 'Total number of reviews',
+				]
+			]);
+
+		/**************************************************************
+		 * KEYWORDS & CATEGORIZATION
+		 **************************************************************/
+		$this->field('keywords')
+			->type('repeater')
+			->label('Keywords')
+			->description('Keywords or tags')
+			->transformer('text_array')
+			->fields([
+				'keyword' => ['type' => 'text', 'label' => 'Keyword']
+			]);
+
+		/**************************************************************
+		 * PERSON FIELDS
+		 **************************************************************/
+		$this->field('givenName')
+			->type('text')
+			->label('First Name')
+			->transformer('text');
+
+		$this->field('familyName')
+			->type('text')
+			->label('Last Name')
+			->transformer('text');
+
+		$this->field('honorificPrefix')
+			->type('text')
+			->label('Honorific Prefix')
+			->description('e.g., Dr., Mr., Ms.')
+			->transformer('text');
+
+		$this->field('honorificSuffix')
+			->type('text')
+			->label('Honorific Suffix')
+			->description('e.g., PhD, MD')
+			->transformer('text');
+
+		$this->field('jobTitle')
+			->type('text')
+			->label('Job Title')
+			->transformer('text');
+
+		$this->field('birthDate')
+			->type('date')
+			->label('Birth Date')
+			->description('For public figures')
+			->transformer('date');
+
+		$this->field('gender')
+			->type('text')
+			->label('Gender')
+			->transformer('text');
+
+		/**************************************************************
+		 * CREATIVE WORK FIELDS
+		 **************************************************************/
+		$this->field('author')
+			->type('text')
+			->label('Author')
+			->description('Author name or reference')
+			->transformer('person_reference');
+
+		$this->field('creator')
+			->type('text')
+			->label('Creator')
+			->description('Creator name or reference')
+			->transformer('text');
+
+		$this->field('dateCreated')
+			->type('text')
+			->label('Date Created')
+			->transformer('text');
+
+		$this->field('datePublished')
+			->type('text')
+			->label('Date Published')
+			->default('{{post_date')
+			->transformer('text');
+
+		$this->field('dateModified')
+			->type('text')
+			->default('{{post_modified}}')
+			->label('Date Modified')
+			->transformer('text');
+
+		/**************************************************************
+		 * VISUAL ARTWORK FIELDS
+		 **************************************************************/
+		$this->field('artform')
+			->type('text')
+			->label('Art Form')
+			->description('e.g., Painting, Sculpture, Tattoo')
+			->transformer('text');
+
+		$this->field('artMedium')
+			->type('text')
+			->label('Art Medium')
+			->description('e.g., Oil, Watercolor, Ink')
+			->transformer('text');
+
+		$this->field('artworkSurface')
+			->type('text')
+			->label('Artwork Surface')
+			->description('e.g., Canvas, Paper, Skin')
+			->transformer('text');
+
+		$this->field('width')
+			->type('text')
+			->label('Width')
+			->description('Width with unit (e.g., 10cm, 5in)')
+			->transformer('dimension');
+
+		$this->field('height')
+			->type('text')
+			->label('Height')
+			->description('Height with unit (e.g., 15cm, 8in)')
+			->transformer('dimension');
+
+		/**************************************************************
+		 * EVENT FIELDS
+		 **************************************************************/
+		$this->field('startDate')
+			->type('text')
+			->default('{{start_date}}')
+			->label('Start Date/Time')
+			->transformer('text');
+
+		$this->field('endDate')
+			->type('text')
+			->default('{{end_date}}')
+			->label('End Date/Time')
+			->transformer('text');
+
+		$this->field('eventStatus')
+			->type('select')
+			->label('Event Status')
+			->options([
+				'https://schema.org/EventScheduled' => 'Scheduled',
+				'https://schema.org/EventCancelled' => 'Cancelled',
+				'https://schema.org/EventPostponed' => 'Postponed',
+				'https://schema.org/EventRescheduled' => 'Rescheduled',
+			])
+			->transformer('text');
+
+		$this->field('eventAttendanceMode')
+			->type('select')
+			->label('Attendance Mode')
+			->options([
+				'https://schema.org/OfflineEventAttendanceMode' => 'In-Person',
+				'https://schema.org/OnlineEventAttendanceMode' => 'Online',
+				'https://schema.org/MixedEventAttendanceMode' => 'Mixed/Hybrid',
+			])
+			->transformer('text');
+
+		/**************************************************************
+		 * PRODUCT FIELDS
+		 **************************************************************/
+		$this->field('brand')
+			->type('group')
+			->label('Brand')
+			->transformer('brand_object')
+			->fields([
+				'type' => [
+					'type' => 'select',
+					'label' => 'Brand Type',
+					'options' => [
+						'text' => 'Text Only',
+						'organization' => 'Organization/Brand',
+					]
+				],
+				'name' => [
+					'type' => 'text',
+					'label' => 'Brand Name',
+				],
+				'url' => [
+					'type' => 'url',
+					'label' => 'Brand Website',
+					'condition' => [
+						'field' => 'type',
+						'value' => 'organization'
+					]
+				],
+				'logo' => [
+					'type' => 'upload',
+					'label' => 'Brand Logo',
+					'condition' => [
+						'field' => 'type',
+						'value' => 'organization'
+					]
+				],
+			]);
+
+		$this->field('sku')
+			->type('text')
+			->label('SKU')
+			->description('Stock Keeping Unit')
+			->transformer('text');
+
+		$this->field('gtin')
+			->type('text')
+			->label('GTIN')
+			->description('Global Trade Item Number')
+			->transformer('text');
+
+		/**************************************************************
+		 * SERVICES & OFFERS
+		 **************************************************************/
+		$this->field('hasOfferCatalog')
+			->type('group')
+			->label('Offer Catalog')
+			->transformer('offer_catalog_array')
+			->fields([
+				'source' => [
+					'type' => 'select',
+					'label' => 'Source',
+					'options' => [
+						'auto' => 'Auto from post type',
+						'manual' => 'Manual entry',
+					]
+				],
+				'post_type' => [
+					'type' => 'select',
+					'label' => 'Post Type',
+					'options' => self::getContentPostTypes(),
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'group_by_taxonomy' => [
+					'type' => 'true_false',
+					'label' => 'Group by category/taxonomy',
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'taxonomy' => [
+					'type' => 'select',
+					'label' => 'Taxonomy',
+					'options' => self::getContentTaxonomies(),
+					'condition' => ['field' => 'group_by_taxonomy', 'value' => '1']
+				],
+				'manual_items' => [
+					'type' => 'repeater',
+					'label' => 'Manual Offers',
+					'condition' => ['field' => 'source', 'value' => 'manual'],
+					'fields' => [
+						'type'	=> ['type' => 'radio', 'label' => 'Type', 'options' => ['Service' => 'Service', 'Product' => 'Product']],
+						'name' => ['type' => 'text', 'label' => 'Offer Name'],
+						'description' => ['type' => 'textarea', 'label' => 'Description'],
+						'price'	=> ['type' => 'text', 'label' => 'Price'],
+					]
+				]
+			]);
+
+		$this->field('knowsAbout')
+			->type('repeater')
+			->label('Areas of Expertise')
+			->description('Skills and specialties')
+			->transformer('text_array')
+			->fields([
+				'topic' => ['type' => 'text', 'label' => 'Topic']
+			]);
+
+		/**************************************************************
+		 * CREDENTIALS & CERTIFICATIONS
+		 **************************************************************/
+		$this->field('hasCredential')
+			->type('repeater')
+			->label('Credentials / Certifications')
+			->description('Professional certifications')
+			->transformer('credential_array')
+			->fields([
+				'credentialCategory' => ['type' => 'text', 'label' => 'Category'],
+				'name' => ['type' => 'text', 'label' => 'Name'],
+				'issuedBy' => ['type' => 'text', 'label' => 'Issued By']
+			]);
+
+		$this->field('award')
+			->type('repeater')
+			->label('Awards & Recognition')
+			->transformer('text_array')
+			->fields([
+				'award' => ['type' => 'text', 'label' => 'Award']
+			]);
+
+		$this->field('serviceArea')
+			->type('repeater')
+			->label('Service Areas')
+			->description('Geographic areas served (cities, neighborhoods, or radius)')
+			->transformer('service_area_array')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Area Name'],
+				'type' => [
+					'type' => 'select',
+					'label' => 'Type',
+					'options' => [
+						'City' => 'City',
+						'AdministrativeArea' => 'Region/Province',
+						'GeoCircle' => 'Radius',
+					]
+				],
+				'radius' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Radius (km)'],
+			]);
+
+		$this->field('makesOffer')
+			->type('group')
+			->label('Featured Offerings')
+			->transformer('offers_from_posts')
+			->fields([
+				'source' => [
+					'type' => 'select',
+					'label' => 'Source',
+					'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry']
+				],
+				'post_type' => [
+					'type' => 'select',
+					'label' => 'Post Type',
+					'options' => self::getContentPostTypes(),
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'limit' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Featured Count',
+					'default' => 5,
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'manual_items' => [
+					'type' => 'repeater',
+					'label' => 'Manual Offers',
+					'condition' => ['field' => 'source', 'value' => 'manual'],
+					'fields' => [
+						'name' => ['type' => 'text', 'label' => 'Offer Name'],
+						'description' => ['type' => 'textarea', 'label' => 'Description'],
+						'price' => ['type' => 'text', 'label' => 'Price/Range'],
+					]
+				]
+			]);
+
+		$this->field('hasMenu')
+			->type('group')
+			->label('Menu Items')
+			->description('Auto-populate from post type or enter manually')
+			->transformer('menu_from_posts')
+			->fields([
+				'source' => [
+					'type' => 'select',
+					'label' => 'Source',
+					'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry']
+				],
+				'post_type' => [
+					'type' => 'select',
+					'label' => 'Post Type',
+					'options' => self::getContentPostTypes(),
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'limit' => [
+					'type' => 'text',
+					'subtype' => 'number',
+					'label' => 'Number of items',
+					'default' => 10,
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'orderby' => [
+					'type' => 'select',
+					'label' => 'Order By',
+					'options' => ['menu_order' => 'Menu Order', 'title' => 'Title', 'date' => 'Date'],
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'manual_items' => [
+					'type' => 'repeater',
+					'label' => 'Manual Items',
+					'condition' => ['field' => 'source', 'value' => 'manual'],
+					'fields' => [
+						'name' => ['type' => 'text', 'label' => 'Item Name'],
+						'description' => ['type' => 'textarea', 'label' => 'Description'],
+						'price' => ['type' => 'text', 'label' => 'Price'],
+					]
+				]
+			]);
+
+		/**************************************************************
+		 * FAQ FIELDS
+		 **************************************************************/
+		$this->field('faq')
+			->type('repeater')
+			->label('FAQ Items')
+			->description('Question and Answer pairs')
+			->transformer('faq_array')
+			->fields([
+				'question' => ['type' => 'text', 'label' => 'Question'],
+				'answer' => ['type' => 'text', 'label' => 'Answer']
+			]);
+
+		/**************************************************************
+		 * FOOD & CUISINE
+		 **************************************************************/
+		$this->field('servesCuisine')
+			->type('repeater')
+			->label('Cuisine Types')
+			->description('Types of cuisine served')
+			->transformer('text_array')
+			->fields([
+				'cuisine' => ['type' => 'text', 'label' => 'Cuisine Type', 'description' => 'e.g., Italian, Mexican, Vegan']
+			]);
+
+		$this->field('menu')
+			->type('url')
+			->label('Menu URL')
+			->description('Link to online menu')
+			->transformer('url');
+
+		/**************************************************************
+		 * PRODUCT/OFFER FIELDS
+		 **************************************************************/
+		$this->field('offers')
+			->type('group')
+			->label('Offer Details')
+			->description('Price and availability information')
+			->transformer('offer_object')
+			->fields([
+				'price' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Price'],
+				'priceCurrency' => ['type' => 'text', 'label' => 'Currency', 'default' => 'USD'],
+				'availability' => [
+					'type' => 'select',
+					'label' => 'Availability',
+					'options' => [
+						'InStock' => 'In Stock',
+						'PreOrder' => 'Pre-Order',
+						'SoldOut' => 'Sold Out',
+						'OutOfStock' => 'Out of Stock',
+						'Discontinued' => 'Discontinued',
+					]
+				],
+				'validFrom' => ['type' => 'text', 'label' => 'Valid From', 'default' => '{{validFrom}}'],
+				'validThrough' => ['type' => 'text', 'label' => 'Valid Through', 'default' => '{{validTo}}'],
+			]);
+
+		$this->field('mpn')
+			->type('text')
+			->label('Manufacturer Part Number')
+			->transformer('text');
+
+		/**************************************************************
+		 * BUSINESS POLICIES & FEATURES
+		 **************************************************************/
+		$this->field('isAccessibleForFree')
+			->type('true_false')
+			->label('Accessible For Free')
+			->description('Is this service/location accessible without payment?')
+			->transformer('boolean');
+
+		$this->field('smokingAllowed')
+			->type('true_false')
+			->label('Smoking Allowed')
+			->transformer('boolean');
+
+		$this->field('petsAllowed')
+			->type('select')
+			->label('Pets Allowed')
+			->options([
+				'' => 'Not specified',
+				'yes' => 'Yes',
+				'no' => 'No',
+			])
+			->transformer('boolean');
+
+		/**************************************************************
+		 * ORGANIZATION RELATIONSHIPS
+		 **************************************************************/
+		$this->field('parentOrganization')
+			->type('group')
+			->label('Parent Organization')
+			->description('Organization this is a part of')
+			->transformer('organization_reference')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Organization Name'],
+				'url' => ['type' => 'url', 'label' => 'Website'],
+			]);
+
+		$this->field('subOrganization')
+			->type('repeater')
+			->label('Sub-Organizations')
+			->description('Child organizations or departments')
+			->transformer('organization_reference_array')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Organization Name'],
+				'url' => ['type' => 'url', 'label' => 'Website'],
+			]);
+
+		$this->field('employee')
+			->type('repeater')
+			->label('Employees')
+			->transformer('person_reference_array')
+			->fields([
+				'name' => ['type' => 'text', 'label' => 'Name'],
+				'jobTitle' => ['type' => 'text', 'label' => 'Job Title'],
+			]);
+
+		/**************************************************************
+		 * HOSPITALITY
+		 **************************************************************/
+		$this->field('checkinTime')
+			->type('time')
+			->label('Check-in Time')
+			->transformer('time');
+
+		$this->field('checkoutTime')
+			->type('time')
+			->label('Check-out Time')
+			->transformer('time');
+
+		$this->field('starRating')
+			->type('group')
+			->label('Star Rating')
+			->transformer('rating_object')
+			->fields([
+				'ratingValue' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5]
+			]);
+
+		/**************************************************************
+		 * REVIEW & RATING
+		 **************************************************************/
+		$this->field('review')
+			->type('repeater')
+			->label('Reviews')
+			->transformer('review_array')
+			->fields([
+				'author' => ['type' => 'text', 'label' => 'Reviewer Name'],
+				'reviewRating' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5],
+				'reviewBody' => ['type' => 'textarea', 'label' => 'Review Text'],
+				'datePublished' => ['type' => 'date', 'label' => 'Date'],
+			]);
+
+		/**************************************************************
+		 * HEALTH & MEDICAL
+		 **************************************************************/
+		$this->field('medicalSpecialty')
+			->type('repeater')
+			->label('Medical Specialties')
+			->transformer('text_array')
+			->fields([
+				'specialty' => ['type' => 'text', 'label' => 'Specialty']
+			]);
+
+		$this->field('healthcareService')
+			->type('repeater')
+			->label('Healthcare Services')
+			->transformer('text_array')
+			->fields([
+				'service' => ['type' => 'text', 'label' => 'Service']
+			]);
+
+		/***************************************************************
+
+		 ***************************************************************/
+		$this->field('termCode')
+			->type('text')
+			->label('Term Code')
+			->description('Unique identifier or code for this term')
+			->transformer('text');
+
+		$this->field('hasDefinedTerm')
+			->type('group')
+			->label('Defined Terms')
+			->description('Terms included in this glossary or collection')
+			->transformer('defined_terms_from_posts')
+			->fields([
+				'source' => [
+					'type' => 'select',
+					'label' => 'Source',
+					'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry']
+				],
+				'post_type' => [
+					'type' => 'select',
+					'label' => 'Post Type',
+					'options' => self::getContentPostTypes(),
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				],
+				'taxonomy' => [
+					'type' => 'select',
+					'label' => 'Filter by Taxonomy',
+					'options' => self::getContentTaxonomies(),
+					'condition' => ['field' => 'source', 'value' => 'auto']
+				]
+			]);
+	}
+
+
+	/**
+	 * Get raw preset definitions (before filters)
+	 */
+	private function getPresetDefinitions(): array
+	{
+		return $this->fieldDefinitions;
+	}
+
+	/**
+	 * Define schema types
+	 */
+	private function registerTypes(): void
+	{
+		/**************************************************************
+		 * GENERAL / SITE-WIDE
+		 **************************************************************/
+		$this->type('WebSite')
+			->label('Website')
+			->group('general')
+			->fields([
+				'name',
+				'description',
+				'url',
+				'inLanguage',
+				'potentialAction',
+				'hasPart',
+				'creator',
+			]);
+
+		/**************************************************************
+		 * PAGE TYPES
+		 **************************************************************/
+		$this->type('WebPage')
+			->label('Web Page')
+			->group('page')
+			->fields([
+				'type',
+				'name',
+				'description',
+				'url',
+				'image',
+				'datePublished',
+				'dateModified',
+				'author',
+			]);
+
+		$this->type('CollectionPage')
+			->label('Collection Page')
+			->group('page')
+			->extends('WebPage');
+
+		$this->type('FAQPage')
+			->label('FAQ Page')
+			->group('page')
+			->extends('WebPage')
+			->addFields([
+				'question',
+				'answer'
+			]);
+
+		/**************************************************************
+		 * ORGANIZATION & BUSINESS
+		 **************************************************************/
+		$this->type('Organization')
+			->label('Organization')
+			->group('business')
+			->fields([
+				'type',
+				'name',
+				'legalName',
+				'alternateName',
+				'description',
+				'url',
+				'logo',
+				'image',
+				'email',
+				'telephone',
+				'sameAs',
+				'founders',
+				'foundingDate',
+				'numberOfEmployees',
+				'taxID',
+				'vatID',
+				'duns',
+				'slogan',
+				'disambiguatingDescription',
+			]);
+
+		$this->type('LocalBusiness')
+			->label('Local Business')
+			->group('business')
+			->extends('Organization')
+			->addFields([
+				'location',
+				'openingHours',
+				'priceRange',
+				'currenciesAccepted',
+				'paymentAccepted',
+				'serviceArea',
+				'areaServed',
+				'hasMap',
+				'amenityFeature',
+				'availableLanguage',
+				'hasOfferCatalog',
+				'makesOffer',
+				'hasMenu',
+				'knowsAbout',
+				'hasCredential',
+				'aggregateRating',
+				'review',
+				'award',
+			]);
+
+		$this->type('TattooParlor')
+			->label('Tattoo Parlor')
+			->group('business')
+			->extends('LocalBusiness')
+			->addFields([
+				'makesOffer',
+				'hasOfferCatalog',
+				'award',
+			]);
+
+		$this->type('HealthBusiness')
+			->label('Health Business')
+			->group('business')
+			->extends('LocalBusiness');
+
+		$this->type('FoodEstablishment')
+			->label('Food Establishment')
+			->group('business')
+			->extends('LocalBusiness')
+			->addFields([
+				'hasMenu',
+				'servesCuisine',
+			]);
+
+		$this->type('FoodTruck')
+			->label('Food Truck')
+			->group('business')
+			->extends('FoodEstablishment')
+			->addField('serviceArea');
+
+		$this->type('Store')
+			->label('Store / Shop')
+			->group('business')
+			->extends('LocalBusiness')
+			->addFields([
+				'hasOfferCatalog',
+				'makesOffer',
+			]);
+
+		$this->type('ProfessionalService')
+			->label('Professional Service')
+			->group('business')
+			->extends('LocalBusiness')
+			->addFields([
+				'serviceArea',
+				'makesOffer',
+				'award',
+			]);
+
+		/**************************************************************
+		 * PERSON
+		 **************************************************************/
+		$this->type('Person')
+			->label('Person')
+			->group('person')
+			->fields([
+				'type',
+				'name',
+				'givenName',
+				'familyName',
+				'honorificPrefix',
+				'honorificSuffix',
+				'alternateName',
+				'description',
+				'image',
+				'url',
+				'email',
+				'telephone',
+				'sameAs',
+				'jobTitle',
+				'knowsLanguage',
+				'knowsAbout',
+				'award',
+				'hasCredential',
+				'birthDate',
+				'gender',
+			]);
+
+		/**************************************************************
+		 * CREATIVE WORKS
+		 **************************************************************/
+		$this->type('CreativeWork')
+			->label('Creative Work')
+			->group('creative')
+			->fields([
+				'type',
+				'name',
+				'description',
+				'image',
+				'author',
+				'creator',
+				'dateCreated',
+				'datePublished',
+				'dateModified',
+				'keywords',
+				'aggregateRating'
+			]);
+
+		$this->type('DefinedTerm')
+			->label('Defined Term')
+			->group('creative')
+			->extends('CreativeWork')
+			->addFields([
+				'termCode',
+//				'inDefinedTermSet',
+			]);
+
+		$this->type('BeforeAfter')
+			->label('Before & After Case')
+			->group('creative')
+			->extends('CreativeWork')
+			->addFields([
+				'about',
+				'temporalCoverage',
+				'hasPart',
+				'associatedMedia',
+				'additionalProperty',
+			]);
+
+		$this->type('VisualArtwork')
+			->label('Visual Artwork')
+			->group('creative')
+			->extends('CreativeWork')
+			->addFields([
+				'artform',
+				'artMedium',
+				'artworkSurface',
+				'width',
+				'height',
+			]);
+
+		$this->type('Tattoo')
+			->label('Tattoo')
+			->group('creative')
+			->extends('VisualArtwork');
+
+		$this->type('Product')
+			->label('Product')
+			->group('creative')
+			->fields([
+				'name',
+				'description',
+				'image',
+				'brand',
+				'sku',
+				'gtin',
+				'offers',
+				'aggregateRating',
+				'review',
+				'award',
+			]);
+
+		/**************************************************************
+		 * EVENTS
+		 **************************************************************/
+		$this->type('Event')
+			->label('Event')
+			->group('event')
+			->fields([
+				'type',
+				'name',
+				'description',
+				'image',
+				'startDate',
+				'endDate',
+				'location',
+				'eventStatus',
+				'eventAttendanceMode',
+			]);
+	}
+
+	/**
+	 * Define type groups for organization
+	 */
+	private function registerTypeGroups(): void
+	{
+		$this->typeGroups = [
+			'general' => 'General',
+			'page' => 'Page Types',
+			'business' => 'Business & Organization',
+			'person' => 'People',
+			'creative' => 'Creative Works',
+			'event' => 'Events',
+		];
+	}
+}
diff --git a/inc/managers/SEO/SchemaFieldHelpers.php b/inc/managers/SEO/SchemaFieldHelpers.php
new file mode 100644
index 0000000..6b9ba0d
--- /dev/null
+++ b/inc/managers/SEO/SchemaFieldHelpers.php
@@ -0,0 +1,1199 @@
+<?php
+namespace JVBase\managers\SEO;
+
+use JVBase\meta\MetaManager;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Helper methods for auto-building complex schema fields
+ *
+ * SINGLE SOURCE OF TRUTH for field enhancement.
+ * All pattern resolution and value enhancement flows through here.
+ */
+class SchemaFieldHelpers
+{
+	/**
+	 * Auto-resolve and enhance field values
+	 * Main entry point for all field enhancement logic
+	 *
+	 * @param string $fieldName Field name
+	 * @param mixed $value Raw value
+	 * @param MetaManager|null $meta Optional MetaManager for accessing related fields
+	 * @return mixed Enhanced value
+	 */
+	public static function autoResolve(string $fieldName, mixed $value, ?MetaManager $meta = null): mixed
+	{
+		// Skip empty values
+		if ($value === null || $value === '') {
+			return $value;
+		}
+
+		// Skip if already enhanced (has @type)
+		if (is_array($value) && isset($value['@type'])) {
+			return $value;
+		}
+
+		// Auto-enhance based on field name
+		return match($fieldName) {
+			// Location data -> PostalAddress + GeoCoordinates
+			'location', 'address' => is_array($value) ? self::buildLocation($value) : $value,
+
+			// Image fields -> ImageObject
+			'image', 'logo', 'photo','image_portrait', 'image_landscape', 'featured_image'
+			=> is_numeric($value) ? self::buildImage($value) : self::wrapImageUrl($value),
+
+			// Hours -> openingHours array
+			'hours', 'opening_hours', 'openingHoursSpecification'
+			=> is_array($value) ? self::buildOpeningHours($value)['openingHours'] ?? $value : $value,
+
+			// Links -> sameAs array
+			'links', 'sameAs'
+			=> is_array($value) ? self::buildSameAs($value)['sameAs'] ?? $value : [$value],
+// Navigation -> SiteNavigationElement array
+
+			'hasPart'
+			=> is_array($value) ? self::buildSiteNavigation($value)['hasPart'] ?? $value : $value,
+			'hasOfferCatalog'
+			=> is_array($value) ? self::offer_catalog_array($value) : $value,
+			// Services -> OfferCatalog
+			'services'
+			=> is_array($value) ? self::buildServiceCatalog($value) : $value,
+
+			// Amenities -> amenityFeature
+			'amenities'
+			=> self::buildAmenityFeatures($value)['amenityFeature'] ?? $value,
+
+			// Languages -> availableLanguage
+			'languages'
+			=> is_array($value) ? self::buildAvailableLanguages($value)['availableLanguage'] ?? $value : $value,
+
+			// Rating -> AggregateRating (needs rating_count from meta)
+			'rating'
+			=> $meta ? self::buildAggregateRating($value, $meta->getValue('rating_count')) : $value,
+
+			// Geo coordinates
+			'geo'
+			=> is_array($value) ? self::buildGeoCoordinates($value) : $value,
+			'image_object' => self::image_object($value),
+			'image_url' => self::image_url($value),
+			'associatedMedia', 'image_object_array' => self::image_object_array($value),
+			// Add to the match statement:
+			'brand' => is_array($value) ? self::buildBrandObject($value) : $value,
+			'offers' => is_array($value) ? self::buildOfferObject($value) : $value,
+			'review' => is_array($value) ? self::buildReviewArray($value) : $value,
+			'parentOrganization', 'subOrganization'
+			=> is_array($value) ? self::buildOrganizationReference($value) : $value,
+			'employee' => is_array($value) ? self::buildPersonReferenceArray($value) : $value,
+			'starRating' => is_array($value) ? self::buildRatingObject($value) : $value,
+			// Default: return as-is
+			default => $value
+		};
+	}
+
+	/**
+	 * Check if a value is a pattern (contains {{...}})
+	 */
+	public static function isPattern(mixed $value): bool
+	{
+		return is_string($value) && str_contains($value, '{{') && str_contains($value, '}}');
+	}
+
+	/**
+	 * Get Jake Van creator attribution (ONLY for Website schema)
+	 */
+	public static function getCreator(): array
+	{
+		return [
+			'@type'         => 'Person',
+			'@id'           => 'https://jakevan.ca/#person',
+			'name'          => 'Jake Vanderwerf',
+			'alternateName' => 'JakeVan',
+			'url'           => 'https://jakevan.ca',
+			'jobTitle'      => ['Graphic Designer', 'Website Designer', 'Website Developer'],
+			'sameAs'        => [
+				'https://github.com/jakevanderwerf',
+				'https://www.linkedin.com/in/jakevanderwerf'
+			]
+		];
+	}
+
+	/**
+	 * Create proper ImageObject from WordPress attachment ID or URL
+	 *
+	 * @param int|string $image Image ID or URL
+	 * @param string $size Image size (default: 'full')
+	 * @return array|string ImageObject schema or URL
+	 */
+	public static function buildImage(int|string $image, string $size = 'full'): array|string
+	{
+		// If it's empty, return empty string
+		if (empty($image)) {
+			return '';
+		}
+
+		// If it's already a URL, wrap it
+		if (is_string($image) && (str_starts_with($image, 'http://') || str_starts_with($image, 'https://'))) {
+			return self::wrapImageUrl($image);
+		}
+
+		// Treat as attachment ID
+		$image_id = (int)$image;
+		$image_url = wp_get_attachment_image_url($image_id, $size);
+
+		if (!$image_url) {
+			return '';
+		}
+
+		$image_meta = wp_get_attachment_metadata($image_id);
+		$image_post = get_post($image_id);
+
+		$imageObject = [
+			'@type'      => 'ImageObject',
+			'url'        => $image_url,
+			'contentUrl' => $image_url,
+		];
+
+		// Add dimensions if available
+		if (!empty($image_meta['width']) && !empty($image_meta['height'])) {
+			$imageObject['width'] = $image_meta['width'];
+			$imageObject['height'] = $image_meta['height'];
+		}
+
+		// Add caption if available
+		if ($image_post && !empty($image_post->post_excerpt)) {
+			$imageObject['caption'] = $image_post->post_excerpt;
+		}
+
+		// Add alt text
+		$alt = get_post_meta($image_id, '_wp_attachment_image_alt', true);
+		if ($alt) {
+			$imageObject['description'] = $alt;
+		}
+
+		return $imageObject;
+	}
+
+	/**
+	 * Wrap a URL string in minimal ImageObject
+	 */
+	private static function wrapImageUrl(mixed $value): array|string
+	{
+		if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) {
+			return $value;
+		}
+
+		return [
+			'@type'      => 'ImageObject',
+			'url'        => $value,
+			'contentUrl' => $value,
+		];
+	}
+
+	/**
+	 * Build PostalAddress and GeoCoordinates from location data
+	 *
+	 * Returns array with 'address' and 'geo' keys
+	 *
+	 * @param array $location Location data from MetaManager
+	 * @return array Schema with address and geo fields
+	 */
+	public static function buildLocation(array $location): array
+	{
+		$schema = [];
+
+		// Build PostalAddress
+		if (!empty($location['address'])) {
+			$address = [
+				'@type'         => 'PostalAddress',
+				'streetAddress' => $location['address']
+			];
+
+			if (!empty($location['city'])) {
+				$address['addressLocality'] = $location['city'];
+			}
+
+			if (!empty($location['province'])) {
+				$address['addressRegion'] = $location['province'];
+			}
+
+			if (!empty($location['postal_code'])) {
+				$address['postalCode'] = $location['postal_code'];
+			}
+
+			if (!empty($location['country'])) {
+				$address['addressCountry'] = $location['country'];
+			}
+
+			$schema['address'] = $address;
+		}
+
+		// Build GeoCoordinates
+		if (!empty($location['lat']) && !empty($location['lng'])) {
+			$schema['geo'] = self::buildGeoCoordinates([
+				'latitude'  => $location['lat'],
+				'longitude' => $location['lng']
+			]);
+		}
+
+		return $schema;
+	}
+
+	/**
+	 * Build GeoCoordinates from lat/lng data
+	 */
+	public static function buildGeoCoordinates(array $coords): array
+	{
+		$lat = $coords['latitude'] ?? $coords['lat'] ?? null;
+		$lng = $coords['longitude'] ?? $coords['lng'] ?? null;
+
+		if (!$lat || !$lng) {
+			return [];
+		}
+
+		return [
+			'@type'     => 'GeoCoordinates',
+			'latitude'  => (float)$lat,
+			'longitude' => (float)$lng
+		];
+	}
+
+	/**
+	 * Build opening hours from repeater field
+	 *
+	 * @param array $hours Hours data from MetaManager
+	 * @return array Schema with openingHours field
+	 */
+	public static function buildOpeningHours(array $hours): array
+	{
+		if (empty($hours)) {
+			return [];
+		}
+
+		$formatted = [];
+
+		foreach ($hours as $entry) {
+			if (empty($entry['day'])) {
+				continue;
+			}
+
+			$day = ucfirst($entry['day']);
+			$opens = $entry['time_opens'] ?? '09:00';
+			$closes = $entry['time_closes'] ?? '17:00';
+
+			// Format: "Mo-Fr 09:00-17:00" or "Mo 09:00-17:00"
+			$formatted[] = "{$day} {$opens}-{$closes}";
+		}
+
+		return !empty($formatted) ? ['openingHours' => $formatted] : [];
+	}
+
+	/**
+	 * Build sameAs array from links repeater
+	 *
+	 * @param array $links Links data from MetaManager
+	 * @return array Schema with sameAs field
+	 */
+	public static function buildSameAs(array $links): array
+	{
+		if (empty($links)) {
+			return [];
+		}
+
+		$urls = [];
+
+		foreach ($links as $link) {
+			if (is_array($link) && !empty($link['url'])) {
+				$urls[] = $link['url'];
+			} elseif (is_string($link)) {
+				$urls[] = $link;
+			}
+		}
+
+		return !empty($urls) ? ['sameAs' => $urls] : [];
+	}
+
+	/**
+	 * Build service catalog from services array
+	 * Returns properly formatted OfferCatalog with itemListElement
+	 *
+	 * @param array $services Services data
+	 * @return array OfferCatalog schema
+	 */
+	public static function buildServiceCatalog(array $services): array
+	{
+		if (empty($services)) {
+			return [];
+		}
+
+		$items = [];
+
+		foreach ($services as $service) {
+			// Support both 'type' and '@type' in service data
+			$serviceType = $service['type'] ?? $service['@type'] ?? 'Service';
+
+			$item = [
+				'@type' => $serviceType,
+				'name'  => $service['name'] ?? $service['title'] ?? ''
+			];
+
+			if (!empty($service['description'])) {
+				$item['description'] = $service['description'];
+			}
+
+			// Handle pricing - can be simple text or structured
+			if (!empty($service['price'])) {
+				// Check if price is already an Offer object
+				if (is_array($service['price']) && isset($service['price']['@type'])) {
+					$item['offers'] = $service['price'];
+				} else {
+					// Create simple offer with price text
+					$item['offers'] = [
+						'@type'         => 'Offer',
+						'price'         => (string)$service['price'],
+						'priceCurrency' => $service['currency'] ?? $service['priceCurrency'] ?? 'CAD'
+					];
+				}
+			}
+
+			// Handle priceRange if provided instead of price
+			if (!empty($service['priceRange'])) {
+				$item['offers'] = [
+					'@type'      => 'Offer',
+					'price'      => $service['priceRange'],
+					'priceCurrency' => $service['currency'] ?? $service['priceCurrency'] ?? 'CAD'
+				];
+			}
+
+			if (!empty($item['name'])) {
+				$items[] = $item;
+			}
+		}
+
+		if (empty($items)) {
+			return [];
+		}
+
+		return [
+			'@type'           => 'OfferCatalog',
+			'name'            => 'Services',
+			'itemListElement' => $items
+		];
+	}
+
+	/**
+	 * Build amenity features from amenities array or string
+	 *
+	 * @param array|string $amenities Amenities data
+	 * @return array Schema with amenityFeature field
+	 */
+	public static function buildAmenityFeatures(array|string $amenities): array
+	{
+		if (empty($amenities)) {
+			return [];
+		}
+
+		// Convert string to array
+		if (is_string($amenities)) {
+			$amenities = array_map('trim', explode(',', $amenities));
+		}
+
+		$features = [];
+
+		foreach ($amenities as $amenity) {
+			if (is_array($amenity) && isset($amenity['name'])) {
+				$features[] = [
+					'@type' => 'LocationFeatureSpecification',
+					'name'  => $amenity['name'],
+					'value' => true
+				];
+			} elseif (is_string($amenity) && $amenity !== '') {
+				$features[] = [
+					'@type' => 'LocationFeatureSpecification',
+					'name'  => $amenity,
+					'value' => true
+				];
+			}
+		}
+
+		return !empty($features) ? ['amenityFeature' => $features] : [];
+	}
+
+	/**
+	 * Build available languages from languages array
+	 *
+	 * @param array $languages Languages data
+	 * @return array Schema with availableLanguage field
+	 */
+	public static function buildAvailableLanguages(array $languages): array
+	{
+		if (empty($languages)) {
+			return [];
+		}
+
+		$items = [];
+
+		foreach ($languages as $lang) {
+			if (is_array($lang) && isset($lang['language'])) {
+				$items[] = [
+					'@type' => 'Language',
+					'name'  => $lang['language']
+				];
+			} elseif (is_string($lang) && $lang !== '') {
+				$items[] = [
+					'@type' => 'Language',
+					'name'  => $lang
+				];
+			}
+		}
+
+		return !empty($items) ? ['availableLanguage' => $items] : [];
+	}
+
+	/**
+	 * Build aggregate rating from rating value and count
+	 *
+	 * @param float|string $rating Rating value
+	 * @param int|string|null $count Number of ratings
+	 * @return array|null Schema with aggregateRating or null
+	 */
+	public static function buildAggregateRating(float|string $rating, int|string|null $count): ?array
+	{
+		if (empty($rating)) {
+			return null;
+		}
+
+		$ratingValue = (float)$rating;
+		$ratingCount = (int)($count ?? 0);
+
+		if ($ratingCount === 0) {
+			// Can't have aggregate rating without count
+			return null;
+		}
+
+		return [
+			'@type'       => 'AggregateRating',
+			'ratingValue' => $ratingValue,
+			'ratingCount' => $ratingCount,
+			'bestRating'  => 5.0,
+			'worstRating' => 1.0
+		];
+	}
+
+	/**
+	 * Transform text value
+	 */
+	public static function text($value): string
+	{
+		return (string)$value;
+	}
+
+	/**
+	 * Transform URL value
+	 */
+	public static function url($value): string
+	{
+		return esc_url_raw($value);
+	}
+
+	/**
+	 * Transform email value
+	 */
+	public static function email($value): string
+	{
+		return sanitize_email($value);
+	}
+
+	/**
+	 * Transform number value
+	 */
+	public static function number($value): float|int
+	{
+		return is_numeric($value) ? (float)$value : 0;
+	}
+
+	/**
+	 * Transform date value to ISO format (YYYY-MM-DD)
+	 */
+	public static function date($value): string
+	{
+		if (empty($value)) return '';
+
+		// If already in ISO format, return as-is
+		if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
+			return $value;
+		}
+
+		// Otherwise convert to ISO format
+		$timestamp = is_numeric($value) ? $value : strtotime($value);
+		return $timestamp ? date('Y-m-d', $timestamp) : '';
+	}
+
+	/**
+	 * Transform datetime value to ISO 8601 format
+	 */
+	public static function datetime($value): string
+	{
+		if (empty($value)) return '';
+
+		// If already in ISO format, return as-is
+		if (preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/', $value)) {
+			return $value;
+		}
+
+		// Otherwise convert to ISO format
+		$timestamp = is_numeric($value) ? $value : strtotime($value);
+		return $timestamp ? date('c', $timestamp) : '';
+	}
+
+	/**
+	 * Transform dimension value to QuantitativeValue schema
+	 * Examples: "10cm" -> {value: 10, unitCode: "CM"}
+	 */
+	public static function dimension($value): array|string
+	{
+		if (empty($value)) return '';
+
+		// If already an object, return as-is
+		if (is_array($value) && isset($value['@type'])) {
+			return $value;
+		}
+
+		// Extract number and unit (e.g., "10cm" -> ["10", "cm"])
+		if (preg_match('/^([\d.]+)\s*([a-z]+)$/i', $value, $matches)) {
+			return [
+				'@type' => 'QuantitativeValue',
+				'value' => (float)$matches[1],
+				'unitCode' => strtoupper($matches[2])
+			];
+		}
+
+		return $value;
+	}
+
+	/**
+	 * Transform array of text values from repeater
+	 * Handles various repeater field formats
+	 */
+	public static function text_array($value): array
+	{
+		if (!is_array($value)) {
+			return [$value];
+		}
+
+		return array_map(function($item) {
+			if (is_array($item)) {
+				// Handle repeater format with common field names
+				return $item['name'] ?? $item['keyword'] ?? $item['topic'] ?? $item['value'] ?? '';
+			}
+			return (string)$item;
+		}, array_filter($value));
+	}
+
+	/**
+	 * Transform array of URLs from repeater
+	 */
+	public static function url_array($value): array
+	{
+		if (!is_array($value)) {
+			return [$value];
+		}
+
+		return array_map(function($item) {
+			if (is_array($item)) {
+				return esc_url_raw($item['url'] ?? '');
+			}
+			return esc_url_raw($item);
+		}, array_filter($value));
+	}
+
+	/**
+	 * Transform image ID to ImageObject
+	 * Reuses existing buildImage method
+	 */
+	public static function image_object($imageId): array|string
+	{
+		if (!$imageId) return '';
+		return self::buildImage($imageId);
+	}
+
+	/**
+	 * Transform array of image IDs to ImageObject array
+	 * Handles two formats:
+	 * 1. Simple array: [123, 456, 789]
+	 * 2. Repeater format: [['image' => 123, 'caption' => 'Before'], ...]
+	 */
+	public static function image_object_array($value): array
+	{
+		if (!is_array($value)) {
+			return [];
+		}
+
+		return array_values(array_filter(array_map(function($item, $index) {
+			// Handle repeater format with sub-fields
+			if (is_array($item) && isset($item['image'])) {
+				$imageObject = self::buildImage($item['image']);
+
+				if (empty($imageObject)) {
+					return null;
+				}
+
+				if (!empty($item['caption'])) {
+					$imageObject['caption'] = $item['caption'];
+				}
+
+				if (isset($item['position'])) {
+					$imageObject['position'] = (int)$item['position'];
+				} else {
+					$imageObject['position'] = $index;
+				}
+
+				return $imageObject;
+			}
+
+			// Handle simple array of IDs
+			if (is_numeric($item)) {
+				$imageObject = self::buildImage($item);
+
+				if (empty($imageObject)) {
+					return null;
+				}
+
+				$imageObject['position'] = $index;
+
+				// Try to get caption from image post
+				$post = get_post($item);
+				if ($post && !empty($post->post_excerpt)) {
+					$imageObject['caption'] = $post->post_excerpt;
+				}
+
+				return $imageObject;
+			}
+
+			return null;
+		}, $value, array_keys($value))));
+	}
+
+	public static function image_url($imageId): string
+	{
+		if (!$imageId) {
+			return '';
+		}
+
+		// If already a URL string, return as-is
+		if (is_string($imageId) && (str_starts_with($imageId, 'http://') || str_starts_with($imageId, 'https://'))) {
+			return $imageId;
+		}
+
+		// Get URL from attachment ID
+		$image_url = wp_get_attachment_image_url((int)$imageId, 'full');
+
+		return $image_url ?: '';
+	}
+	/**
+	 * Transform location to PostalAddress + GeoCoordinates
+	 * Returns array with 'address' and 'geo' keys
+	 *
+	 * Special case: returns multiple schema properties
+	 */
+	public static function location_complex($location): array
+	{
+		if (!$location) return [];
+		return self::buildLocation($location);
+	}
+
+	/**
+	 * Transform location to just PostalAddress
+	 */
+	public static function postal_address($location): array
+	{
+		if (!is_array($location) || empty($location['address'])) {
+			return [];
+		}
+
+		$address = [
+			'@type'         => 'PostalAddress',
+			'streetAddress' => $location['address']
+		];
+
+		if (!empty($location['city'])) {
+			$address['addressLocality'] = $location['city'];
+		}
+
+		if (!empty($location['province'])) {
+			$address['addressRegion'] = $location['province'];
+		}
+
+		if (!empty($location['postal_code'])) {
+			$address['postalCode'] = $location['postal_code'];
+		}
+
+		if (!empty($location['country'])) {
+			$address['addressCountry'] = $location['country'];
+		}
+
+		return $address;
+	}
+
+	/**
+	 * Transform coordinates to GeoCoordinates
+	 * Reuses existing buildGeoCoordinates method
+	 */
+	public static function geo_coordinates($coords): array
+	{
+		if (!is_array($coords)) return [];
+		return self::buildGeoCoordinates($coords);
+	}
+
+	/**
+	 * Transform opening hours group to OpeningHoursSpecification
+	 * Reuses existing buildOpeningHours method
+	 */
+	public static function opening_hours_specification($hours): array
+	{
+		if (!is_array($hours)) return [];
+		$result = self::buildOpeningHours($hours);
+		return $result['openingHours'] ?? [];
+	}
+
+	/**
+	 * Transform contact points repeater to ContactPoint array
+	 */
+	public static function contact_point_array($contacts): array
+	{
+		if (!is_array($contacts)) return [];
+
+		$contactPoints = [];
+		foreach ($contacts as $contact) {
+			if (empty($contact['contactType'])) continue;
+
+			$point = [
+				'@type' => 'ContactPoint',
+				'contactType' => $contact['contactType']
+			];
+
+			if (!empty($contact['telephone'])) {
+				$point['telephone'] = $contact['telephone'];
+			}
+
+			if (!empty($contact['email'])) {
+				$point['email'] = $contact['email'];
+			}
+
+			$contactPoints[] = $point;
+		}
+
+		return $contactPoints;
+	}
+
+	/**
+	 * Transform amenity features repeater
+	 * Reuses existing buildAmenityFeatures method
+	 */
+	public static function amenity_feature_array($amenities): array
+	{
+		if (!is_array($amenities)) return [];
+		$result = self::buildAmenityFeatures($amenities);
+		return $result['amenityFeature'] ?? [];
+	}
+
+	/**
+	 * Transform languages repeater
+	 * Reuses existing buildAvailableLanguages method
+	 */
+	public static function language_array($languages): array
+	{
+		if (!is_array($languages)) return [];
+		$result = self::buildAvailableLanguages($languages);
+		return $result['availableLanguage'] ?? [];
+	}
+
+	/**
+	 * Transform aggregate rating group to AggregateRating schema
+	 */
+	public static function aggregate_rating($rating): ?array
+	{
+		if (!is_array($rating) || empty($rating['ratingValue'])) {
+			return null;
+		}
+
+		$aggregateRating = [
+			'@type' => 'AggregateRating',
+			'ratingValue' => (float)$rating['ratingValue']
+		];
+
+		if (!empty($rating['bestRating'])) {
+			$aggregateRating['bestRating'] = (float)$rating['bestRating'];
+		}
+
+		if (!empty($rating['worstRating'])) {
+			$aggregateRating['worstRating'] = (float)$rating['worstRating'];
+		}
+
+		if (!empty($rating['ratingCount'])) {
+			$aggregateRating['ratingCount'] = (int)$rating['ratingCount'];
+		}
+
+		if (!empty($rating['reviewCount'])) {
+			$aggregateRating['reviewCount'] = (int)$rating['reviewCount'];
+		}
+
+		return $aggregateRating;
+	}
+
+	/**
+	 * Transform hasOfferCatalog field data to OfferCatalog schema
+	 * Handles both manual items and auto-generated from post type
+	 * Reuses existing buildServiceCatalog method
+	 */
+	public static function offer_catalog_array($data): array
+	{
+		if (!is_array($data)) return [];
+
+		// Extract manual items if present
+		if (array_key_exists('manual_items', $data) && !empty($data['manual_items'])) {
+			$services = $data['manual_items'];
+		}
+		// Otherwise expect array of items directly
+		else if (isset($data[0])) {
+			$services = $data;
+		}
+		else {
+			return [];
+		}
+
+		// Build the catalog using existing method
+		return self::buildServiceCatalog($services);
+	}
+
+	/**
+	 * Transform credentials repeater to EducationalOccupationalCredential array
+	 */
+	public static function credential_array($credentials): array
+	{
+		if (!is_array($credentials)) return [];
+
+		$items = [];
+		foreach ($credentials as $cred) {
+			if (empty($cred['name'])) continue;
+
+			$item = [
+				'@type' => 'EducationalOccupationalCredential',
+				'name' => $cred['name']
+			];
+
+			if (!empty($cred['credentialCategory'])) {
+				$item['credentialCategory'] = $cred['credentialCategory'];
+			}
+
+			if (!empty($cred['issuedBy'])) {
+				$item['recognizedBy'] = [
+					'@type' => 'Organization',
+					'name' => $cred['issuedBy']
+				];
+			}
+
+			$items[] = $item;
+		}
+
+		return $items;
+	}
+
+	/**
+	 * Transform FAQ repeater to Question schema array
+	 */
+	public static function faq_array($faqs): array
+	{
+		if (!is_array($faqs)) return [];
+
+		$questions = [];
+		foreach ($faqs as $faq) {
+			if (empty($faq['question']) || empty($faq['answer'])) {
+				continue;
+			}
+
+			$questions[] = [
+				'@type' => 'Question',
+				'name' => $faq['question'],
+				'acceptedAnswer' => [
+					'@type' => 'Answer',
+					'text' => $faq['answer']
+				]
+			];
+		}
+
+		return $questions;
+	}
+
+	/**
+	 * Transform PotentialAction configurations to schema.org format
+	 *
+	 * @param array $actions Array of action configurations
+	 * @return array Formatted PotentialAction array
+	 */
+	public static function potential_action_array($actions): array
+	{
+		if (empty($actions) || !is_array($actions)) {
+			return [];
+		}
+
+		$formatted = [];
+
+		foreach ($actions as $action) {
+			if (empty($action['type']) || empty($action['name'])) {
+				continue; // Skip invalid actions
+			}
+
+			$formattedAction = [
+				'@type' => $action['type'],
+				'name'  => $action['name'],
+			];
+
+			// Add target (required for most actions)
+			if (!empty($action['target'])) {
+				$target = $action['target'];
+
+				// If target contains a query placeholder, format as EntryPoint
+				if (str_contains($target, '{')) {
+					$formattedAction['target'] = [
+						'@type'       => 'EntryPoint',
+						'urlTemplate' => $target,
+					];
+				} else {
+					$formattedAction['target'] = $target;
+				}
+			}
+
+			// Add optional fields
+			if (!empty($action['description'])) {
+				$formattedAction['description'] = $action['description'];
+			}
+
+			if (!empty($action['url'])) {
+				$formattedAction['url'] = $action['url'];
+			}
+
+			$formatted[] = $formattedAction;
+		}
+
+		return $formatted;
+	}
+
+	/**
+	 * Build SiteNavigationElement array from navigation items
+	 */
+	public static function buildSiteNavigation(array $items): array
+	{
+		$elements = [];
+		$position = 1;
+
+		foreach ($items as $item) {
+			if (empty($item['name']) || empty($item['url'])) continue;
+
+			$nav = [
+				'@type' => 'SiteNavigationElement',
+				'@id' => $item['url'] . '#navigation',
+				'position' => $position++,
+				'name' => $item['name'],
+				'url' => $item['url'],
+			];
+
+			if (!empty($item['description'])) {
+				$nav['description'] = $item['description'];
+			}
+
+			$elements[] = $nav;
+		}
+
+		return ['hasPart' => $elements];
+	}
+
+	/**
+	 * Build Offer object
+	 */
+	public static function buildOfferObject(array $data): array
+	{
+		$offer = ['@type' => 'Offer'];
+
+		if (!empty($data['price'])) {
+			$offer['price'] = (string)$data['price'];
+			$offer['priceCurrency'] = $data['priceCurrency'] ?? 'USD';
+		}
+
+		if (!empty($data['availability'])) {
+			$offer['availability'] = 'https://schema.org/' . $data['availability'];
+		}
+
+		if (!empty($data['validFrom'])) {
+			$offer['validFrom'] = $data['validFrom'];
+		}
+
+		if (!empty($data['validThrough'])) {
+			$offer['validThrough'] = $data['validThrough'];
+		}
+
+		return $offer;
+	}
+
+	/**
+	 * Build Brand object or simple text
+	 */
+	public static function buildBrandObject(array $data): array|string
+	{
+		if (empty($data['name'])) {
+			return '';
+		}
+
+		// Simple text brand
+		if (empty($data['type']) || $data['type'] === 'text') {
+			return $data['name'];
+		}
+
+		// Organization/Brand object
+		$brand = [
+			'@type' => 'Brand',
+			'name' => $data['name'],
+		];
+
+		if (!empty($data['url'])) {
+			$brand['url'] = $data['url'];
+		}
+
+		if (!empty($data['logo'])) {
+			$brand['logo'] = self::buildImage($data['logo']);
+		}
+
+		return $brand;
+	}
+
+	/**
+	 * Build Review array
+	 */
+	public static function buildReviewArray(array $reviews): array
+	{
+		$output = [];
+
+		foreach ($reviews as $review) {
+			if (empty($review['author']) && empty($review['reviewBody'])) {
+				continue;
+			}
+
+			$item = ['@type' => 'Review'];
+
+			if (!empty($review['author'])) {
+				$item['author'] = [
+					'@type' => 'Person',
+					'name' => $review['author']
+				];
+			}
+
+			if (!empty($review['reviewRating'])) {
+				$item['reviewRating'] = [
+					'@type' => 'Rating',
+					'ratingValue' => $review['reviewRating'],
+				];
+			}
+
+			if (!empty($review['reviewBody'])) {
+				$item['reviewBody'] = $review['reviewBody'];
+			}
+
+			if (!empty($review['datePublished'])) {
+				$item['datePublished'] = $review['datePublished'];
+			}
+
+			$output[] = $item;
+		}
+
+		return $output;
+	}
+
+	/**
+	 * Build organization reference
+	 */
+	public static function buildOrganizationReference(array $data): array
+	{
+		if (empty($data['name'])) {
+			return [];
+		}
+
+		$org = [
+			'@type' => 'Organization',
+			'name' => $data['name'],
+		];
+
+		if (!empty($data['url'])) {
+			$org['url'] = $data['url'];
+		}
+
+		return $org;
+	}
+
+	/**
+	 * Build organization reference array
+	 */
+	public static function buildOrganizationReferenceArray(array $items): array
+	{
+		return array_map([self::class, 'buildOrganizationReference'], $items);
+	}
+
+	/**
+	 * Build person reference array
+	 */
+	public static function buildPersonReferenceArray(array $items): array
+	{
+		$output = [];
+
+		foreach ($items as $item) {
+			if (empty($item['name'])) continue;
+
+			$person = [
+				'@type' => 'Person',
+				'name' => $item['name'],
+			];
+
+			if (!empty($item['jobTitle'])) {
+				$person['jobTitle'] = $item['jobTitle'];
+			}
+
+			$output[] = $person;
+		}
+
+		return $output;
+	}
+
+	/**
+	 * Boolean transformer
+	 */
+	public static function buildBoolean(mixed $value): bool
+	{
+		return (bool)$value;
+	}
+
+	/**
+	 * Time transformer
+	 */
+	public static function buildTime(string $value): string
+	{
+		// Ensure format is HH:MM
+		return date('H:i', strtotime($value));
+	}
+
+	/**
+	 * Rating object
+	 */
+	public static function buildRatingObject(array $data): array
+	{
+		if (empty($data['ratingValue'])) {
+			return [];
+		}
+
+		return [
+			'@type' => 'Rating',
+			'ratingValue' => (float)$data['ratingValue'],
+			'bestRating' => 5,
+		];
+	}
+}
diff --git a/inc/managers/SEO/SchemaOutputManager.php b/inc/managers/SEO/SchemaOutputManager.php
new file mode 100644
index 0000000..051eeb7
--- /dev/null
+++ b/inc/managers/SEO/SchemaOutputManager.php
@@ -0,0 +1,710 @@
+<?php
+namespace JVBase\managers\SEO;
+
+use JVBase\managers\CacheManager;
+use JVBase\meta\MetaManager;
+use WP_Term;
+use WP_User;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Handles SEO output: Schema.org JSON and TSF meta filtering
+ *
+ * Integrates with The SEO Framework, letting it handle defaults
+ * while we override with our configured templates.
+ *
+ * Now with integrated caching via CacheManager for performance.
+ */
+class SchemaOutputManager
+{
+	private ConfigManager $config;
+	private SchemaBuilder $registry;
+	private ?TemplateResolver $resolver = null;
+	private CacheManager $cache;
+	private array $pseudoTypes = [
+		'BeforeAfter',
+	];
+
+	public function __construct()
+	{
+		$this->registry = SchemaBuilder::getInstance();
+		$this->cache = CacheManager::for('schema');
+
+		// Register cache connections
+		$this->cache->connectTo('post', 'id');
+		$this->cache->connectTo('taxonomy', 'id');
+		$this->cache->connectTo('user', 'id');
+
+		// Hook into TSF for meta
+		add_filter('the_seo_framework_title_from_generation', [$this, 'filterTitle'], 10, 2);
+		add_filter('the_seo_framework_generated_description', [$this, 'filterDescription'], 10, 3);
+
+		// Add image filters
+		add_filter('the_seo_framework_image_generation_params', [$this, 'filterImage'], 10, 3);
+
+		// Disable TSF schema on our content (we'll output our own)
+		add_filter('the_seo_framework_schema_graph_data', [$this, 'filterTSFSchema'], 10, 2);
+
+		// Output our schema
+		add_action('wp_head', [$this, 'outputSchema'], 1);
+	}
+
+	/**
+	 * Filter the SEO title
+	 */
+	public function filterTitle(string $title, ?array $args): string
+	{
+		if ($args !== null) {
+			// Not in the loop (admin, etc.)
+			return $title;
+		}
+
+		$context = $this->getCurrentContext();
+		if (!$context) {
+			return $title;
+		}
+
+		$metaConfig = $this->config->meta();
+
+		if (empty($metaConfig['title'])) {
+			return $title;
+		}
+
+		$resolver = $this->getResolver();
+		$customTitle = $resolver->resolve($metaConfig['title']);
+
+		return $customTitle ?: $title;
+	}
+
+	/**
+	 * Filter the SEO description
+	 */
+	public function filterDescription(string $description, ?array $args, string $type): string
+	{
+		if ($args !== null) {
+			return $description;
+		}
+
+		$context = $this->getCurrentContext();
+		if (!$context) {
+			return $description;
+		}
+
+		$metaConfig = $this->config->meta();
+
+		if (empty($metaConfig['description'])) {
+			return $description;
+		}
+
+		$resolver = $this->getResolver();
+		$customDescription = $resolver->resolve($metaConfig['description']);
+
+		// Truncate to reasonable length
+		if (strlen($customDescription) > 160) {
+			$customDescription = substr($customDescription, 0, 157) . '...';
+		}
+
+		return $customDescription ?: $description;
+	}
+
+	/**
+	 * Filter the SEO image for social previews
+	 */
+	public function filterImage(array $params, ?array $args, $tsf_id): array
+	{
+		if ($args !== null) {
+			return $params;
+		}
+
+		$context = $this->getCurrentContext();
+		if (!$context) {
+			return $params;
+		}
+
+		$metaConfig = $this->config->meta();
+
+		// Check for custom image
+		if (!empty($metaConfig['image'])) {
+			$resolver = $this->getResolver();
+			$imageUrl = $resolver->resolve($metaConfig['image']);
+
+			if ($imageUrl) {
+				$params['og:image'] = $imageUrl;
+
+				// Use twitter-specific image if set, otherwise use main image
+				if (!empty($metaConfig['twitter_image'])) {
+					$twitterImage = $resolver->resolve($metaConfig['twitter_image']);
+					$params['twitter:image'] = $twitterImage ?: $imageUrl;
+				} else {
+					$params['twitter:image'] = $imageUrl;
+				}
+			}
+		}
+
+		return $params;
+	}
+
+	/**
+	 * Disable TSF schema for our custom content types
+	 */
+	public function filterTSFSchema(array $graph, ?array $args): array
+	{
+		if ($args !== null) {
+			return $graph;
+		}
+
+		$context = $this->getCurrentContext();
+		if ($context) {
+			// We're handling schema for this content
+			return [];
+		}
+
+		return $graph;
+	}
+
+	/**
+	 * Output schema JSON-LD
+	 */
+	public function outputSchema(): void
+	{
+		// Build cache key
+		$context = $this->getCurrentContext();
+		$cacheKey = $this->buildCacheKey($context);
+
+		// Try to get from cache
+		$schema = $this->cache->get($cacheKey);
+
+		if ($schema === false) {
+			// Build schema
+			$schema = $this->buildSchema();
+
+			// Cache for 1 hour (will auto-invalidate on content update)
+			$this->cache->set($cacheKey, $schema, HOUR_IN_SECONDS);
+		}
+
+		if (empty($schema)) {
+			return;
+		}
+
+		echo "\n<!-- SEO Schema by Jake Van -->\n";
+		echo '<script type="application/ld+json">' . "\n";
+		echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
+		echo "\n" . '</script>' . "\n";
+	}
+
+	private function resolveSchemaType(string $configuredType): string
+	{
+		// Only resolve pseudo-types (custom types not in schema.org)
+		if (in_array($configuredType, $this->pseudoTypes)) {
+			$typeDef = $this->registry->getTypeDefinition($configuredType);
+			if ($typeDef && !empty($typeDef['extends'])) {
+				// Recursively resolve in case parent is also pseudo
+				return $this->resolveSchemaType($typeDef['extends']);
+			}
+		}
+
+		// Use configured type (it's a real schema.org type)
+		return $configuredType;
+	}
+
+	/**
+	 * Build cache key for current context
+	 */
+	private function buildCacheKey(?array $context): string
+	{
+		if (!$context) {
+			return 'home_' . get_current_blog_id();
+		}
+
+		return "{$context['objectType']}_{$context['objectId']}_{$context['type']}";
+	}
+
+	/**
+	 * Build complete schema structure
+	 */
+	private function buildSchema(): array
+	{
+		$schema = [
+			'@context' => 'https://schema.org',
+			'@graph'   => []
+		];
+
+		// Always include Website schema
+		$websiteSchema = $this->buildSchemaForType('website', 'WebSite', '/#website');
+		if ($websiteSchema) {
+			$websiteSchema['url'] = $websiteSchema['url'] ?? get_home_url();
+			$websiteSchema['name'] = $websiteSchema['name'] ?? get_bloginfo('name');
+			$websiteSchema['publisher'] = ['@id' => get_home_url() . '/#organization'];
+			$websiteSchema['creator'] = SchemaFieldHelpers::getCreator();
+			$schema['@graph'][] = $websiteSchema;
+		}
+
+		// Include Organization schema on home page
+		if (is_front_page()) {
+			$orgSchema = $this->buildSchemaForType('organization', null, '/#organization');
+			if ($orgSchema && !empty($orgSchema['name'])) {
+				$schema['@graph'][] = $orgSchema;
+			}
+		}
+
+		$webPageSchema = $this->buildWebPageSchema();
+		if ($webPageSchema) {
+			$schema['@graph'][] = $webPageSchema;
+		}
+
+		// Include context-specific schema
+		$contextSchema = $this->buildContextSchema();
+		if ($contextSchema) {
+			$schema['@graph'][] = $contextSchema;
+		}
+
+		// Include breadcrumbs
+		$breadcrumbs = $this->buildBreadcrumbSchema();
+		if ($breadcrumbs) {
+			$schema['@graph'][] = $breadcrumbs;
+		}
+
+		return $schema;
+	}
+
+	/**
+	 * Generic schema builder - replaces buildWebsiteSchema, buildOrganizationSchema, etc.
+	 *
+	 * @param string $configKey Config key (site, business, post_type, etc.)
+	 * @param string|null $forceType Force a specific schema type (optional)
+	 * @param string|null $id Schema @id suffix
+	 */
+	private function buildSchemaForType(string $configKey, ?string $forceType = null, ?string $id = null): ?array
+	{
+		$this->config = ConfigManager::for($configKey);
+		$config = $this->config->schema();
+
+		if (empty($config)) {
+			return null;
+		}
+
+		$schemaType = $forceType ?? $config['type'] ?? null;
+		if (!$schemaType) {
+			return null;
+		}
+
+		// Build full @id if suffix provided
+		$fullId = $id ? get_home_url() . $id : null;
+
+		// Use the generic builder
+		return $this->buildSchemaFromConfig($config, $schemaType, $fullId);
+	}
+
+	/**
+	 * Build schema for current context (page, post, term, etc.)
+	 */
+	private function buildContextSchema(): ?array
+	{
+		$context = $this->getCurrentContext();
+
+		if (!$context) {
+			return null;
+		}
+
+		// For archives, use archive config
+		if (in_array($context['objectType'], ['archive', 'term'])) {
+			return $this->buildArchiveSchema($context);
+		}
+
+		$schemaConfig = $this->config->schema();
+
+		if (empty($schemaConfig) || empty($schemaConfig['type'])) {
+			return null;
+		}
+
+		$resolver = $this->getResolver();
+		$schemaType = $schemaConfig['type'];
+
+		// Resolve all field values from templates
+		$resolvedConfig = $this->resolveConfigTemplates($schemaConfig, $resolver);
+
+		// Build schema with resolved values
+		$schema = $this->buildSchemaFromConfig(
+			$resolvedConfig,
+			$schemaType,
+			$resolver->resolveVariable('permalink') . '#' . strtolower($schemaType)
+		);
+
+		// Add mainEntityOfPage for content items
+		if ($schema && $schemaType !== 'FAQPage') {
+			$schema['mainEntityOfPage'] = [
+				'@type' => 'WebPage',
+				'@id'   => $resolver->resolveVariable('permalink'),
+			];
+		}
+
+		return $schema;
+	}
+	/**
+	 * Build schema for archive pages
+	 * Automatically generates mainEntity from archive posts
+	 */
+	private function buildArchiveSchema(array $context): ?array
+	{
+		// Ensure archive config is initialized
+		if (!$this->config->archive()) {
+			$this->config->setupArchive();
+		}
+
+		$archiveConfig = $this->config->archive();
+
+		// Return null if no config or no type defined
+		if (empty($archiveConfig) || empty($archiveConfig['type'])) {
+			return null;
+		}
+
+		$resolver = $this->getResolver();
+		$schemaType = $archiveConfig['type'];
+
+		// Resolve templates from archive config
+		$resolvedConfig = $this->resolveConfigTemplates($archiveConfig, $resolver);
+
+		// Build base schema
+		$schema = $this->buildSchemaFromConfig(
+			$resolvedConfig,
+			$schemaType,
+			$resolver->resolveVariable('permalink') . '#' . strtolower($schemaType)
+		);
+
+		if (!$schema) {
+			return null;
+		}
+
+		// Automatically add mainEntity for types that need it
+		$mainEntity = $this->buildMainEntity($schemaType, $context['type']);
+		if ($mainEntity) {
+			$schema['mainEntity'] = $mainEntity;
+		}
+
+		return $schema;
+	}
+
+	/**
+	 * Automatically build mainEntity for archive pages
+	 * Uses SchemaReferenceBuilder to generate entities from archive posts
+	 *
+	 * @param string $archiveSchemaType The archive's @type (FAQPage, CollectionPage, etc.)
+	 * @param string $contentType The content type being archived (faq, artwork, etc.)
+	 * @return array|null Array of entities or null if not applicable
+	 */
+	private function buildMainEntity(string $archiveSchemaType, string $contentType): ?array
+	{
+		// Only certain archive types need mainEntity
+		$typesNeedingMainEntity = ['FAQPage', 'CollectionPage', 'ItemList'];
+		if (!in_array($archiveSchemaType, $typesNeedingMainEntity)) {
+			return null;
+		}
+
+		$context = $this->getCurrentContext();
+
+		// For taxonomy term archives, get posts from the term
+		if ($context['objectType'] === 'term') {
+			// Get the post type(s) this taxonomy is for
+			$taxonomy = defined('JVB_TAXONOMY') && isset(JVB_TAXONOMY[$contentType])
+				? JVB_TAXONOMY[$contentType]
+				: null;
+
+			if (!$taxonomy || empty($taxonomy['for_content'])) {
+				return null;
+			}
+
+			// Use the first post type (most common case)
+			$postType = $taxonomy['for_content'][0];
+
+			return SchemaReferenceBuilder::buildFromTerm(
+				$context['objectId'],
+				$postType,
+				10,  // limit
+				null, // auto-infer type
+				true  // include context
+			);
+		}
+
+		// For post type archives
+		if ($context['objectType'] === 'archive') {
+			return SchemaReferenceBuilder::buildFromArchive($contentType);
+		}
+
+		return null;
+	}
+
+	/**
+	 * Resolve all template patterns in config
+	 */
+	private function resolveConfigTemplates(array $config, TemplateResolver $resolver): array
+	{
+		$resolved = ['type' => $config['type']];
+
+		foreach ($config as $fieldName => $value) {
+			if ($fieldName === 'type') {
+				continue;
+			}
+
+			$resolvedValue = $this->resolveFieldValue($fieldName, $value, $resolver);
+
+			if ($resolvedValue !== null && $resolvedValue !== '') {
+				$resolved[$fieldName] = $resolvedValue;
+			}
+		}
+
+		return $resolved;
+	}
+
+	/**
+	 * Enhanced buildSchemaFromConfig with MetaManager integration
+	 */
+	private function buildSchemaFromConfig(array $config, string $schemaType, ?string $id = null): ?array
+	{
+		// Build base schema
+		$schema = ['@type' => $this->resolveSchemaType($schemaType)];
+
+		if ($id) {
+			$schema['@id'] = $id;
+		}
+
+		// Get MetaManager if we have a context
+		$meta = null;
+		$context = $this->getCurrentContext();
+		if ($context) {
+			$meta = new MetaManager($context['objectId'], $context['objectType']);
+		}
+
+		// Process each field
+		foreach ($config as $fieldName => $value) {
+			// Skip meta fields and empty values
+			if ($fieldName === 'type' || $value === null || $value === '' || $value === []) {
+				continue;
+			}
+
+			// Auto-resolve field value (handles images, locations, etc.)
+			$value = SchemaFieldHelpers::autoResolve($fieldName, $value, $meta);
+
+			// Get field definition for transformer
+			$fieldDef = $this->registry->getFieldDefinition($fieldName);
+
+			// Apply transformer if defined
+			if ($fieldDef && !empty($fieldDef['transformer'])) {
+				$value = $this->applyTransformer($value, $fieldDef['transformer'], $fieldName);
+			}
+
+			// Skip if empty after transformation
+			if ($value === null || $value === '' || $value === []) {
+				continue;
+			}
+
+			// Handle multi-property transformers (like location_complex returns address + geo)
+			if (is_array($value) && !isset($value['@type']) && !isset($value[0])) {
+				$multiProps = ['address', 'geo', 'openingHours', 'sameAs'];
+				if (!empty(array_intersect(array_keys($value), $multiProps))) {
+					foreach ($value as $subKey => $subValue) {
+						if ($subValue !== null && $subValue !== '' && $subValue !== []) {
+							$schema[$subKey] = $subValue;
+						}
+					}
+					continue;
+				}
+			}
+
+			// Normal case: add single property
+			$schema[$fieldName] = $value;
+		}
+
+		// Return null if only @type remains
+		return (count($schema) > 1) ? $schema : null;
+	}
+
+	/**
+	 * Apply transformer to a field value
+	 */
+	private function applyTransformer(mixed $value, string $transformer, string $fieldName): mixed
+	{
+		// Check if transformer method exists in SchemaFieldHelpers
+		if (method_exists(SchemaFieldHelpers::class, $transformer)) {
+			try {
+				return SchemaFieldHelpers::$transformer($value);
+			} catch (\Throwable $e) {
+				// Log error but don't break schema output
+				error_log("Schema transformer error for {$fieldName}: {$e->getMessage()}");
+				return $value;
+			}
+		}
+
+		// No transformer found, return value as-is
+		return $value;
+	}
+
+	/**
+	 * Resolve a field value from template
+	 */
+	private function resolveFieldValue(string $key, mixed $template, TemplateResolver $resolver): mixed
+	{
+		if (is_string($template)) {
+			// Simple template pattern
+			$value = $resolver->resolve($template);
+
+			// If it's still a pattern (unresolved), skip it
+			if (SchemaFieldHelpers::isPattern($value)) {
+				return null;
+			}
+
+			return $value !== '' ? $value : null;
+		}
+
+		if (is_array($template)) {
+			// Complex nested structure - resolve recursively
+			$resolved = [];
+			foreach ($template as $subKey => $subValue) {
+				$resolvedValue = $this->resolveFieldValue($subKey, $subValue, $resolver);
+				if ($resolvedValue !== null) {
+					$resolved[$subKey] = $resolvedValue;
+				}
+			}
+			return !empty($resolved) ? $resolved : null;
+		}
+
+		// Direct value (not a template)
+		return $template;
+	}
+
+	/**
+	 * Build WebPage schema for current page (including homepage)
+	 */
+	private function buildWebPageSchema(): ?array
+	{
+		$webpage = [
+			'@type'    => 'WebPage',
+			'@id'      => get_permalink() . '/#webpage',
+			'url'      => get_permalink(),
+			'isPartOf' => ['@id' => get_home_url() . '/#website'],
+		];
+
+		// Add about relationship on homepage (pointing to organization)
+		if (is_front_page()) {
+			$webpage['about'] = ['@id' => get_home_url() . '/#organization'];
+			$webpage['name'] = get_bloginfo('name');
+			$webpage['description'] = get_bloginfo('description');
+		} else {
+			// For other pages, use page-specific meta
+			$resolver = $this->getResolver();
+			$metaConfig = $this->config->meta();
+
+			if (!empty($metaConfig['title'])) {
+				$webpage['name'] = $resolver->resolve($metaConfig['title']);
+			}
+
+			if (!empty($metaConfig['description'])) {
+				$webpage['description'] = $resolver->resolve($metaConfig['description']);
+			}
+		}
+
+		return $webpage;
+	}
+
+	/**
+	 * Build breadcrumb schema
+	 */
+	private function buildBreadcrumbSchema(): array
+	{
+		$breadcrumbs = BreadcrumbManager::getInstance();
+		return $breadcrumbs->toSchema();
+	}
+
+	/**
+	 * Get current context (what page/content are we on?)
+	 */
+	private function getCurrentContext(): ?array
+	{
+		if (is_singular()) {
+			$post = get_post();
+			if ($post) {
+				$postType = jvbNoBase($post->post_type);
+				if (defined('JVB_CONTENT') && isset(JVB_CONTENT[$postType])) {
+					$this->config = ConfigManager::for($postType);
+					return [
+						'objectType' => 'post',
+						'objectId'   => $post->ID,
+						'type'       => $postType,
+					];
+				}
+			}
+		} elseif (is_tax()) {
+			$term = get_queried_object();
+			if ($term instanceof WP_Term) {
+				$taxonomy = jvbNoBase($term->taxonomy);
+				if (defined('JVB_TAXONOMY') && isset(JVB_TAXONOMY[$taxonomy])) {
+					$this->config = ConfigManager::for($taxonomy);
+					return [
+						'objectType' => 'term',
+						'objectId'   => $term->term_id,
+						'type'       => $taxonomy,
+					];
+				}
+			}
+		} elseif (is_author()) {
+			$user = get_queried_object();
+			if ($user instanceof WP_User) {
+				$role = jvbUserRole($user->ID);
+				if (defined('JVB_USER') && isset(JVB_USER[$role])) {
+					$this->config = ConfigManager::for($role);
+					return [
+						'objectType' => 'user',
+						'objectId'   => $user->ID,
+						'type'       => $role,
+					];
+				}
+			}
+		}  elseif (is_post_type_archive()) {
+			$postType = get_query_var('post_type');
+			if (is_array($postType)) {
+				$postType = reset($postType);
+			}
+			$postType = jvbNoBase($postType);
+
+			if (defined('JVB_CONTENT') && isset(JVB_CONTENT[$postType])) {
+				$this->config = ConfigManager::for($postType);
+				return [
+					'objectType' => 'archive',
+					'objectId' => 0,
+					'type' => $postType,
+				];
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get or create resolver for current context
+	 */
+	private function getResolver(): TemplateResolver
+	{
+		if ($this->resolver === null) {
+			$this->resolver = TemplateResolver::forCurrentObject();
+		}
+		return $this->resolver;
+	}
+
+	/**
+	 * Extract URLs from array of link objects or strings
+	 */
+	private function extractUrls(array $links): array
+	{
+		$urls = [];
+		foreach ($links as $link) {
+			if (is_array($link) && isset($link['url'])) {
+				$urls[] = $link['url'];
+			} elseif (is_string($link)) {
+				$urls[] = $link;
+			}
+		}
+		return $urls;
+	}
+}
diff --git a/inc/managers/SEO/SchemaReferenceBuilder.php b/inc/managers/SEO/SchemaReferenceBuilder.php
new file mode 100644
index 0000000..507adea
--- /dev/null
+++ b/inc/managers/SEO/SchemaReferenceBuilder.php
@@ -0,0 +1,539 @@
+<?php
+namespace JVBase\managers\SEO;
+
+use JVBase\meta\MetaManager;
+use WP_Term;
+use WP_User;
+use WP_Post;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Builds minimal schema references for related entities
+ *
+ * When an artist references a shop or an artwork references an artist,
+ * we don't want to embed the full schema—just a reference with minimal data.
+ *
+ * Usage:
+ * - Artist referencing a Shop: "worksFor": SchemaReferenceBuilder::build('term', $shop_id)
+ * - Artwork referencing an Artist: "creator": SchemaReferenceBuilder::build('post', $artist_id)
+ */
+class SchemaReferenceBuilder
+{
+	/**
+	 * Build a schema reference
+	 * Automatically transforms types for archives (FAQPage → Question)
+	 *
+	 * @param string $objectType 'post', 'term', or 'user'
+	 * @param int $objectId Object ID
+	 * @param string|null $schemaType Override @type (null = auto-infer and transform)
+	 * @param bool $includeContext Add contextual fields
+	 * @return array|string Schema reference or empty string if invalid
+	 */
+	public static function build(
+		string $objectType,
+		int $objectId,
+		?string $schemaType = null,
+		bool $includeContext = true
+	): array|string {
+		// Get basic info
+		$url = self::getUrl($objectType, $objectId);
+		$name = self::getName($objectType, $objectId);
+
+		if (!$url || !$name) {
+			return '';
+		}
+
+		// Get config for templates and schema type
+		$config = self::getConfigFor($objectType, $objectId);
+		$schemaConfig = $config['seo']['schema'] ?? [];
+
+		// Determine schema type
+		if ($schemaType === null) {
+			// Auto-infer from config
+			$schemaType = self::inferSchemaType($objectType, $objectId);
+			$inferredType = true;
+		} else {
+			// Explicit type provided (for cross-references)
+			$inferredType = false;
+		}
+
+		// If type was inferred, check for archive transformations
+		if ($inferredType) {
+			$schemaType = self::transformForArchive($schemaType);
+		}
+
+		// Create resolver for template resolution
+		$resolver = $objectType === 'post' ? new TemplateResolver($objectId,'post') :
+			($objectType === 'term' ? new TemplateResolver($objectId,'term') :
+				($objectType === 'user' ? new TemplateResolver($objectId, 'user') : null));
+
+		// Build reference based on schema type
+		switch ($schemaType) {
+			case 'Question':
+				// Build Question from FAQPage config
+				$questionTemplate = $schemaConfig['question'] ?? '{{post_title}}';
+				$answerTemplate = $schemaConfig['answer'] ?? '{{post_content}}';
+
+				return [
+					'@type' => 'Question',
+					'@id' => $url . '#question',
+					'name' => $resolver ? $resolver->resolve($questionTemplate) : $name,
+					'acceptedAnswer' => [
+						'@type' => 'Answer',
+						'text' => $resolver ? $resolver->resolve($answerTemplate) : ''
+					]
+				];
+
+			default:
+				// Standard reference: @type, @id, name, url
+				$reference = [
+					'@type' => $schemaType,
+					'@id'   => $url . '#' . strtolower($schemaType),
+					'name'  => $name,
+					'url'   => $url,
+				];
+
+				// Add contextual fields if requested
+				if ($includeContext) {
+					// Add description if in config
+					if ($resolver && isset($schemaConfig['description'])) {
+						$description = $resolver->resolve($schemaConfig['description']);
+						if ($description) {
+							$reference['description'] = $description;
+						}
+					}
+
+					// Add image if in config
+					if ($resolver && isset($schemaConfig['image'])) {
+						$imageUrl = $resolver->resolve($schemaConfig['image']);
+						if ($imageUrl) {
+							$reference['image'] = SchemaFieldHelpers::image_object($imageUrl);
+						}
+					}
+
+					// Add minimal type-specific fields (existing logic)
+					$reference = self::addMinimalFields($reference, $objectType, $objectId, $schemaType);
+				}
+
+				return $reference;
+		}
+	}
+
+	/**
+	 * Transform schema types for archive/collection contexts
+	 *
+	 * @param string $schemaType Original schema type
+	 * @return string Transformed type (or original if no transform needed)
+	 */
+	private static function transformForArchive(string $schemaType): string
+	{
+		return match($schemaType) {
+			'FAQPage' => 'Question',
+			// Add other transformations as needed
+			default => $schemaType
+		};
+	}
+
+	/**
+	 * Get config for an object
+	 */
+	private static function getConfigFor(string $objectType, int $objectId): ?array
+	{
+		switch ($objectType) {
+			case 'post':
+				$postType = get_post_type($objectId);
+				$typeKey = str_replace(BASE, '', $postType);
+				return defined('JVB_CONTENT') && isset(JVB_CONTENT[$typeKey])
+					? JVB_CONTENT[$typeKey]
+					: null;
+
+			case 'term':
+				$term = get_term($objectId);
+				if (!$term || is_wp_error($term)) {
+					return null;
+				}
+				$typeKey = str_replace(BASE, '', $term->taxonomy);
+				return defined('JVB_TAXONOMY') && isset(JVB_TAXONOMY[$typeKey])
+					? JVB_TAXONOMY[$typeKey]
+					: null;
+
+			case 'user':
+				$role = jvbUserRole($objectId);
+				return defined('JVB_USER') && isset(JVB_USER[$role])
+					? JVB_USER[$role]
+					: null;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Build just an @id reference (most minimal)
+	 *
+	 * @param string $objectType 'post', 'term', or 'user'
+	 * @param int $objectId Object ID
+	 * @param string|null $schemaType Override @type
+	 * @return string @id URL or empty string
+	 */
+	public static function buildIdOnly(
+		string $objectType,
+		int $objectId,
+		?string $schemaType = null
+	): string {
+		$url = self::getUrl($objectType, $objectId);
+		if (!$url) {
+			return '';
+		}
+
+		if (!$schemaType) {
+			$schemaType = self::inferSchemaType($objectType, $objectId);
+		}
+
+		return $url . '#' . strtolower($schemaType);
+	}
+
+	/**
+	 * Build array of references from array of IDs
+	 *
+	 * @param string $objectType 'post', 'term', or 'user'
+	 * @param array $objectIds Array of object IDs
+	 * @param string|null $schemaType Override @type
+	 * @param bool $includeContext Add contextual fields
+	 * @return array Array of schema references
+	 */
+	public static function buildMultiple(
+		string $objectType,
+		array $objectIds,
+		?string $schemaType = null,
+		bool $includeContext = false
+	): array {
+		$references = [];
+
+		foreach ($objectIds as $id) {
+			$ref = self::build($objectType, $id, $schemaType, $includeContext);
+			if ($ref !== '') {
+				$references[] = $ref;
+			}
+		}
+
+		return $references;
+	}
+
+	/**
+	 * Build references for posts related to a term
+	 *
+	 * Perfect for: shop showing its artists, style showing its artists, etc.
+	 *
+	 * @param int $termId Term ID to get posts from
+	 * @param string $postType Post type to query (without BASE prefix)
+	 * @param int $limit Maximum number of references to return (default: 10)
+	 * @param string|null $schemaType Override @type for all references
+	 * @param bool $includeContext Add contextual fields
+	 * @param string $orderby How to order results (default: 'date')
+	 * @return array Array of schema references
+	 */
+	public static function buildFromTerm(
+		int $termId,
+		string $postType,
+		int $limit = 10,
+		?string $schemaType = null,
+		bool $includeContext = false,
+		string $orderby = 'date'
+	): array {
+		$term = get_term($termId);
+		if (!$term || is_wp_error($term)) {
+			return [];
+		}
+
+		// Get posts in this term
+		$args = [
+			'post_type'      => jvbCheckBase($postType),
+			'posts_per_page' => $limit,
+			'post_status'    => 'publish',
+			'orderby'        => $orderby,
+			'order'          => 'DESC',
+			'tax_query'      => [
+				[
+					'taxonomy' => $term->taxonomy,
+					'field'    => 'term_id',
+					'terms'    => $termId,
+				]
+			],
+			'fields'         => 'ids', // Only get IDs for performance
+		];
+
+		$post_ids = get_posts($args);
+
+		if (empty($post_ids)) {
+			return [];
+		}
+
+		return self::buildMultiple('post', $post_ids, $schemaType, $includeContext);
+	}
+
+	/**
+	 * Build references for posts in a post type archive
+	 *
+	 * @param string $postType Post type (without BASE prefix)
+	 * @param int $limit Maximum number of references to return (default: 10)
+	 * @param bool $includeContext Add contextual fields
+	 * @param string $orderby How to order results (default: 'date')
+	 * @return array Array of schema references
+	 */
+	public static function buildFromArchive(
+		string $postType,
+		int $limit = 10,
+		bool $includeContext = true,
+		string $orderby = 'date'
+	): array {
+		// Get posts from current query or fresh query
+		global $wp_query;
+
+		// If we're already on the archive, use those posts
+		if (is_post_type_archive() && !empty($wp_query->posts)) {
+			$post_ids = wp_list_pluck($wp_query->posts, 'ID');
+		} else {
+			// Otherwise query fresh
+			$args = [
+				'post_type'      => jvbCheckBase($postType),
+				'posts_per_page' => $limit,
+				'post_status'    => 'publish',
+				'orderby'        => $orderby,
+				'order'          => 'DESC',
+				'fields'         => 'ids',
+			];
+			$post_ids = get_posts($args);
+		}
+
+		if (empty($post_ids)) {
+			return [];
+		}
+
+		// Let build() infer types and transform as needed
+		return self::buildMultiple('post', $post_ids, null, $includeContext);
+	}
+
+	/**
+	 * Build references for terms related to a post
+	 *
+	 * Perfect for: artist showing their styles, artwork showing its themes, etc.
+	 *
+	 * @param int $postId Post ID to get terms from
+	 * @param string $taxonomy Taxonomy to query (without BASE prefix)
+	 * @param int $limit Maximum number of references to return (default: 10)
+	 * @param string|null $schemaType Override @type for all references
+	 * @param bool $includeContext Add contextual fields
+	 * @return array Array of schema references
+	 */
+	public static function buildFromPost(
+		int $postId,
+		string $taxonomy,
+		int $limit = 10,
+		?string $schemaType = null,
+		bool $includeContext = false
+	): array {
+		$terms = wp_get_post_terms($postId, jvbCheckBase($taxonomy), [
+			'number' => $limit,
+			'fields' => 'ids',
+		]);
+
+		if (is_wp_error($terms) || empty($terms)) {
+			return [];
+		}
+
+		return self::buildMultiple('term', $terms, $schemaType, $includeContext);
+	}
+
+	/**
+	 * Build ID-only references (most performant)
+	 *
+	 * Use when you just need the @id URIs without any additional data
+	 *
+	 * @param string $objectType 'post', 'term', or 'user'
+	 * @param array $objectIds Array of object IDs
+	 * @param string|null $schemaType Override @type
+	 * @return array Array of @id strings
+	 */
+	public static function buildIdOnlyMultiple(
+		string $objectType,
+		array $objectIds,
+		?string $schemaType = null
+	): array {
+		$ids = [];
+
+		foreach ($objectIds as $id) {
+			$idString = self::buildIdOnly($objectType, $id, $schemaType);
+			if ($idString !== '') {
+				$ids[] = $idString;
+			}
+		}
+
+		return $ids;
+	}
+
+	/**
+	 * Get URL for object
+	 */
+	private static function getUrl(string $objectType, int $objectId): string
+	{
+		return match($objectType) {
+			'post' => get_permalink($objectId) ?: '',
+			'term' => is_wp_error($link = get_term_link($objectId)) ? '' : $link,
+			'user' => get_author_posts_url($objectId) ?: '',
+			default => ''
+		};
+	}
+
+	/**
+	 * Get name for object
+	 */
+	private static function getName(string $objectType, int $objectId): string
+	{
+		return match($objectType) {
+			'post' => get_the_title($objectId) ?: '',
+			'term' => get_term($objectId)?->name ?: '',
+			'user' => get_userdata($objectId)?->display_name ?: '',
+			default => ''
+		};
+	}
+
+	/**
+	 * Infer schema type from content configuration
+	 */
+	private static function inferSchemaType(string $objectType, int $objectId): string
+	{
+		if ($objectType === 'post') {
+			$postType = get_post_type($objectId);
+			$typeKey = str_replace(BASE, '', $postType);
+
+			if (defined('JVB_CONTENT') && isset(JVB_CONTENT[$typeKey])) {
+				$config = JVB_CONTENT[$typeKey];
+				return $config['seo']['schema']['type'] ?? 'CreativeWork';
+			}
+
+			return 'CreativeWork';
+		}
+
+		if ($objectType === 'term') {
+			$term = get_term($objectId);
+			if (!$term || is_wp_error($term)) {
+				return 'DefinedTerm';
+			}
+
+			$taxonomyKey = str_replace(BASE, '', $term->taxonomy);
+
+			if (defined('JVB_TAXONOMY') && isset(JVB_TAXONOMY[$taxonomyKey])) {
+				$config = JVB_TAXONOMY[$taxonomyKey];
+				return $config['seo']['schema']['type'] ?? 'DefinedTerm';
+			}
+
+			return 'DefinedTerm';
+		}
+
+		if ($objectType === 'user') {
+			return 'Person';
+		}
+
+		return 'Thing';
+	}
+
+	/**
+	 * Add minimal context-specific fields based on schema type
+	 */
+	private static function addMinimalFields(
+		array $reference,
+		string $objectType,
+		int $objectId,
+		string $schemaType
+	): array {
+		switch ($schemaType) {
+			case 'Person':
+				// Add small thumbnail image if available
+				$imageId = null;
+
+				if ($objectType === 'user') {
+					$imageId = get_user_meta($objectId, 'image_portrait', true);
+				} elseif ($objectType === 'post') {
+					$imageId = get_post_thumbnail_id($objectId);
+				}
+
+				if ($imageId) {
+					$imageUrl = wp_get_attachment_image_url($imageId, 'thumbnail');
+					if ($imageUrl) {
+						$reference['image'] = $imageUrl; // Simple URL for refs
+					}
+				}
+
+				// Add job title if in user meta
+				if ($objectType === 'user') {
+					$jobTitle = get_user_meta($objectId, 'job_title', true);
+					if ($jobTitle) {
+						$reference['jobTitle'] = $jobTitle;
+					}
+				}
+				break;
+
+			case 'LocalBusiness':
+			case 'TattooParlor':
+			case 'Organization':
+				// Add minimal location (just street address)
+				$meta = new MetaManager($objectId, $objectType);
+				$location = $meta->getValue('location');
+
+				if ($location && isset($location['address'])) {
+					$reference['address'] = [
+						'@type'         => 'PostalAddress',
+						'streetAddress' => $location['address']
+					];
+				}
+				break;
+
+			case 'CreativeWork':
+			case 'VisualArtwork':
+			case 'Article':
+				// Add featured image if available
+				if ($objectType === 'post') {
+					$imageId = get_post_thumbnail_id($objectId);
+					if ($imageId) {
+						$imageUrl = wp_get_attachment_image_url($imageId, 'medium');
+						if ($imageUrl) {
+							$reference['image'] = $imageUrl;
+						}
+					}
+				}
+				break;
+
+			case 'DefinedTerm':
+				// Add term description if available
+				if ($objectType === 'term') {
+					$term = get_term($objectId);
+					if ($term && !is_wp_error($term) && !empty($term->description)) {
+						$reference['description'] = wp_trim_words($term->description, 20);
+					}
+				} elseif ($objectType === 'post') {
+					// Add post content as description
+					$post = get_post($objectId);
+					if ($post && !empty($post->post_content)) {
+						$reference['description'] = wp_trim_words(strip_tags($post->post_content), 20);
+					}
+
+					// If we're in an archive context, add inDefinedTermSet
+					if (is_post_type_archive()) {
+						$postType = get_post_type($objectId);
+						$archiveUrl = get_post_type_archive_link($postType);
+						if ($archiveUrl) {
+							$reference['inDefinedTermSet'] = [
+								'@id' => $archiveUrl . '#definedtermset'
+							];
+						}
+					}
+				}
+				break;
+		}
+
+		return $reference;
+	}
+}
diff --git a/inc/managers/SEO/SchemaRegistry.php b/inc/managers/SEO/SchemaRegistry.php
new file mode 100644
index 0000000..df1b6d2
--- /dev/null
+++ b/inc/managers/SEO/SchemaRegistry.php
@@ -0,0 +1,1857 @@
+<?php
+namespace JVBase\managers\SEO;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Schema.org Registry - Centralized field and type definitions
+ *
+ * Field definitions use MetaManager field types and include transformer hints.
+ * Types reference field names and support inheritance via 'extends'.
+ */
+class SchemaRegistry
+{
+	private static ?self $instance = null;
+	private array $fieldDefinitions = [];
+	private array $typeDefinitions = [];
+	private array $typeGroups = [];
+
+	private array $metaFields = ['metaTitle', 'metaDescription','socialPreviewImage', 'twitterImage'];
+
+	private array $defaultMetaValues = [
+		'title' => '{{post_title}} | {{site_name}}',
+		'description' => '{{post_excerpt}}',
+		'image' => '{{featured_image}}',
+		'twitter_image' => ''
+	];
+
+	public static function getInstance(): self
+	{
+		if (self::$instance === null) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+
+	public array $schemaTypes = [
+		'WebSite'			=> 'Web Site',
+		'Organization' 		=> 'Organization',
+		'LocalBusiness' 	=> '  - Local Business',
+		'TattooParlor' 		=> '    - - Tattoo Shop',
+		'HealthBusiness' 	=> '    - - Health Business',
+		'FoodEstablishment' => '    - - Restaurant',
+		'WebPage' 			=> 'Web Page',
+		'CollectionPage'	=> '  - Collection Page',
+		'FAQPage'			=> '  - FAQ Page',
+		'Person'			=> 'Person',
+		'CreativeWork'		=> 'Creative Work',
+		'DefinedTerm'		=> '  - Defined Term',
+		'VisualArtwork'		=> '  - Visual Artwork',
+		'Tattoo'			=> '    - - Tattoo',
+		'BeforeAfter'		=> '  - Before & After',
+		'Product'			=> 'Product',
+		'Event'				=> 'Event',
+	];
+
+	private function __construct()
+	{
+		$this->registerFieldDefinitions();
+		$this->registerTypeDefinitions();
+		$this->registerTypeGroups();
+
+		do_action(BASE . 'schema_registry_loaded', $this);
+	}
+
+	/**
+	 * Get field definition for a specific field
+	 */
+	public function getFieldDefinition(string $fieldName): ?array
+	{
+		$definitions = $this->getFieldDefinitions();
+		return $definitions[$fieldName] ?? null;
+	}
+
+	/**
+	 * Get all field definitions
+	 */
+	public function getFieldDefinitions(): array
+	{
+		return apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions);
+	}
+
+	public function getMetaFields(): array
+	{
+		return $this->metaFields;
+	}
+
+	public function getDefaultMetaValues(): array
+	{
+		return $this->defaultMetaValues;
+	}
+
+	/**
+	 * Get type definition
+	 */
+	public function getTypeDefinition(string $type): ?array
+	{
+		$definitions = $this->getTypeDefinitions();
+		return $definitions[$type] ?? null;
+	}
+
+	/**
+	 * Get all type definitions
+	 */
+	public function getTypeDefinitions(): array
+	{
+		return apply_filters(BASE . 'schema_type_definitions', $this->typeDefinitions);
+	}
+
+	/**
+	 * Get all fields for a specific type (with inheritance)
+	 */
+	public function getFieldsForType(string $type): array
+	{
+		$fields = [];
+
+		$typeDefinition = $this->getTypeDefinition($type);
+		if (!$typeDefinition) {
+			return $fields;
+		}
+
+		$fields = array_merge($fields, $typeDefinition['fields'] ?? []);
+
+		// Handle inheritance
+		if (!empty($typeDefinition['extends'])) {
+			$parentFields = $this->getFieldsForType($typeDefinition['extends']);
+			$fields = array_unique(array_merge($parentFields, $fields));
+		}
+
+		return $fields;
+	}
+
+	/**
+	 * Get MetaManager configuration for a schema type
+	 * This creates the form fields for the selected @type
+	 */
+	public function getMetaConfigForType(string $type): array
+	{
+		$fields = $this->getFieldsForType($type);
+		$config = [];
+
+		foreach ($fields as $fieldName) {
+			$fieldDef = $this->getFieldDefinition($fieldName);
+			if ($fieldDef) {
+				// Use the field name as the key (this IS the schema property)
+				$config[$fieldName] = $fieldDef;
+			}
+		}
+
+		return $config;
+	}
+
+	/**
+	 * Get types organized by group for UI display
+	 */
+	public function getTypesByGroup(): array
+	{
+		$types = $this->getTypeDefinitions();
+		$grouped = [];
+
+		foreach ($types as $typeName => $config) {
+			$group = $config['group'] ?? 'general';
+
+			if (!isset($grouped[$group])) {
+				$grouped[$group] = [
+					'label' => $this->typeGroups[$group] ?? ucfirst($group),
+					'types' => []
+				];
+			}
+
+			$grouped[$group]['types'][$typeName] = $config['label'] ?? $typeName;
+		}
+
+		return $grouped;
+	}
+
+	/**
+	 * Register all field definitions
+	 * Array key = schema property name
+	 */
+	private function registerFieldDefinitions(): void
+	{
+		$this->fieldDefinitions = [
+			'type'	=> [
+				'type' => 'select',
+				'label' => 'Type',
+				'options' => array_merge(['' => '-- Content Type'], $this->schemaTypes)
+			],
+			/**************************************************************
+			META FIELDS
+			 **************************************************************/
+			'metaTitle' => [
+				'type' => 'text',
+				'label' => 'Meta Title',
+				'hint' => 'Used in search results and when shared on social media. Leave blank to use default.',
+				'default' => '{{post_title}} | {{site_name}}'
+			],
+			'metaDescription' => [
+				'type' => 'textarea',
+				'label' => 'Meta Description',
+				'hint' => 'Brief description shown in search results and social previews.',
+				'default' => '{{post_excerpt}}',
+				'rows' => 3
+			],
+			'socialPreviewImage' => [
+				'type' => 'upload',
+				'label' => 'Social Preview Image',
+				'hint' => 'Image shown when shared on social media. Recommended: 1200x630px.',
+				'transformer' => 'image_url'
+			],
+			'twitterImage' => [
+				'type' => 'upload',
+				'label' => 'Twitter Card Image (Optional)',
+				'hint' => 'Separate image for Twitter. Falls back to main image if empty.',
+				'transformer' => 'image_url'
+			],
+			/**************************************************************
+			 CORE IDENTITY FIELDS
+			**************************************************************/
+			'name' => [
+				'type' => 'text',
+				'label' => 'Name',
+				'description' => 'The name of the item',
+				'transformer' => 'text',
+			],
+
+			'alternateName' => [
+				'type' => 'repeater',
+				'label' => 'Alternate Name(s)',
+				'description' => 'Alternative names or nicknames',
+				'transformer' => 'text_array',
+				'fields' => [
+					'name' => [
+						'type' => 'text',
+						'label' => 'Name',
+					]
+				]
+			],
+
+			'legalName' => [
+				'type' => 'text',
+				'label' => 'Legal Name',
+				'description' => 'The official legal name',
+				'transformer' => 'text',
+			],
+
+			'description' => [
+				'type' => 'textarea',
+				'label' => 'Description',
+				'description' => 'A description of the item',
+				'transformer' => 'text',
+			],
+
+			'disambiguatingDescription' => [
+				'type' => 'textarea',
+				'label' => 'Disambiguating Description',
+				'description' => 'Brief clarification to distinguish from similar items',
+				'transformer' => 'text',
+			],
+
+			'url' => [
+				'type' => 'url',
+				'label' => 'URL',
+				'description' => 'Website URL',
+				'transformer' => 'url',
+			],
+
+			'slogan' => [
+				'type' => 'text',
+				'label' => 'Slogan',
+				'description' => 'A slogan or tagline',
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			Before/After
+			 **************************************************************/
+			'about' => [
+				'type' => 'reference',
+				'label' => 'About (Service/Topic)',
+				'transformer' => 'reference',
+			],
+
+			'temporalCoverage' => [
+				'type' => 'text',
+				'label' => 'Time Period',
+				'description' => 'ISO 8601 format: 2024-01-10/2024-09-01',
+				'transformer' => 'text',
+			],
+
+			'associatedMedia' => [
+				'type' => 'repeater',
+				'label' => 'Associated Media',
+				'transformer' => 'image_object_array',
+				'fields' => [
+					'image' => ['type' => 'image', 'label' => 'Image'],
+					'caption' => ['type' => 'text', 'label' => 'Caption'],
+					'position' => ['type' => 'number', 'label' => 'Position'],
+				]
+			],
+
+			'additionalProperty' => [
+				'type' => 'repeater',
+				'label' => 'Additional Properties',
+				'transformer' => 'property_value_array',
+				'fields' => [
+					'name' => ['type' => 'text', 'label' => 'Property Name'],
+					'value' => ['type' => 'text', 'label' => 'Value'],
+				]
+			],
+			/**************************************************************
+			 IMAGE FIELDS
+			**************************************************************/
+			'image' => [
+				'type' => 'image',
+				'label' => 'Image',
+				'description' => 'Primary image',
+				'transformer' => 'image_object',
+			],
+
+			'logo' => [
+				'type' => 'upload',
+				'label' => 'Logo',
+				'transformer' => 'image_object',
+			],
+
+			'photo' => [
+				'type' => 'upload',
+				'label' => 'Photo of Location',
+				'transformer' => 'image_object',
+			],
+
+			/**************************************************************
+			 LOCATION & CONTACT FIELDS
+			**************************************************************/
+			'location' => [
+				'type' => 'location',
+				'label' => 'Location',
+				'description' => 'Physical location with address and coordinates',
+				'transformer' => 'location_complex', // Returns array with 'address' and 'geo'
+			],
+
+			'address' => [
+				'type' => 'location',
+				'label' => 'Address',
+				'description' => 'Postal address',
+				'transformer' => 'postal_address',
+			],
+
+			'geo' => [
+				'type' => 'group',
+				'label' => 'Geographic Coordinates',
+				'description' => 'Latitude and longitude',
+				'transformer' => 'geo_coordinates',
+				'fields' => [
+					'latitude' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Latitude',
+					],
+					'longitude' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Longitude',
+					]
+				]
+			],
+
+			'telephone' => [
+				'type' => 'text',
+				'subtype'=> 'tel',
+				'label' => 'Telephone',
+				'description' => 'Phone number',
+				'transformer' => 'text',
+			],
+
+			'faxNumber' => [
+				'type' => 'text',
+				'subtype'=> 'tel',
+				'label' => 'Fax Number',
+				'transformer' => 'text',
+			],
+
+			'email' => [
+				'type' => 'email',
+				'label' => 'Email',
+				'description' => 'Email address',
+				'transformer' => 'email',
+			],
+
+			'contactPoint' => [
+				'type' => 'repeater',
+				'label' => 'Contact Points',
+				'description' => 'Additional contact methods',
+				'transformer' => 'contact_point_array',
+				'fields' => [
+					'contactType' => [
+						'type' => 'text',
+						'label' => 'Contact Type',
+						'description' => 'e.g., customer service, sales',
+					],
+					'telephone' => [
+						'type' => 'text',
+						'label' => 'Phone',
+					],
+					'email' => [
+						'type' => 'email',
+						'label' => 'Email',
+					]
+				]
+			],
+
+			'potentialAction' => [
+				'type'	=> 'repeater',
+				'label'	=> 'Potential Actions',
+				'fields'	=> [
+					'action' => [
+						'type'	=> 'radio',
+						'label'	=> 'Action',
+						'options'	=> [
+							'searchAction'	=> 'Search Action',
+							'communicateAction' => 'Contact Action',
+							'scheduleAction'	=> 'Reserve Action',
+							'applyAction'		=> 'Estimate Action'
+						]
+					],
+					'name'	=> [
+						'type'	=> 'text',
+						'label' => 'Name',
+					],
+					'target'	=> [
+						'type'	=> 'url',
+						'label'	=> 'Action URL',
+					],
+					'description'	=> [
+						'type'	=> 'textarea',
+						'label'	=> 'Description'
+					]
+				],
+				'default'	=> [
+					[
+						'action'	=> 'searchAction',
+						'target'	=> get_home_url(null,'/search/?s={query}')
+					]
+				],
+				'transformer' 	=> 'potential_action_array'
+			],
+
+			/**************************************************************
+			 HOURS & OPERATIONAL FIELDS
+			**************************************************************/
+			'openingHours' => [
+				'type' => 'group',
+				'label' => 'Opening Hours',
+				'description' => 'Business hours specification',
+				'transformer' => 'opening_hours_specification',
+				'fields' => [
+					'monday' => [
+						'type' => 'group',
+						'label' => 'Monday',
+						'fields'	=> [
+							'opens' => [
+								'type'	=> 'time',
+								'label'	=> 'Opens'
+							],
+							'closes' => [
+								'type'	=> 'time',
+								'label'	=> 'Closes'
+							]
+						]
+					],
+					'tuesday' =>  [
+						'type' => 'group',
+						'label' => 'Tuesday',
+						'fields'	=> [
+							'opens' => [
+								'type'	=> 'time',
+								'label'	=> 'Opens'
+							],
+							'closes' => [
+								'type'	=> 'time',
+								'label'	=> 'Closes'
+							]
+						]
+					],
+					'wednesday' =>  [
+						'type' => 'group',
+						'label' => 'Wednesday',
+						'fields'	=> [
+							'opens' => [
+								'type'	=> 'time',
+								'label'	=> 'Opens'
+							],
+							'closes' => [
+								'type'	=> 'time',
+								'label'	=> 'Closes'
+							]
+						]
+					],
+					'thursday' =>  [
+						'type' => 'group',
+						'label' => 'Thursday',
+						'fields'	=> [
+							'opens' => [
+								'type'	=> 'time',
+								'label'	=> 'Opens'
+							],
+							'closes' => [
+								'type'	=> 'time',
+								'label'	=> 'Closes'
+							]
+						]
+					],
+					'friday' =>  [
+						'type' => 'group',
+						'label' => 'Friday',
+						'fields'	=> [
+							'opens' => [
+								'type'	=> 'time',
+								'label'	=> 'Opens'
+							],
+							'closes' => [
+								'type'	=> 'time',
+								'label'	=> 'Closes'
+							]
+						]
+					],
+					'saturday' =>  [
+						'type' => 'group',
+						'label' => 'Saturday',
+						'fields'	=> [
+							'opens' => [
+								'type'	=> 'time',
+								'label'	=> 'Opens'
+							],
+							'closes' => [
+								'type'	=> 'time',
+								'label'	=> 'Closes'
+							]
+						]
+					],
+					'sunday' =>  [
+						'type' => 'group',
+						'label' => 'Sunday',
+						'fields'	=> [
+							'opens' => [
+								'type'	=> 'time',
+								'label'	=> 'Opens'
+							],
+							'closes' => [
+								'type'	=> 'time',
+								'label'	=> 'Closes'
+							]
+						]
+					],
+				]
+			],
+			'hasPart' => [
+				'type' => 'repeater',
+				'label' => 'Site Navigation',
+				'description' => 'Main navigation menu items',
+				'transformer' => 'navigation_array',
+				'fields' => [
+					'name' => ['type' => 'text', 'label' => 'Link Text'],
+					'url' => ['type' => 'url', 'label' => 'URL'],
+					'description' => ['type' => 'textarea', 'label' => 'Description (optional)'],
+				]
+			],
+
+			'priceRange' => [
+				'type' => 'text',
+				'label' => 'Price Range',
+				'description' => 'e.g., $$, $100-$500',
+				'transformer' => 'text',
+			],
+
+			'currenciesAccepted' => [
+				'type' => 'checkbox',
+				'label' => 'Currencies Accepted',
+				'options'	=> [
+					'CAD'	=> 'CAD',
+					'USD'	=> 'USD',
+				],
+				'transformer' => 'text',
+			],
+
+			'paymentAccepted' => [
+				'type' => 'checkbox',
+				'label' => 'Payment Methods',
+				'options' => [
+					'Cash'        => 'Cash',
+					'Credit Card' => 'Credit Card',
+					'Debit'       => 'Debit',
+					'Google Pay'  => 'Google Pay',
+					'Apple Pay'   => 'Apple Pay',
+					'PayPal'      => 'PayPal',
+					'Interac'     => 'Interac',
+					'AMEX'        => 'AMEX',
+				],
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			 ORGANIZATION & BUSINESS FIELDS
+			**************************************************************/
+			'foundingDate' => [
+				'type' => 'date',
+				'label' => 'Founding Date',
+				'description' => 'Date the organization was founded',
+				'transformer' => 'date',
+			],
+
+			'dissolutionDate' => [
+				'type' => 'date',
+				'label' => 'Dissolution Date',
+				'description' => 'Date the organization closed',
+				'transformer' => 'date',
+			],
+
+			'founders' => [
+				'type' => 'repeater',
+				'label' => 'Founders',
+				'description' => 'Name of founder(s)',
+				'fields'	=> [
+					'name'	=> [
+						'type'	=> 'text',
+						'label'	=> 'Name',
+					],
+					'url'	=> [
+						'type'	=> 'url',
+						'label'	=> 'URL',
+					]
+				],
+				'transformer' => 'founders',
+			],
+
+			'numberOfEmployees' => [
+				'type' => 'text',
+				'subtype' => 'number',
+				'label' => 'Number of Employees',
+				'transformer' => 'number',
+			],
+
+			'taxID' => [
+				'type' => 'text',
+				'label' => 'Tax ID',
+				'description' => 'Tax identification number',
+				'transformer' => 'text',
+			],
+
+			'vatID' => [
+				'type' => 'text',
+				'label' => 'VAT ID',
+				'description' => 'VAT registration number',
+				'transformer' => 'text',
+			],
+
+			'duns' => [
+				'type' => 'text',
+				'label' => 'D-U-N-S Number',
+				'description' => 'Dun & Bradstreet number',
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			 SOCIAL & LINKS
+			**************************************************************/
+			'sameAs' => [
+				'type' => 'repeater',
+				'label' => 'Social Media & Links',
+				'description' => 'URLs to social profiles and related pages',
+				'transformer' => 'url_array',
+				'fields' => [
+					'url' => [
+						'type' => 'url',
+						'label' => 'URL',
+					]
+				]
+			],
+
+			/**************************************************************
+			 AREA & GEOGRAPHY
+			**************************************************************/
+			'areaServed' => [
+				'type' => 'repeater',
+				'label' => 'Area Served',
+				'description' => 'Geographic areas served',
+				'transformer' => 'text_array',
+				'fields' => [
+					'name' => [
+						'type' => 'text',
+						'label' => 'Location Name',
+					],
+					'url'	=> [
+						'type'	=> 'url',
+						'label'	=> 'Wikipedia Page',
+					]
+				]
+			],
+
+			'hasMap' => [
+				'type' => 'url',
+				'label' => 'Map URL',
+				'description' => 'Link to a map (e.g., Google Maps)',
+				'transformer' => 'url',
+			],
+
+			/**************************************************************
+			 AMENITIES & FEATURES
+			**************************************************************/
+			'amenityFeature' => [
+				'type' => 'checkbox',
+				'label' => 'Amenity Features',
+				'description' => 'Available facilities and features',
+				'transformer' => 'text',
+				'options' => [
+					'Wheelchair Accessible'        => 'Wheelchair Accessible',
+					'Free Parking'                 => 'Free Parking',
+					'Private Rooms'                => 'Private Rooms',
+					'Air Conditioning'             => 'Air Conditioning',
+					'WiFi'                         => 'WiFi',
+					'Gender Neutral Restroom'      => 'Gender Neutral Restroom',
+					'LGBTQ+ Friendly'              => 'LGBTQ+ Friendly',
+					'Sterilization Room'           => 'Sterilization Room',
+					'Refreshments Available'       => 'Refreshments Available',
+					'Street Level Access'          => 'Street Level Access',
+					'Single Use Needles'           => 'Single Use Needles',
+					'Consultation Room'            => 'Consultation Room',
+					'Aftercare Products Available' => 'Aftercare Products Available',
+					'Walk-Ins Welcome'             => 'Walk-Ins Welcome',
+					'By Appointment'				=> 'By Appointment Only',
+				],
+			],
+
+			/**************************************************************
+			 LANGUAGES
+			**************************************************************/
+			'availableLanguage' => [
+				'type' => 'repeater',
+				'label' => 'Languages Available',
+				'description' => 'Languages spoken or supported',
+				'transformer' => 'language_array',
+				'fields' => [
+					'language' => [
+						'type' => 'text',
+						'label' => 'Language',
+					]
+				]
+			],
+
+			'knowsLanguage' => [
+				'type' => 'repeater',
+				'label' => 'Languages Known',
+				'description' => 'Languages the person knows',
+				'transformer' => 'language_array',
+				'fields' => [
+					'language' => [
+						'type' => 'text',
+						'label' => 'Language',
+					]
+				]
+			],
+
+			'inLanguage' => [
+				'type' => 'radio',
+				'label' => 'In Language',
+				'options'	=> [
+					'en-CA'	=> 'English, Canadian',
+					'en-US' => 'English, American',
+					'fr-CA'	=> 'French, Canadian'
+				],
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			 RATINGS & REVIEWS
+			**************************************************************/
+			'aggregateRating' => [
+				'type' => 'group',
+				'label' => 'Aggregate Rating',
+				'description' => 'Overall rating and review count',
+				'transformer' => 'aggregate_rating',
+				'fields' => [
+					'ratingValue' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Rating Value',
+						'description' => 'Average rating (e.g., 4.5)',
+					],
+					'bestRating' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Best Rating',
+						'default'	=> 5,
+						'description' => 'Highest possible rating (e.g., 5)',
+					],
+					'worstRating' => [
+						'default'	=> 1,
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Worst Rating',
+						'description' => 'Lowest possible rating (e.g., 1)',
+					],
+					'ratingCount' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Rating Count',
+						'description' => 'Total number of ratings',
+					],
+					'reviewCount' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Review Count',
+						'description' => 'Total number of reviews',
+					]
+				]
+			],
+
+			/**************************************************************
+			 KEYWORDS & CATEGORIZATION
+			**************************************************************/
+			'keywords' => [
+				'type' => 'repeater',
+				'label' => 'Keywords',
+				'description' => 'Keywords or tags',
+				'transformer' => 'text_array',
+				'fields' => [
+					'keyword' => [
+						'type' => 'text',
+						'label' => 'Keyword',
+					]
+				]
+			],
+
+			/**************************************************************
+			 PERSON FIELDS
+			**************************************************************/
+			'givenName' => [
+				'type' => 'text',
+				'label' => 'First Name',
+				'transformer' => 'text',
+			],
+
+			'familyName' => [
+				'type' => 'text',
+				'label' => 'Last Name',
+				'transformer' => 'text',
+			],
+
+			'honorificPrefix' => [
+				'type' => 'text',
+				'label' => 'Honorific Prefix',
+				'description' => 'e.g., Dr., Mr., Ms.',
+				'transformer' => 'text',
+			],
+
+			'honorificSuffix' => [
+				'type' => 'text',
+				'label' => 'Honorific Suffix',
+				'description' => 'e.g., PhD, MD',
+				'transformer' => 'text',
+			],
+
+			'jobTitle' => [
+				'type' => 'text',
+				'label' => 'Job Title',
+				'transformer' => 'text',
+			],
+
+			'birthDate' => [
+				'type' => 'date',
+				'label' => 'Birth Date',
+				'description' => 'For public figures',
+				'transformer' => 'date',
+			],
+
+			'gender' => [
+				'type' => 'text',
+				'label' => 'Gender',
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			 CREATIVE WORK FIELDS
+			**************************************************************/
+			'author' => [
+				'type' => 'text',
+				'label' => 'Author',
+				'description' => 'Author name or reference',
+				'transformer' => 'text',
+			],
+
+			'creator' => [
+				'type' => 'text',
+				'label' => 'Creator',
+				'description' => 'Creator name or reference',
+				'transformer' => 'text',
+			],
+
+			'dateCreated' => [
+				'type' => 'date',
+				'label' => 'Date Created',
+				'transformer' => 'date',
+			],
+
+			'datePublished' => [
+				'type' => 'date',
+				'label' => 'Date Published',
+				'transformer' => 'date',
+			],
+
+			'dateModified' => [
+				'type' => 'date',
+				'label' => 'Date Modified',
+				'transformer' => 'date',
+			],
+
+			/**************************************************************
+			 VISUAL ARTWORK FIELDS
+			**************************************************************/
+			'artform' => [
+				'type' => 'text',
+				'label' => 'Art Form',
+				'description' => 'e.g., Painting, Sculpture, Tattoo',
+				'transformer' => 'text',
+			],
+
+			'artMedium' => [
+				'type' => 'text',
+				'label' => 'Art Medium',
+				'description' => 'e.g., Oil, Watercolor, Ink',
+				'transformer' => 'text',
+			],
+
+			'artworkSurface' => [
+				'type' => 'text',
+				'label' => 'Artwork Surface',
+				'description' => 'e.g., Canvas, Paper, Skin',
+				'transformer' => 'text',
+			],
+
+			'width' => [
+				'type' => 'text',
+				'label' => 'Width',
+				'description' => 'Width with unit (e.g., 10cm, 5in)',
+				'transformer' => 'dimension',
+			],
+
+			'height' => [
+				'type' => 'text',
+				'label' => 'Height',
+				'description' => 'Height with unit (e.g., 15cm, 8in)',
+				'transformer' => 'dimension',
+			],
+
+			/**************************************************************
+			 EVENT FIELDS
+			**************************************************************/
+			'startDate' => [
+				'type' => 'datetime',
+				'label' => 'Start Date/Time',
+				'transformer' => 'datetime',
+			],
+
+			'endDate' => [
+				'type' => 'datetime',
+				'label' => 'End Date/Time',
+				'transformer' => 'datetime',
+			],
+
+			'eventStatus' => [
+				'type' => 'select',
+				'label' => 'Event Status',
+				'options' => [
+					'https://schema.org/EventScheduled' => 'Scheduled',
+					'https://schema.org/EventCancelled' => 'Cancelled',
+					'https://schema.org/EventPostponed' => 'Postponed',
+					'https://schema.org/EventRescheduled' => 'Rescheduled',
+				],
+				'transformer' => 'text',
+			],
+
+			'eventAttendanceMode' => [
+				'type' => 'select',
+				'label' => 'Attendance Mode',
+				'options' => [
+					'https://schema.org/OfflineEventAttendanceMode' => 'In-Person',
+					'https://schema.org/OnlineEventAttendanceMode' => 'Online',
+					'https://schema.org/MixedEventAttendanceMode' => 'Mixed/Hybrid',
+				],
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			 PRODUCT FIELDS
+			**************************************************************/
+			'brand' => [
+				'type' => 'group',
+				'label' => 'Brand',
+				'transformer' => 'brand_object',
+				'fields' => [
+					'type' => [
+						'type' => 'select',
+						'label' => 'Brand Type',
+						'options' => [
+							'text' => 'Text Only',
+							'organization' => 'Organization/Brand',
+						]
+					],
+					'name' => [
+						'type' => 'text',
+						'label' => 'Brand Name',
+					],
+					'url' => [
+						'type' => 'url',
+						'label' => 'Brand Website',
+						'condition' => [
+							'field' => 'type',
+							'value' => 'organization'
+						]
+					],
+					'logo' => [
+						'type' => 'upload',
+						'label' => 'Brand Logo',
+						'condition' => [
+							'field' => 'type',
+							'value' => 'organization'
+						]
+					],
+				]
+			],
+
+			'sku' => [
+				'type' => 'text',
+				'label' => 'SKU',
+				'description' => 'Stock Keeping Unit',
+				'transformer' => 'text',
+			],
+
+			'gtin' => [
+				'type' => 'text',
+				'label' => 'GTIN',
+				'description' => 'Global Trade Item Number',
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			 SERVICES & OFFERS
+			**************************************************************/
+			'hasOfferCatalog' => [
+				'type' => 'group',
+				'label' => 'Offer Catalog',
+				'transformer' => 'offer_catalog_from_posts',
+				'fields' => [
+					'source' => [
+						'type' => 'select',
+						'label' => 'Source',
+						'options' => [
+							'auto' => 'Auto from post type',
+							'manual' => 'Manual entry',
+						]
+					],
+					'post_type' => [
+						'type' => 'select',
+						'label' => 'Post Type',
+						'options' => $this->getContentPostTypes(),
+						'condition' => [
+							'field' => 'source',
+							'value' => 'auto'
+						]
+					],
+					'group_by_taxonomy' => [
+						'type' => 'true_false',
+						'label' => 'Group by category/taxonomy',
+						'condition' => [
+							'field' => 'source',
+							'value' => 'auto'
+						]
+					],
+					'taxonomy' => [
+						'type' => 'select',
+						'label' => 'Taxonomy',
+						'options' => $this->getContentTaxonomies(),
+						'condition' => [
+							'field' => 'group_by_taxonomy',
+							'value' => '1' // or '1' depending on how checkbox stores
+						]
+					]
+				]
+			],
+
+			'knowsAbout' => [
+				'type' => 'repeater',
+				'label' => 'Areas of Expertise',
+				'description' => 'Skills and specialties',
+				'transformer' => 'text_array',
+				'fields' => [
+					'topic' => [
+						'type' => 'text',
+						'label' => 'Topic',
+					]
+				]
+			],
+
+			/**************************************************************
+			 CREDENTIALS & CERTIFICATIONS
+			**************************************************************/
+			'hasCredential' => [
+				'type' => 'repeater',
+				'label' => 'Credentials / Certifications',
+				'description' => 'Professional certifications',
+				'transformer' => 'credential_array',
+				'fields' => [
+					'credentialCategory' => [
+						'type' => 'text',
+						'label' => 'Category',
+					],
+					'name' => [
+						'type' => 'text',
+						'label' => 'Name',
+					],
+					'issuedBy' => [
+						'type' => 'text',
+						'label' => 'Issued By',
+					]
+				]
+			],
+
+			'award' => [
+				'type' => 'repeater',
+				'label' => 'Awards & Recognition',
+				'transformer' => 'text_array',
+				'fields' => [
+					'award' => ['type' => 'text', 'label' => 'Award'],
+				]
+			],
+			'serviceArea' => [
+				'type' => 'repeater',
+				'label' => 'Service Areas',
+				'description' => 'Geographic areas served (cities, neighborhoods, or radius)',
+				'transformer' => 'service_area_array',
+				'fields' => [
+					'name' => ['type' => 'text', 'label' => 'Area Name'],
+					'type' => [
+						'type' => 'select',
+						'label' => 'Type',
+						'options' => [
+							'City' => 'City',
+							'AdministrativeArea' => 'Region/Province',
+							'GeoCircle' => 'Radius',
+						]
+					],
+					'radius' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Radius (km)'],
+				]
+			],
+
+			// Specialties
+			'makesOffer' => [
+				'type' => 'group',
+				'label' => 'Featured Offerings',
+				'transformer' => 'offers_from_posts',
+				'fields' => [
+					'source' => [
+						'type' => 'select',
+						'label' => 'Source',
+						'options' => [
+							'auto' => 'Auto from post type',
+							'manual' => 'Manual entry',
+						]
+					],
+					'post_type' => [
+						'type' => 'select',
+						'label' => 'Post Type',
+						'options' => $this->getContentPostTypes(),
+						'condition' => [
+							'field' => 'source',
+							'value' => 'auto'
+						]
+					],
+					'limit' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Featured Count',
+						'default' => 5,
+						'condition' => [
+							'field' => 'source',
+							'value' => 'auto'
+						]
+					],
+					'manual_items' => [
+						'type' => 'repeater',
+						'label' => 'Manual Offers',
+						'condition' => [
+							'field' => 'source',
+							'value' => 'manual'
+						],
+						'fields' => [
+							'name' => ['type' => 'text', 'label' => 'Offer Name'],
+							'description' => ['type' => 'textarea', 'label' => 'Description'],
+							'price' => ['type' => 'text', 'label' => 'Price/Range'],
+						]
+					]
+				]
+			],
+
+			'hasMenu' => [
+				'type' => 'group',
+				'label' => 'Menu Items',
+				'description' => 'Auto-populate from post type or enter manually',
+				'transformer' => 'menu_from_posts',
+				'fields' => [
+					'source' => [
+						'type' => 'select',
+						'label' => 'Source',
+						'options' => [
+							'auto' => 'Auto from post type',
+							'manual' => 'Manual entry',
+						],
+					],
+					'post_type' => [
+						'type' => 'select',
+						'label' => 'Post Type',
+						'options' => $this->getContentPostTypes(), // Dynamic callback
+						'condition' => [
+							'field' => 'source',
+							'value' => 'auto',
+							'operator' => '=='
+						]
+					],
+					'limit' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Number of items',
+						'default' => 10,
+						'condition' => [
+							'field' => 'source',
+							'value' => 'auto'
+						]
+					],
+					'orderby' => [
+						'type' => 'select',
+						'label' => 'Order By',
+						'options' => [
+							'menu_order' => 'Menu Order',
+							'title' => 'Title',
+							'date' => 'Date',
+						],
+						'condition' => [
+							'field' => 'source',
+							'value' => 'auto'
+						]
+					],
+					'manual_items' => [
+						'type' => 'repeater',
+						'label' => 'Manual Items',
+						'condition' => [
+							'field' => 'source',
+							'value' => 'manual'
+						],
+						'fields' => [
+							'name' => ['type' => 'text', 'label' => 'Item Name'],
+							'description' => ['type' => 'textarea', 'label' => 'Description'],
+							'price' => ['type' => 'text', 'label' => 'Price'],
+						]
+					]
+				]
+			],
+
+			/**************************************************************
+			 FAQ FIELDS
+			**************************************************************/
+			'mainEntity' => [
+				'type' => 'repeater',
+				'label' => 'FAQ Items',
+				'description' => 'Question and Answer pairs',
+				'transformer' => 'faq_array',
+				'fields' => [
+					'question' => [
+						'type' => 'text',
+						'label' => 'Question',
+					],
+					'answer' => [
+						'type' => 'text',
+						'label' => 'Answer',
+					]
+				]
+			],
+			/**************************************************************
+			 FOOD & CUISINE
+			**************************************************************/
+			'servesCuisine' => [
+				'type' => 'repeater',
+				'label' => 'Cuisine Types',
+				'description' => 'Types of cuisine served',
+				'transformer' => 'text_array',
+				'fields' => [
+					'cuisine' => [
+						'type' => 'text',
+						'label' => 'Cuisine Type',
+						'description' => 'e.g., Italian, Mexican, Vegan'
+					]
+				]
+			],
+
+			'menu' => [
+				'type' => 'url',
+				'label' => 'Menu URL',
+				'description' => 'Link to online menu',
+				'transformer' => 'url',
+			],
+
+			/**************************************************************
+			 PRODUCT/OFFER FIELDS
+			**************************************************************/
+			'offers' => [
+				'type' => 'group',
+				'label' => 'Offer Details',
+				'description' => 'Price and availability information',
+				'transformer' => 'offer_object',
+				'fields' => [
+					'price' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Price',
+					],
+					'priceCurrency' => [
+						'type' => 'text',
+						'label' => 'Currency',
+						'default' => 'USD',
+					],
+					'availability' => [
+						'type' => 'select',
+						'label' => 'Availability',
+						'options' => [
+							'InStock' => 'In Stock',
+							'PreOrder' => 'Pre-Order',
+							'SoldOut' => 'Sold Out',
+							'OutOfStock' => 'Out of Stock',
+							'Discontinued' => 'Discontinued',
+						]
+					],
+					'validFrom' => [
+						'type' => 'date',
+						'label' => 'Valid From',
+					],
+					'validThrough' => [
+						'type' => 'date',
+						'label' => 'Valid Through',
+					],
+				]
+			],
+
+			'mpn' => [
+				'type' => 'text',
+				'label' => 'Manufacturer Part Number',
+				'transformer' => 'text',
+			],
+
+			/**************************************************************
+			 BUSINESS POLICIES & FEATURES
+			**************************************************************/
+			'isAccessibleForFree' => [
+				'type' => 'true_false',
+				'label' => 'Accessible For Free',
+				'description' => 'Is this service/location accessible without payment?',
+				'transformer' => 'boolean',
+			],
+
+			'smokingAllowed' => [
+				'type' => 'true_false',
+				'label' => 'Smoking Allowed',
+				'transformer' => 'boolean',
+			],
+
+			'petsAllowed' => [
+				'type' => 'select',
+				'label' => 'Pets Allowed',
+				'options' => [
+					'' => 'Not specified',
+					'yes' => 'Yes',
+					'no' => 'No',
+				],
+				'transformer' => 'boolean',
+			],
+
+			/**************************************************************
+			 ORGANIZATION RELATIONSHIPS
+			**************************************************************/
+			'parentOrganization' => [
+				'type' => 'group',
+				'label' => 'Parent Organization',
+				'description' => 'Organization this is a part of',
+				'transformer' => 'organization_reference',
+				'fields' => [
+					'name' => ['type' => 'text', 'label' => 'Organization Name'],
+					'url' => ['type' => 'url', 'label' => 'Website'],
+				]
+			],
+
+			'subOrganization' => [
+				'type' => 'repeater',
+				'label' => 'Sub-Organizations',
+				'description' => 'Child organizations or departments',
+				'transformer' => 'organization_reference_array',
+				'fields' => [
+					'name' => ['type' => 'text', 'label' => 'Organization Name'],
+					'url' => ['type' => 'url', 'label' => 'Website'],
+				]
+			],
+
+			'employee' => [
+				'type' => 'repeater',
+				'label' => 'Employees',
+				'transformer' => 'person_reference_array',
+				'fields' => [
+					'name' => ['type' => 'text', 'label' => 'Name'],
+					'jobTitle' => ['type' => 'text', 'label' => 'Job Title'],
+				]
+			],
+
+			/**************************************************************
+			 HOSPITALITY (for hotels, etc.)
+			**************************************************************/
+			'checkinTime' => [
+				'type' => 'time',
+				'label' => 'Check-in Time',
+				'transformer' => 'time',
+			],
+
+			'checkoutTime' => [
+				'type' => 'time',
+				'label' => 'Check-out Time',
+				'transformer' => 'time',
+			],
+
+			'starRating' => [
+				'type' => 'group',
+				'label' => 'Star Rating',
+				'transformer' => 'rating_object',
+				'fields' => [
+					'ratingValue' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Rating',
+						'min' => 1,
+						'max' => 5,
+					],
+				]
+			],
+
+			/**************************************************************
+			 REVIEW & RATING
+			**************************************************************/
+			'review' => [
+				'type' => 'repeater',
+				'label' => 'Reviews',
+				'transformer' => 'review_array',
+				'fields' => [
+					'author' => ['type' => 'text', 'label' => 'Reviewer Name'],
+					'reviewRating' => [
+						'type' => 'text',
+						'subtype' => 'number',
+						'label' => 'Rating',
+						'min' => 1,
+						'max' => 5,
+					],
+					'reviewBody' => ['type' => 'textarea', 'label' => 'Review Text'],
+					'datePublished' => ['type' => 'date', 'label' => 'Date'],
+				]
+			],
+
+			/**************************************************************
+			 HEALTH & MEDICAL
+			**************************************************************/
+			'medicalSpecialty' => [
+				'type' => 'repeater',
+				'label' => 'Medical Specialties',
+				'transformer' => 'text_array',
+				'fields' => [
+					'specialty' => ['type' => 'text', 'label' => 'Specialty']
+				]
+			],
+
+			'healthcareService' => [
+				'type' => 'repeater',
+				'label' => 'Healthcare Services',
+				'transformer' => 'text_array',
+				'fields' => [
+					'service' => ['type' => 'text', 'label' => 'Service']
+				]
+			],
+		];
+	}
+
+	/**
+	 * Register all type definitions
+	 * Each type lists the fields it uses
+	 */
+	private function registerTypeDefinitions(): void
+	{
+		$this->typeDefinitions = [
+			/**************************************************************
+			 GENERAL / SITE-WIDE
+			**************************************************************/
+			'WebSite' => [
+				'label' => 'Website',
+				'group' => 'general',
+				'fields' => [
+					'name',
+					'description',
+					'url',
+					'inLanguage',
+					'potentialAction',
+					'hasPart',
+					'creator',
+				],
+			],
+
+			/**************************************************************
+			 PAGE TYPES
+			**************************************************************/
+			'WebPage' => [
+				'label' => 'Web Page',
+				'group' => 'page',
+				'fields' => [
+					'name',
+					'description',
+					'url',
+					'image',
+					'datePublished',
+					'dateModified',
+					'author',
+				],
+			],
+
+			'CollectionPage' => [
+				'label' => 'Collection Page',
+				'group' => 'page',
+				'extends' => 'WebPage',
+			],
+
+			'FAQPage' => [
+				'label' => 'FAQ Page',
+				'group' => 'page',
+				'extends' => 'WebPage',
+				'fields' => [
+					'mainEntity', // FAQ items
+				],
+			],
+
+			/**************************************************************
+			 ORGANIZATION & BUSINESS
+			**************************************************************/
+			'Organization' => [
+				'label' => 'Organization',
+				'group' => 'business',
+				'fields' => [
+					'name',
+					'legalName',
+					'alternateName',
+					'description',
+					'url',
+					'logo',
+					'image',
+					'email',
+					'telephone',
+					'sameAs',
+					'founders',
+					'foundingDate',
+					'numberOfEmployees',
+					'taxID',
+					'vatID',
+					'duns',
+					'slogan',
+					'disambiguatingDescription',
+				],
+			],
+
+
+
+			'LocalBusiness' => [
+				'label' => 'Local Business',
+				'group' => 'business',
+				'extends' => 'Organization',
+				'fields' => [
+					'location',
+					'openingHours',
+					'priceRange',
+					'currenciesAccepted',
+					'paymentAccepted',
+					'serviceArea',
+					'areaServed',
+					'hasMap',
+					'amenityFeature',
+					'availableLanguage',
+					'hasOfferCatalog',
+					'makesOffer',
+					'hasMenu',
+					'knowsAbout',
+					'hasCredential',
+					'aggregateRating',
+					'award',
+				],
+			],
+
+			'TattooParlor' => [
+				'label' => 'Tattoo Parlor',
+				'group' => 'business',
+				'extends' => 'LocalBusiness',
+				'fields' => [
+					'makesOffer',           // Tattoo styles/services
+					'hasOfferCatalog',      // Portfolio as catalog
+					'award',
+				],
+			],
+
+			'HealthBusiness' => [
+				'label' => 'Health Business',
+				'group' => 'business',
+				'extends' => 'LocalBusiness',
+				'description' => 'Healthcare providers',
+			],
+
+			'FoodEstablishment' => [
+				'label' => 'Food Establishment',
+				'group' => 'business',
+				'extends' => 'LocalBusiness',
+				'fields' => [
+					'hasMenu',
+					'servesCuisine',
+				],
+			],
+			'FoodTruck' => [
+				'label' => 'Food Truck',
+				'group' => 'business',
+				'extends' => 'FoodEstablishment',
+				'fields' => [
+					'serviceArea',
+				],
+			],
+
+			'Store' => [
+				'label' => 'Store / Shop',
+				'group' => 'business',
+				'extends' => 'LocalBusiness',
+				'fields' => [
+					'hasOfferCatalog',
+					'makesOffer',
+				],
+			],
+
+			'ProfessionalService' => [
+				'label' => 'Professional Service',
+				'group' => 'business',
+				'extends' => 'LocalBusiness',
+				'fields' => [
+					'serviceArea',          // Where they operate
+					'makesOffer',           // Services offered
+					'award',                // Professional recognition
+				],
+			],
+
+			/**************************************************************
+			 PERSON
+			**************************************************************/
+			'Person' => [
+				'label' => 'Person',
+				'group' => 'person',
+				'fields' => [
+					'name',
+					'givenName',
+					'familyName',
+					'honorificPrefix',
+					'honorificSuffix',
+					'alternateName',
+					'description',
+					'image',
+					'url',
+					'email',
+					'telephone',
+					'sameAs',
+					'jobTitle',
+					'knowsLanguage',
+					'birthDate',
+					'gender',
+				],
+			],
+
+			/**************************************************************
+			 CREATIVE WORKS
+			**************************************************************/
+			'CreativeWork' => [
+				'label' => 'Creative Work',
+				'group' => 'creative',
+				'fields' => [
+					'name',
+					'description',
+					'image',
+					'author',
+					'creator',
+					'dateCreated',
+					'datePublished',
+					'dateModified',
+					'keywords',
+				],
+			],
+
+			'DefinedTermSet'	=> [
+				'label'	=> 'Defined Term',
+				'group'	=> 'creative',
+				'extends'	=> 'CreativeWork',
+				'fields'	=> [
+					'DefinedTerm',
+				]
+			],
+
+			'BeforeAfter' => [
+				'label' => 'Before & After Case',
+				'group' => 'creative',
+				'extends' => 'CreativeWork',
+				'fields' => [
+					'about',              // Service (Laser Tattoo Removal)
+					'temporalCoverage',   // Treatment period
+					'hasPart',            // Individual images (as references)
+					'associatedMedia',    // Alternative to hasPart
+					'additionalProperty', // Sessions, treatment area
+				],
+			],
+
+			'VisualArtwork' => [
+				'label' => 'Visual Artwork',
+				'group' => 'creative',
+				'extends' => 'CreativeWork',
+				'fields' => [
+					'artform',
+					'artMedium',
+					'artworkSurface',
+					'width',
+					'height',
+				],
+			],
+
+			'Tattoo' => [
+				'label' => 'Tattoo',
+				'group' => 'creative',
+				'extends' => 'VisualArtwork',
+				'description' => 'A tattoo artwork (custom extension)',
+			],
+
+			'Product' => [
+				'label' => 'Product',
+				'group' => 'creative',
+				'fields' => [
+					'name',
+					'description',
+					'image',
+					'brand',
+					'sku',
+					'gtin',
+					'offers',               // Price, availability
+					'aggregateRating',      // Reviews
+					'award',                // Product awards
+				],
+			],
+
+			/**************************************************************
+			 EVENTS
+			**************************************************************/
+			'Event' => [
+				'label' => 'Event',
+				'group' => 'event',
+				'fields' => [
+					'name',
+					'description',
+					'image',
+					'startDate',
+					'endDate',
+					'location',
+					'eventStatus',
+					'eventAttendanceMode',
+				],
+			],
+		];
+	}
+
+	/**
+	 * Register type groups for UI organization
+	 */
+	private function registerTypeGroups(): void
+	{
+		$this->typeGroups = [
+			'general' => 'General',
+			'page' => 'Page Types',
+			'business' => 'Business & Organization',
+			'person' => 'People',
+			'creative' => 'Creative Works',
+			'event' => 'Events',
+		];
+	}
+
+	/**
+	 * Register a custom field definition
+	 */
+	public function registerField(string $fieldName, array $config): void
+	{
+		$this->fieldDefinitions[$fieldName] = $config;
+	}
+
+	/**
+	 * Register a custom type definition
+	 */
+	public function registerType(string $typeName, array $config): void
+	{
+		$this->typeDefinitions[$typeName] = $config;
+	}
+
+	/**
+	 * Register a type group
+	 */
+	public function registerGroup(string $key, string $label): void
+	{
+		$this->typeGroups[$key] = $label;
+	}
+
+	/**
+	 * Get post types for select options
+	 */
+	public static function getContentPostTypes(): array
+	{
+		$options = ['' => '-- Select Post Type --'];
+
+		if (defined('JVB_CONTENT')) {
+			foreach (JVB_CONTENT as $key => $config) {
+				$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
+			}
+		}
+
+		return $options;
+	}
+
+	/**
+	 * Get taxonomies for select options
+	 */
+	public static function getContentTaxonomies(): array
+	{
+		$options = ['' => '-- Select Taxonomy --'];
+
+		if (defined('JVB_TAXONOMY')) {
+			foreach (JVB_TAXONOMY as $key => $config) {
+				$options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
+			}
+		}
+
+		return $options;
+	}
+}
diff --git a/inc/managers/SEO/TemplateResolver.php b/inc/managers/SEO/TemplateResolver.php
new file mode 100644
index 0000000..1090f9c
--- /dev/null
+++ b/inc/managers/SEO/TemplateResolver.php
@@ -0,0 +1,663 @@
+<?php
+namespace JVBase\managers\SEO;
+
+use JVBase\meta\MetaManager;
+use WP_Post;
+use WP_Term;
+use WP_User;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Resolves template variables like {{post_title}} and {{author.name}}
+ *
+ * Supports:
+ * - Direct field access: {{post_title}}, {{bio}}
+ * - Relation access: {{author.name}}, {{shop.location}}
+ * - Site variables: {{site_name}}, {{site_url}}
+ * - Special accessors: {{featured_image_url}}, {{permalink}}
+ * - Auto-enhancement via SchemaFieldHelpers
+ */
+class TemplateResolver
+{
+	private ?int $objectId = null;
+	private ?string $objectType = null;
+	private ?string $contentType = null;
+	private ?MetaManager $meta = null;
+	private array $context = [];
+	private array $fieldDefinitions = [];
+
+	/**
+	 * Create resolver for a specific object
+	 */
+	public function __construct(?int $objectId = null, ?string $objectType = null, ?string $contentType = null)
+	{
+		$this->objectId = $objectId;
+		$this->objectType = $objectType;
+		$this->contentType = $contentType;
+
+		if ($objectId && $objectType) {
+			$this->meta = new MetaManager($objectId, $objectType, $contentType);
+			$this->loadFieldDefinitions();
+		}
+
+		$this->buildContext();
+	}
+
+	/**
+	 * Create resolver for current queried object
+	 */
+	public static function forCurrentObject(): self
+	{
+		if (is_singular()) {
+			$post = get_post();
+			if ($post) {
+				return new self($post->ID, 'post', $post->post_type);
+			}
+		} elseif (is_tax() || is_category() || is_tag()) {
+			$term = get_queried_object();
+			if ($term instanceof WP_Term) {
+				return new self($term->term_id, 'term', $term->taxonomy);
+			}
+		} elseif (is_author()) {
+			$author = get_queried_object();
+			if ($author instanceof WP_User) {
+				return new self($author->ID, 'user', jvbUserRole($author->ID));
+			}
+		} elseif (is_post_type_archive()) {
+			// Get the post type being archived
+			$postType = get_query_var('post_type');
+			if (is_array($postType)) {
+				$postType = reset($postType);
+			}
+
+			// Create resolver with archive context (no objectId needed)
+			return new self(null, 'archive', $postType);
+		}
+
+		// Fallback for pages without specific objects
+		return new self();
+	}
+
+	/**
+	 * Resolve a template string
+	 *
+	 * @param string $template Template with {{variables}}
+	 * @return string Resolved string
+	 */
+	public function resolve(string $template): string
+	{
+		return preg_replace_callback(
+			'/\{\{([^}]+)\}\}/',
+			fn($matches) => $this->resolveVariable($matches[1]),
+			$template
+		);
+	}
+
+	/**
+	 * Resolve a single variable
+	 */
+	public function resolveVariable(string $variable): mixed
+	{
+		$variable = trim($variable);
+
+		$custom = apply_filters(
+			'jvbSEOResolveVariable',
+			null,
+			$variable,
+			$this->objectId,
+			$this->objectType,
+			$this->contentType,
+			$this->meta
+		);
+
+		if ($custom !== null) {
+			return $this->formatValue($custom, $variable);
+		}
+
+		// Check for dot notation (relation access)
+		if (str_contains($variable, '.')) {
+			return $this->resolveRelation($variable);
+		}
+
+		// Check context first (site variables, etc.)
+		if (isset($this->context[$variable])) {
+			return $this->context[$variable];
+		}
+
+		// Check special accessors
+		$special = $this->resolveSpecial($variable);
+		if ($special !== null) {
+			return $special;
+		}
+
+		// Try to get from MetaManager
+		if ($this->meta) {
+			$value = $this->meta->getValue($variable);
+
+			// Auto-resolve complex field types via SchemaFieldHelpers
+			$value = $this->autoResolveField($variable, $value);
+
+			return $this->formatValue($value, $variable);
+		}
+
+		// Return empty if not found
+		return '';
+	}
+
+	/**
+	 * Auto-resolve field via SchemaFieldHelpers (DELEGATED)
+	 *
+	 * This is the main integration point - all enhancement logic
+	 * is now handled by SchemaFieldHelpers.autoResolve()
+	 */
+	private function autoResolveField(string $fieldName, mixed $value): mixed
+	{
+		if ($value === null || $value === '') {
+			return $value;
+		}
+
+		// Check if this is a relational field that needs a schema reference
+		$fieldDef = $this->fieldDefinitions[$fieldName] ?? null;
+		if ($fieldDef && $this->isRelationalField($fieldDef) && is_numeric($value)) {
+			$objectType = $this->mapFieldTypeToObjectType($fieldDef['type']);
+			return SchemaReferenceBuilder::build($objectType, (int)$value);
+		}
+
+		// Check if this is a term asking for related posts (e.g., shop → artists)
+		if ($this->objectType === 'term' && $this->isRelatedPostsField($fieldName)) {
+			return $this->buildRelatedPostsReferences($fieldName);
+		}
+
+		// Check if this is a post asking for related terms (e.g., artist → styles)
+		if ($this->objectType === 'post' && $this->isRelatedTermsField($fieldName)) {
+			return $this->buildRelatedTermsReferences($fieldName);
+		}
+
+		// Delegate to SchemaFieldHelpers for all other enhancement
+		return SchemaFieldHelpers::autoResolve($fieldName, $value, $this->meta);
+	}
+
+	/**
+	 * Check if field name indicates related posts (plural form of post type)
+	 *
+	 * Examples: "artists", "artworks", "partners"
+	 */
+	private function isRelatedPostsField(string $fieldName): bool
+	{
+		if (!defined('JVB_CONTENT')) {
+			return false;
+		}
+
+		// Check if field name matches any plural content type
+		foreach (JVB_CONTENT as $type => $config) {
+			$plural = strtolower($config['plural'] ?? '');
+			if ($plural && $fieldName === $plural) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Check if field name indicates related terms (plural form of taxonomy)
+	 *
+	 * Examples: "styles", "themes", "shops"
+	 */
+	private function isRelatedTermsField(string $fieldName): bool
+	{
+		if (!defined('JVB_TAXONOMY')) {
+			return false;
+		}
+
+		// Check if field name matches any plural taxonomy
+		foreach (JVB_TAXONOMY as $taxonomy => $config) {
+			$plural = strtolower($config['plural'] ?? '');
+			if ($plural && $fieldName === $plural) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Build references for posts related to current term
+	 */
+	private function buildRelatedPostsReferences(string $fieldName): array
+	{
+		if ($this->objectType !== 'term' || !$this->objectId) {
+			return [];
+		}
+
+		// Find the post type from the field name
+		$postType = $this->getPostTypeFromPluralName($fieldName);
+		if (!$postType) {
+			return [];
+		}
+
+		// Build references (default: 10 items, minimal context)
+		return SchemaReferenceBuilder::buildFromTerm(
+			$this->objectId,
+			$postType,
+			limit: 10,
+			includeContext: true
+		);
+	}
+
+	/**
+	 * Build references for terms related to current post
+	 */
+	private function buildRelatedTermsReferences(string $fieldName): array
+	{
+		if ($this->objectType !== 'post' || !$this->objectId) {
+			return [];
+		}
+
+		// Find the taxonomy from the field name
+		$taxonomy = $this->getTaxonomyFromPluralName($fieldName);
+		if (!$taxonomy) {
+			return [];
+		}
+
+		// Build references (default: 10 items, minimal context)
+		return SchemaReferenceBuilder::buildFromPost(
+			$this->objectId,
+			$taxonomy,
+			limit: 10,
+			includeContext: false  // Terms usually don't need context
+		);
+	}
+
+	/**
+	 * Get post type key from plural name
+	 */
+	private function getPostTypeFromPluralName(string $pluralName): ?string
+	{
+		if (!defined('JVB_CONTENT')) {
+			return null;
+		}
+
+		foreach (JVB_CONTENT as $type => $config) {
+			$plural = strtolower($config['plural'] ?? '');
+			if ($plural === $pluralName) {
+				return $type;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get taxonomy key from plural name
+	 */
+	private function getTaxonomyFromPluralName(string $pluralName): ?string
+	{
+		if (!defined('JVB_TAXONOMY')) {
+			return null;
+		}
+
+		foreach (JVB_TAXONOMY as $taxonomy => $config) {
+			$plural = strtolower($config['plural'] ?? '');
+			if ($plural === $pluralName) {
+				return $taxonomy;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Check if field is relational (references another entity)
+	 */
+	private function isRelationalField(array $fieldDef): bool
+	{
+		return in_array($fieldDef['type'] ?? '', ['post', 'post_object', 'taxonomy', 'user']);
+	}
+
+	/**
+	 * Map field type to object type for SchemaReferenceBuilder
+	 */
+	private function mapFieldTypeToObjectType(string $fieldType): string
+	{
+		return match($fieldType) {
+			'post', 'post_object' => 'post',
+			'taxonomy' => 'term',
+			'user' => 'user',
+			default => 'post'
+		};
+	}
+
+	/**
+	 * Resolve dot notation like {{author.name}} or {{shop.location.address}}
+	 */
+	private function resolveRelation(string $path): string
+	{
+		$parts = explode('.', $path);
+		$relation = array_shift($parts);
+		$field = implode('.', $parts);
+
+		// Get the related object
+		$related = $this->getRelatedObject($relation);
+		if (!$related) {
+			return '';
+		}
+
+		// Create a resolver for the related object and resolve the field
+		$relatedResolver = $this->createRelatedResolver($related);
+		if (!$relatedResolver) {
+			return '';
+		}
+
+		return $relatedResolver->resolveVariable($field);
+	}
+
+	/**
+	 * Get a related object by relation name
+	 */
+	private function getRelatedObject(string $relation): mixed
+	{
+		if (!$this->meta) {
+			return null;
+		}
+
+		// Common relations
+		switch ($relation) {
+			case 'author':
+				if ($this->objectType === 'post') {
+					$post = get_post($this->objectId);
+					return $post ? get_user_by('id', $post->post_author) : null;
+				}
+				break;
+
+			case 'featured_image':
+				if ($this->objectType === 'post') {
+					$imageId = get_post_thumbnail_id($this->objectId);
+					return $imageId ? get_post($imageId) : null;
+				}
+				break;
+		}
+
+		// Check field definitions for taxonomy or post relations
+		if (isset($this->fieldDefinitions[$relation])) {
+			$fieldDef = $this->fieldDefinitions[$relation];
+			$value = $this->meta->getValue($relation);
+
+			if (!$value) {
+				return null;
+			}
+
+			switch ($fieldDef['type'] ?? '') {
+				case 'taxonomy':
+					// Get first term from taxonomy
+					$taxonomy = $fieldDef['taxonomy'] ?? $relation;
+					if ($this->objectType === 'post') {
+						$terms = wp_get_post_terms($this->objectId, jvbCheckBase($taxonomy));
+						return !empty($terms) && !is_wp_error($terms) ? $terms[0] : null;
+					}
+					return is_numeric($value) ? get_term($value) : null;
+
+				case 'post':
+				case 'post_object':
+					return get_post($value);
+
+				case 'user':
+					return get_user_by('id', $value);
+			}
+		}
+
+		// Check if it's a taxonomy on the post
+		if ($this->objectType === 'post') {
+			$taxonomyName = jvbCheckBase($relation);
+			if (taxonomy_exists($taxonomyName)) {
+				$terms = wp_get_post_terms($this->objectId, $taxonomyName);
+				return !empty($terms) && !is_wp_error($terms) ? $terms[0] : null;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Create a resolver for a related object
+	 */
+	private function createRelatedResolver(mixed $object): ?self
+	{
+		if ($object instanceof WP_Post) {
+			return new self($object->ID, 'post', $object->post_type);
+		} elseif ($object instanceof WP_Term) {
+			return new self($object->term_id, 'term', $object->taxonomy);
+		} elseif ($object instanceof WP_User) {
+			return new self($object->ID, 'user', jvbUserRole($object->ID));
+		}
+
+		return null;
+	}
+
+	/**
+	 * Resolve special built-in variables
+	 */
+	private function resolveSpecial(string $variable): ?string
+	{
+		// Location component accessors
+		if (str_starts_with($variable, 'location_')) {
+			$component = substr($variable, 9);
+			return $this->resolveLocationComponent($component);
+		}
+
+		// Image URL accessors for different sizes
+		if (str_ends_with($variable, '_image_url')) {
+			$field = str_replace('_image_url', '', $variable);
+			$imageId = $this->meta?->getValue($field);
+			if ($imageId) {
+				return wp_get_attachment_image_url($imageId, 'full') ?: '';
+			}
+		}
+
+		switch ($variable) {
+			case 'permalink':
+			case 'url':
+				return $this->getObjectUrl();
+
+			case 'featured_image_url':
+			case 'thumbnail_url':
+				if ($this->objectType === 'post') {
+					$url = get_the_post_thumbnail_url($this->objectId, 'full');
+					return $url ?: '';
+				}
+				return '';
+
+			case 'post_date':
+			case 'date_published':
+				if ($this->objectType === 'post') {
+					return get_the_date('c', $this->objectId);
+				}
+				return '';
+
+			case 'post_modified':
+			case 'date_modified':
+				if ($this->objectType === 'post') {
+					return get_the_modified_date('c', $this->objectId);
+				}
+				return '';
+
+			case 'term_count':
+			case 'count':
+				if ($this->objectType === 'term') {
+					$term = get_term($this->objectId);
+					return $term ? (string)$term->count : '0';
+				}
+				return '';
+		}
+
+		return null;
+	}
+
+	/**
+	 * Resolve location component (e.g., location_address, location_city)
+	 */
+	private function resolveLocationComponent(string $component): string
+	{
+		$location = $this->meta?->getValue('location');
+
+		if (!is_array($location)) {
+			return '';
+		}
+
+		return (string)($location[$component] ?? '');
+	}
+
+	/**
+	 * Get URL for current object
+	 */
+	private function getObjectUrl(): string
+	{
+		return match($this->objectType) {
+			'post' => get_permalink($this->objectId) ?: '',
+			'term' => is_wp_error($link = get_term_link($this->objectId)) ? '' : $link,
+			'user' => get_author_posts_url($this->objectId) ?: '',
+			'archive' => $this->contentType ? (get_post_type_archive_link(jvbCheckBase($this->contentType)) ?: '') : '',
+			default => ''
+		};
+	}
+
+	/**
+	 * Format a value for output
+	 */
+	private function formatValue(mixed $value, string $field = ''): string
+	{
+		if (is_null($value) || $value === '') {
+			return '';
+		}
+
+		if (is_array($value)) {
+			return $this->formatArrayValue($value, $field);
+		}
+
+		if (is_bool($value)) {
+			return $value ? 'true' : 'false';
+		}
+
+		return (string)$value;
+	}
+
+	/**
+	 * Format array values
+	 */
+	private function formatArrayValue(array $value, string $field): string
+	{
+		// Check if it's a repeater with sub-fields
+		if (isset($value[0]) && is_array($value[0])) {
+			// Extract specific field if pattern indicates
+			$subField = $this->getArraySubField($field);
+			if ($subField) {
+				$extracted = array_column($value, $subField);
+				return implode(', ', array_filter($extracted));
+			}
+
+			// Default: try common field names
+			foreach (['name', 'title', 'url', 'value', 'keyword', 'language'] as $key) {
+				if (isset($value[0][$key])) {
+					return implode(', ', array_column($value, $key));
+				}
+			}
+		}
+
+		// Simple array
+		return implode(', ', array_filter($value));
+	}
+
+	/**
+	 * Check if field name indicates a sub-field extraction
+	 */
+	private function getArraySubField(string $field): ?string
+	{
+		// Pattern: field_name[sub_field]
+		if (preg_match('/\[(\w+)\]$/', $field, $matches)) {
+			return $matches[1];
+		}
+		return null;
+	}
+
+	/**
+	 * Build context with site-wide variables
+	 */
+	private function buildContext(): void
+	{
+		$this->context = [
+			'site_name'        => get_bloginfo('name'),
+			'site_description' => get_bloginfo('description'),
+			'site_url'         => get_home_url(),
+			'current_url'      => $this->getCurrentUrl(),
+			'current_year'     => date('Y'),
+			'current_date'     => date('Y-m-d'),
+		];
+
+		// Add object-specific context
+		if ($this->objectType === 'post' && $this->objectId) {
+			$post = get_post($this->objectId);
+			if ($post) {
+				$this->context['post_title'] = $post->post_title;
+				$this->context['post_excerpt'] = $post->post_excerpt ?: wp_trim_words($post->post_content, 20);
+				$this->context['post_type'] = $post->post_type;
+			}
+		} elseif ($this->objectType === 'term' && $this->objectId) {
+			$term = get_term($this->objectId);
+			if ($term && !is_wp_error($term)) {
+				$this->context['term_name'] = $term->name;
+				$this->context['term_description'] = $term->description;
+				$this->context['taxonomy'] = $term->taxonomy;
+			}
+		} elseif ($this->objectType === 'user' && $this->objectId) {
+			$user = get_userdata($this->objectId);
+			if ($user) {
+				$this->context['user_name'] = $user->display_name;
+				$this->context['user_login'] = $user->user_login;
+			}
+		} elseif ($this->objectType === 'archive' && $this->contentType) {
+			// Archive-specific context
+			$postType = jvbCheckBase($this->contentType);
+			$postTypeObject = get_post_type_object($postType);
+
+			if ($postTypeObject) {
+				$this->context['archive_title'] = $postTypeObject->labels->name ?? '';
+				$this->context['archive_description'] = $postTypeObject->description ?? '';
+				$this->context['archive_url'] = get_post_type_archive_link($postType) ?: '';
+				$this->context['post_type'] = $postType;
+				$this->context['post_type_label'] = $postTypeObject->labels->singular_name ?? '';
+			}
+		}
+	}
+
+	/**
+	 * Get current URL
+	 */
+	private function getCurrentUrl(): string
+	{
+		global $wp;
+		return home_url($wp->request);
+	}
+
+	/**
+	 * Load field definitions from content config
+	 */
+	private function loadFieldDefinitions(): void
+	{
+		if (!$this->contentType) {
+			return;
+		}
+
+		$typeKey = str_replace(BASE, '', $this->contentType);
+
+		if ($this->objectType === 'post' && defined('JVB_CONTENT')) {
+			$this->fieldDefinitions = JVB_CONTENT[$typeKey]['fields'] ?? [];
+		} elseif ($this->objectType === 'term' && defined('JVB_TAXONOMY')) {
+			$this->fieldDefinitions = JVB_TAXONOMY[$typeKey]['fields'] ?? [];
+		} elseif ($this->objectType === 'user' && defined('JVB_USER')) {
+			$this->fieldDefinitions = JVB_USER[$typeKey]['fields'] ?? [];
+		}
+	}
+}
diff --git a/inc/managers/SEO/TypeBuilder.php b/inc/managers/SEO/TypeBuilder.php
new file mode 100644
index 0000000..5ecd450
--- /dev/null
+++ b/inc/managers/SEO/TypeBuilder.php
@@ -0,0 +1,85 @@
+<?php
+namespace JVBase\managers\SEO;
+
+
+/**
+ * Type Builder - Fluent API for schema type definitions
+ */
+class TypeBuilder
+{
+	private SchemaBuilder $schema;
+	private string $name;
+	private array $definition = [
+		'fields' => [],
+	];
+
+	public function __construct(SchemaBuilder $schema, string $name)
+	{
+		$this->schema = $schema;
+		$this->name = $name;
+	}
+
+	public function label(string $label): self
+	{
+		$this->definition['label'] = $label;
+		return $this;
+	}
+
+	public function group(string $group): self
+	{
+		$this->definition['group'] = $group;
+		return $this;
+	}
+
+	public function extends(string $parentType): self
+	{
+		$this->definition['extends'] = $parentType;
+		return $this;
+	}
+
+	public function fields(array $fields): self
+	{
+		$this->definition['fields'] = $fields;
+		return $this;
+	}
+
+	public function addField(string $field): self
+	{
+		$this->definition['fields'][] = $field;
+		return $this;
+	}
+
+	public function addFields(array $fields): self
+	{
+		$this->definition['fields'] = array_merge($this->definition['fields'], $fields);
+		return $this;
+	}
+
+	/**
+	 * Override a specific field's definition for this type
+	 */
+	public function field(string $fieldName): FieldOverrideBuilder
+	{
+		return new FieldOverrideBuilder($this, $fieldName);
+	}
+
+	/**
+	 * Internal: Store field override
+	 */
+	public function setFieldOverride(string $fieldName, array $overrides): self
+	{
+		if (!isset($this->definition['fieldOverrides'])) {
+			$this->definition['fieldOverrides'] = [];
+		}
+		$this->definition['fieldOverrides'][$fieldName] = $overrides;
+		return $this;
+	}
+
+	/**
+	 * Finish building and register the type
+	 */
+	public function __destruct()
+	{
+		$this->schema->registerType($this->name, $this->definition);
+	}
+}
diff --git a/inc/managers/SEO/_edmonotonink.php b/inc/managers/SEO/_edmonotonink.php
new file mode 100644
index 0000000..c4a356a
--- /dev/null
+++ b/inc/managers/SEO/_edmonotonink.php
@@ -0,0 +1,537 @@
+	<?php
+
+/**
+ * Edmonton.ink Configuration
+ *
+ * Add this to your edmonton.ink child theme/plugin
+ * This replaces all the hardcoded logic in SchemaManager and SEOMetaManager
+ */
+
+// ==================================================
+// SITE-WIDE SCHEMA CONFIGURATION
+// ==================================================
+
+add_filter('jvb_schema', function ($schema) {
+	return array_merge($schema, [
+		'site_type' => 'directory',
+		'organization' => [
+			'type' => 'Organization',
+			'name' => 'edmonton.ink',
+			'url' => 'https://edmonton.ink',
+			'description' => 'Your tattoo scene on your screen. Discover Edmonton\'s best tattoo artists, shops, and styles.',
+			'publisher' => [
+				'type' => 'Organization',
+				'name' => 'Legacy Tattoo Removal',
+				'url' => 'https://legacytattooremoval.ca',
+				'logo' => 'https://legacytattooremoval.ca/wp-content/uploads/2024/09/legacy-tattoo-removal.webp',
+				'same_as' => [
+					'https://www.instagram.com/legacytattooremoval',
+					'https://www.facebook.com/legacytattooremoval',
+				]
+			]
+		],
+		'attribution' => [
+			'enabled' => true,
+		]
+	]);
+});
+
+// ==================================================
+// CONTENT TYPES CONFIGURATION
+// ==================================================
+
+add_filter('jvb_content', function ($content) {
+
+	// ARTIST
+	$content['artist'] = array_merge($content['artist'] ?? [], [
+		'seo' => [
+			'title_template' => '{{name}} | {{primary_style}} Tattoo Artist in {{city}}',
+			'description_template' => '{{name}} is a {{primary_style}} tattoo artist {{location_text}}. {{bio}} Browse portfolio and book appointments.',
+			'variables' => [
+				'name' => 'post_title',
+				'primary_style' => ['taxonomy' => BASE . 'style', 'primary' => true],
+				'styles' => ['taxonomy' => BASE . 'style'],
+				'city' => ['taxonomy' => BASE . 'city', 'primary' => true],
+				'primary_shop' => ['taxonomy' => BASE . 'shop', 'primary' => true],
+				'bio' => ['meta' => 'bio', 'truncate' => 100],
+				'location_text' => ['callback' => function ($post_id, $context) {
+					$city_terms = get_the_terms($post_id, BASE . 'city');
+					$shop_terms = get_the_terms($post_id, BASE . 'shop');
+
+					if ($shop_terms && !is_wp_error($shop_terms)) {
+						$shop = $shop_terms[0];
+						return "working at {$shop->name}";
+					} elseif ($city_terms && !is_wp_error($city_terms)) {
+						$city = $city_terms[0];
+						return "in {$city->name}";
+					}
+					return "in Edmonton";
+				}],
+			],
+			'archive_title' => 'Edmonton\'s Best Tattoo Artists | Browse by Style & Shop',
+			'archive_description' => 'Explore Edmonton\'s top tattoo artists. Filter by style, shop, or location. View portfolios and book your next tattoo today.'
+		],
+
+		'schema' => [
+			'type' => 'Person',
+			'additional_types' => ['Artist'],
+			'properties' => [
+				'jobTitle' => ['callback' => function ($id, $context, $meta) {
+					$job = $meta['job_title'] ?? null;
+					if ($job) return $job;
+
+					// Try to build from specialties
+					$styles = get_the_terms($id, BASE . 'style');
+					if ($styles && !is_wp_error($styles) && count($styles) > 0) {
+						$primary = $styles[0]->name;
+						return "{$primary} Tattoo Artist";
+					}
+					return "Tattoo Artist";
+				}],
+				'telephone' => 'phone',
+				'email' => 'email',
+				'url' => ['callback' => function ($id) {
+					return get_permalink($id);
+				}],
+				'image' => ['callback' => function ($id) {
+					return get_the_post_thumbnail_url($id, 'full');
+				}],
+				'knowsAbout' => ['taxonomy' => BASE . 'style'],
+				'worksFor' => ['callback' => function ($id, $context, $meta) {
+					$shops = get_the_terms($id, BASE . 'shop');
+					if (!$shops || is_wp_error($shops)) {
+						return null;
+					}
+
+					return array_map(function ($shop) {
+						return [
+							'@type' => 'LocalBusiness',
+							'@id' => get_term_link($shop) . '#business',
+							'name' => $shop->name,
+							'url' => get_term_link($shop)
+						];
+					}, $shops);
+				}],
+				'memberOf' => [
+					'@id' => 'https://edmonton.ink/#organization'
+				],
+			]
+		]
+	]);
+
+	// PARTNER (Shops/Organizations)
+	$content['partner'] = array_merge($content['partner'] ?? [], [
+		'seo' => [
+			'title_template' => '{{name}} | Community Partner',
+			'description_template' => '{{name}} - {{excerpt}}',
+			'variables' => [
+				'name' => 'post_title',
+				'excerpt' => ['callback' => function ($id) {
+					return get_the_excerpt($id);
+				}],
+			],
+			'archive_title' => 'Our Community Partners | Supporting Edmonton\'s Tattoo Scene',
+			'archive_description' => 'Meet the businesses and organizations supporting Edmonton\'s tattoo community. Our partners help make edmonton.ink possible.'
+		],
+
+		'schema' => [
+			'type' => 'Organization',
+			'properties' => [
+				'name' => ['callback' => function ($id) {
+					return get_the_title($id);
+				}],
+				'description' => ['callback' => function ($id) {
+					return get_the_excerpt($id);
+				}],
+				'url' => ['callback' => function ($id, $context, $meta) {
+					$website = $meta['website'] ?? null;
+					return $website ?: get_permalink($id);
+				}],
+				'logo' => ['callback' => function ($id, $context, $meta) {
+					$image_id = $meta['image'] ?? null;
+					if ($image_id) {
+						return wp_get_attachment_image_url($image_id, 'full');
+					}
+					return get_the_post_thumbnail_url($id, 'full');
+				}],
+				'telephone' => 'phone',
+				'email' => 'email',
+				'address' => 'address',
+				'sameAs' => ['callback' => function ($id, $context, $meta) {
+					$links = [];
+					if (!empty($meta['instagram'])) $links[] = $meta['instagram'];
+					if (!empty($meta['facebook'])) $links[] = $meta['facebook'];
+					if (!empty($meta['twitter'])) $links[] = $meta['twitter'];
+					return !empty($links) ? $links : null;
+				}],
+				'memberOf' => [
+					'@id' => 'https://edmonton.ink/#organization'
+				],
+			]
+		]
+	]);
+
+	// TATTOO
+	$content['tattoo'] = array_merge($content['tattoo'] ?? [], [
+		'seo' => [
+			'title_template' => '{{style}} Tattoo by {{artist}} | Edmonton',
+			'description_template' => 'Beautiful {{style}} tattoo by {{artist}}{{location}}. View more work from Edmonton\'s talented artists.',
+			'variables' => [
+				'style' => ['taxonomy' => BASE . 'style', 'primary' => true],
+				'artist' => ['callback' => function ($id) {
+					$artist_id = get_post_meta($id, BASE . 'link', true);
+					if ($artist_id) {
+						return get_the_title($artist_id);
+					}
+					return 'Edmonton Artist';
+				}],
+				'location' => ['callback' => function ($id) {
+					$artist_id = get_post_meta($id, BASE . 'link', true);
+					if (!$artist_id) return '';
+
+					$shops = get_the_terms($artist_id, BASE . 'shop');
+					if ($shops && !is_wp_error($shops)) {
+						return ' at ' . $shops[0]->name;
+					}
+					return '';
+				}],
+			],
+			'archive_title' => 'Edmonton Tattoos | Browse by Style, Artist & Shop',
+			'archive_description' => 'Browse tattoos from Edmonton\'s best artists. Filter by style, theme, or artist to find inspiration for your next piece.'
+		],
+
+		'schema' => [
+			'type' => 'CreativeWork',
+			'additional_types' => ['VisualArtwork'],
+			'properties' => [
+				'creator' => ['callback' => function ($id) {
+					$artist_id = get_post_meta($id, BASE . 'link', true);
+					if ($artist_id) {
+						return [
+							'@type' => 'Person',
+							'@id' => get_permalink($artist_id) . '#person',
+							'name' => get_the_title($artist_id),
+							'url' => get_permalink($artist_id)
+						];
+					}
+					return null;
+				}],
+				'image' => ['callback' => function ($id) {
+					return get_the_post_thumbnail_url($id, 'full');
+				}],
+				'about' => ['taxonomy' => BASE . 'theme'],
+				'artform' => 'Tattoo',
+			]
+		]
+	]);
+
+	// PIERCING
+	$content['piercing'] = array_merge($content['piercing'] ?? [], [
+		'seo' => [
+			'title_template' => '{{type}} Piercing by {{artist}} | Edmonton',
+			'description_template' => 'Professional {{type}} piercing by {{artist}}. View more piercing work from Edmonton\'s skilled professionals.',
+			'variables' => [
+				'type' => ['taxonomy' => BASE . 'pstyle', 'primary' => true],
+				'artist' => ['callback' => function ($id) {
+					$artist_id = get_post_meta($id, BASE . 'link', true);
+					return $artist_id ? get_the_title($artist_id) : 'Edmonton Professional';
+				}],
+			]
+		],
+
+		'schema' => [
+			'type' => 'CreativeWork',
+			'properties' => [
+				'creator' => ['callback' => function ($id) {
+					$artist_id = get_post_meta($id, BASE . 'link', true);
+					if ($artist_id) {
+						return [
+							'@type' => 'Person',
+							'@id' => get_permalink($artist_id) . '#person',
+							'name' => get_the_title($artist_id),
+							'url' => get_permalink($artist_id)
+						];
+					}
+					return null;
+				}],
+				'image' => ['callback' => function ($id) {
+					return get_the_post_thumbnail_url($id, 'full');
+				}],
+			]
+		]
+	]);
+
+	// EVENT
+	$content['event'] = array_merge($content['event'] ?? [], [
+		'seo' => [
+			'title_builder' => function ($post_id, $meta) {
+				$title = get_the_title($post_id);
+				$date = $meta->getValue('event_date');
+				if ($date) {
+					$formatted_date = date('F j, Y', strtotime($date));
+					return "{$title} | {$formatted_date}";
+				}
+				return "{$title} | Edmonton Tattoo Event";
+			},
+			'description_builder' => function ($post_id, $meta) {
+				$excerpt = get_the_excerpt($post_id);
+				$venue = $meta->getValue('venue');
+				$date = $meta->getValue('event_date');
+
+				$desc = $excerpt;
+				if ($venue) $desc .= " Location: {$venue}.";
+				if ($date) {
+					$formatted = date('F j, Y', strtotime($date));
+					$desc .= " Date: {$formatted}.";
+				}
+				return $desc;
+			}
+		],
+
+		'schema' => [
+			'custom_builder' => function ($post_id) {
+				$meta = new \JVBase\meta\MetaManager($post_id, 'post');
+
+				$schema = [
+					'@type' => 'Event',
+					'name' => get_the_title($post_id),
+					'url' => get_permalink($post_id),
+				];
+
+				$date = $meta->getValue('event_date');
+				if ($date) {
+					$schema['startDate'] = date('c', strtotime($date));
+				}
+
+				$venue = $meta->getValue('venue');
+				$venue_address = $meta->getValue('venue_address');
+				if ($venue) {
+					$schema['location'] = [
+						'@type' => 'Place',
+						'name' => $venue,
+					];
+					if ($venue_address) {
+						$schema['location']['address'] = $venue_address;
+					}
+				}
+
+				$image_id = get_post_thumbnail_id($post_id);
+				if ($image_id) {
+					$schema['image'] = wp_get_attachment_image_url($image_id, 'full');
+				}
+
+				return $schema;
+			}
+		]
+	]);
+
+	return $content;
+});
+
+// ==================================================
+// TAXONOMY CONFIGURATION
+// ==================================================
+
+add_filter('jvb_taxonomy', function ($taxonomies) {
+
+	// SHOP
+	$taxonomies['shop'] = array_merge($taxonomies['shop'] ?? [], [
+		'seo' => [
+			'title_template' => '{{name}} | Tattoo Shop in {{city}}',
+			'description_template' => '{{name}}{{tagline_text}}{{established_text}} in {{city}}. Featuring {{artist_count}} talented artists. Book your appointment today.',
+			'variables' => [
+				'name' => 'term_name',
+				'city' => ['callback' => function ($term_id, $context) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$city_id = $meta->getValue('city');
+					if ($city_id && term_exists((int)$city_id, BASE . 'city')) {
+						$city_term = get_term($city_id, BASE . 'city');
+						if ($city_term && !is_wp_error($city_term)) {
+							return $city_term->name;
+						}
+					}
+					return 'Edmonton';
+				}],
+				'tagline_text' => ['callback' => function ($term_id) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$tagline = $meta->getValue('tagline');
+					return $tagline ? " - {$tagline}" : '';
+				}],
+				'established_text' => ['callback' => function ($term_id) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$established = $meta->getValue('established');
+					return $established ? " Established in {$established}" : '';
+				}],
+				'artist_count' => ['callback' => function ($term_id) {
+					$artists = get_posts([
+						'post_type' => BASE . 'artist',
+						'tax_query' => [[
+							'taxonomy' => BASE . 'shop',
+							'terms' => $term_id
+						]],
+						'posts_per_page' => -1,
+						'fields' => 'ids'
+					]);
+					return count($artists);
+				}],
+			]
+		],
+
+		'schema' => [
+			'type' => 'LocalBusiness',
+			'additional_types' => ['TattooParlor'],
+			'properties' => [
+				'address' => 'address',
+				'telephone' => 'phone',
+				'email' => 'email',
+				'openingHours' => 'hours',
+				'priceRange' => 'price_range',
+				'image' => 'logo',
+				'url' => ['callback' => function ($term_id) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$website = $meta->getValue('website');
+					return $website ?: get_term_link($term_id);
+				}],
+				'sameAs' => ['callback' => function ($term_id) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$links = [];
+					if ($ig = $meta->getValue('instagram')) $links[] = $ig;
+					if ($fb = $meta->getValue('facebook')) $links[] = $fb;
+					return !empty($links) ? $links : null;
+				}],
+				'memberOf' => [
+					'@id' => 'https://edmonton.ink/#organization'
+				],
+			]
+		]
+	]);
+
+	// STYLE
+	$taxonomies['style'] = array_merge($taxonomies['style'] ?? [], [
+		'seo' => [
+			'title_template' => 'Edmonton {{name}} Tattoo Artists | Specialists in {{name}}',
+			'description_template' => '{{name}}{{alt_names}} is a distinctive tattoo style. {{characteristics}} Find Edmonton artists specializing in {{name}} tattoos.',
+			'variables' => [
+				'name' => 'term_name',
+				'alt_names' => ['callback' => function ($term_id) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$alts = $meta->getValue('alternate_name');
+					if (!empty($alts) && is_array($alts)) {
+						$names = array_filter(array_column($alts, 'name'));
+						if (!empty($names)) {
+							return ' (also known as ' . implode(', ', array_slice($names, 0, 2)) . ')';
+						}
+					}
+					return '';
+				}],
+				'characteristics' => ['meta' => 'characteristics', 'truncate' => 100],
+			]
+		],
+
+		'schema' => [
+			'type' => 'CreativeWork',
+			'properties' => [
+				'name' => ['callback' => function ($term_id) {
+					return get_term($term_id)->name . ' Tattoo Style';
+				}],
+				'description' => 'characteristics',
+				'about' => ['meta' => 'description'],
+				'alternateName' => ['callback' => function ($term_id) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$alts = $meta->getValue('alternate_name');
+					if (!empty($alts) && is_array($alts)) {
+						return array_filter(array_column($alts, 'name'));
+					}
+					return null;
+				}],
+			]
+		]
+	]);
+
+	// THEME
+	$taxonomies['theme'] = array_merge($taxonomies['theme'] ?? [], [
+		'seo' => [
+			'title_template' => 'Edmonton {{name}} Tattoos | Find {{name}} Tattoo Designs',
+			'description_template' => 'Explore {{name}} tattoos, a popular motif in Edmonton\'s tattoo scene. {{similar}}Find artists specializing in {{name}} designs.',
+			'variables' => [
+				'name' => 'term_name',
+				'similar' => ['callback' => function ($term_id) {
+					$meta = new \JVBase\meta\MetaManager($term_id, 'term');
+					$similar = $meta->getValue('similar');
+					if (!empty($similar)) {
+						$similar_names = [];
+						foreach ((array)$similar as $similar_id) {
+							$term = get_term($similar_id, BASE . 'theme');
+							if ($term && !is_wp_error($term)) {
+								$similar_names[] = $term->name;
+							}
+						}
+						if (!empty($similar_names)) {
+							return 'Similar themes include ' . implode(', ', array_slice($similar_names, 0, 2)) . '. ';
+						}
+					}
+					return '';
+				}],
+			]
+		],
+
+		'schema' => [
+			'type' => 'CreativeWork',
+			'properties' => [
+				'name' => ['callback' => function ($term_id) {
+					return get_term($term_id)->name . ' Tattoo Theme';
+				}],
+				'description' => ['meta' => 'description'],
+			]
+		]
+	]);
+
+	// CITY
+	$taxonomies['city'] = array_merge($taxonomies['city'] ?? [], [
+		'seo' => [
+			'title_template' => '{{name}} Tattoo Artists & Shops | edmonton.ink',
+			'description_template' => 'Discover {{name}}\'s vibrant tattoo scene featuring {{shop_count}} local shops and {{artist_count}} talented artists. Find top local talent and book your next tattoo today.',
+			'variables' => [
+				'name' => 'term_name',
+				'shop_count' => ['callback' => function ($term_id) {
+					$shops = get_terms([
+						'taxonomy' => BASE . 'shop',
+						'meta_key' => BASE . 'city',
+						'meta_value' => $term_id,
+						'fields' => 'count'
+					]);
+					return is_wp_error($shops) ? 0 : $shops;
+				}],
+				'artist_count' => ['callback' => function ($term_id) {
+					$artists = get_posts([
+						'post_type' => BASE . 'artist',
+						'tax_query' => [[
+							'taxonomy' => BASE . 'city',
+							'terms' => $term_id
+						]],
+						'posts_per_page' => -1,
+						'fields' => 'ids'
+					]);
+					return count($artists);
+				}],
+			]
+		],
+
+		'schema' => [
+			'type' => 'Place',
+			'properties' => [
+				'address' => ['callback' => function ($term_id) {
+					$term = get_term($term_id);
+					return [
+						'@type' => 'PostalAddress',
+						'addressLocality' => $term->name,
+						'addressRegion' => 'Alberta',
+						'addressCountry' => 'CA'
+					];
+				}],
+			]
+		]
+	]);
+
+	return $taxonomies;
+});
diff --git a/inc/managers/SEO/_setup.php b/inc/managers/SEO/_setup.php
new file mode 100644
index 0000000..7cb84e1
--- /dev/null
+++ b/inc/managers/SEO/_setup.php
@@ -0,0 +1,15 @@
+<?php
+
+//require(JVB_DIR . '/inc/managers/SEO/SchemaRegistry.php');
+require(JVB_DIR . '/inc/managers/SEO/FieldBuilder.php');
+require(JVB_DIR . '/inc/managers/SEO/FieldOverrideBuilder.php');
+require(JVB_DIR . '/inc/managers/SEO/TypeBuilder.php');
+require(JVB_DIR . '/inc/managers/SEO/SchemaBuilder.php');
+require(JVB_DIR.'/base/seo.php');
+require(JVB_DIR . '/inc/managers/SEO/ConfigManager.php');
+require(JVB_DIR . '/inc/managers/SEO/BreadcrumbManager.php');
+require(JVB_DIR . '/inc/managers/SEO/SchemaFieldHelpers.php');
+require(JVB_DIR . '/inc/managers/SEO/SchemaReferenceBuilder.php');
+require(JVB_DIR . '/inc/managers/SEO/TemplateResolver.php');
+require(JVB_DIR . '/inc/managers/SEO/SchemaOutputManager.php');
+require(JVB_DIR . '/inc/managers/SEO/SEOAdminPage.php');
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
new file mode 100644
index 0000000..78f3a19
--- /dev/null
+++ b/inc/managers/ScriptLoader.php
@@ -0,0 +1,558 @@
+<?php
+add_action('init', 'jvbRegisterScripts', 5);
+
+function jvbRegisterScripts() {
+	$version = '1.0.9';
+	$strategy = [
+		'strategy'	=> 'defer',
+		'in_footer'	=> true
+	];
+
+	wp_register_style(
+		'jvb-form',
+		JVB_URL.'assets/css/forms.min.css',
+		[],
+		$version,
+	);
+
+	wp_register_style(
+		'jvb-copy-hours',
+		JVB_URL.'assets/css/copy-hours.min.css',
+		[],
+		$version,
+	);
+	wp_register_style(
+		'jvb-dash',
+		JVB_URL.'assets/css/dash.min.css',
+		[],
+		$version
+	);
+
+	//Helper functions used by other classes
+	wp_register_script(
+		'jvb-utility',
+		JVB_URL.'assets/js/min/utility.min.js',
+		['jvb-auth'],
+		$version,
+		$strategy
+	);
+
+	wp_register_script(
+		'jvb-auth',
+		JVB_URL.'assets/js/min/auth.min.js',
+		[],
+		$version,
+		$strategy
+	);
+	wp_register_script(
+		'jvb-interactions',
+		JVB_URL.'assets/js/min/interactions.min.js',
+		[
+			'jvb-queue',
+			'jvb-data-store'
+		],
+		$version,
+		$strategy
+	);
+
+
+	wp_register_script(
+		'jvb-favourites',
+		JVB_URL.'assets/js/min/favourites.min.js',
+		[
+			'jvb-queue',
+			'jvb-data-store',
+			'jvb-interactions',
+		],
+		$version,
+		$strategy
+	);
+
+	wp_register_script(
+		'jvb-votes',
+		JVB_URL.'assets/js/min/votes.min.js',
+		[
+			'jvb-queue',
+			'jvb-data-store',
+			'jvb-interactions',
+		],
+		$version,
+		$strategy
+	);
+
+
+	wp_register_script(
+		'jvb-settings',
+		JVB_URL.'assets/js/min/settings.min.js',
+		[
+			'jvb-utility',
+			'jvb-data-store'
+		],
+		$version,
+		$strategy
+	);
+
+	wp_register_script(
+		'jvb-popup',
+		JVB_URL.'assets/js/min/popup.min.js',
+		[
+			'jvb-a11y'
+		],
+		$version,
+		$strategy
+	);
+
+	//TODO remove?
+//	wp_register_script(
+//		'jvb-media',
+//		JVB_URL.'assets/js/min/media.min.js',
+//		[],
+//		$version,
+//		$strategy
+//	);
+
+
+	wp_register_script(
+		'jvb-copy-hours',
+		JVB_URL.'assets/js/min/hours.min.js',
+		[
+			'jvb-form',
+			'jvb-utility',
+			'jvb-modal',
+			'jvb-a11y'
+		],
+		$version,
+		$strategy
+	);
+
+	wp_register_script(
+		'jvb-gallery',
+		JVB_URL.'assets/js/min/gallery.min.js',
+		[
+			'jvb-utility',
+			'jvb-modal',
+		],
+		$version,
+		$strategy
+	);
+
+	wp_register_script(
+		'jvb-swiper',
+		JVB_URL.'assets/js/min/swiper.min.js',
+		[
+			'jvb-utility',
+		],
+		$version,
+		$strategy
+	);
+
+
+	wp_register_script(
+		'jvb-integrations',
+		JVB_URL.'assets/js/min/integrations.min.js',
+		[],
+		$version,
+		$strategy
+	);
+		$integration_nonces = [
+			'jvb_square_sync' => wp_create_nonce('jvb_square_sync'),
+			'jvb_gmb_sync_reviews' => wp_create_nonce('jvb_gmb_sync'),
+			'jvb_gmb_test_api' => wp_create_nonce('jvb_gmb_test'),
+			'jvb_bluesky_test_post' => wp_create_nonce('jvb_bluesky_test'),
+			'jvb_facebook_test_post' => wp_create_nonce('jvb_facebook_test'),
+			'jvb_instagram_test_post' => wp_create_nonce('jvb_instagram_test'),
+			'jvb_instagram_sync_media' => wp_create_nonce('jvb_instagram_sync'),
+			'jvb_umami_refresh_data' => wp_create_nonce('jvb_umami_refresh'),
+			'jvb_export_integration_settings' => wp_create_nonce('jvb_integration_export'),
+		];
+
+
+	wp_register_script(
+		'jvb-page-nav',
+		JVB_URL.'assets/js/min/page-nav.min.js',
+		[],
+		$version,
+		$strategy
+	);
+
+	//A11y accessibility
+	wp_register_script(
+		'jvb-a11y',
+		JVB_URL.'assets/js/min/a11y.min.js',
+		[],
+		$version,
+		$strategy
+	);
+
+	//Central Error Management
+	wp_register_script(
+		'jvb-error',
+		JVB_URL.'assets/js/min/error.min.js',
+		[
+
+		],
+		$version,
+		$strategy
+	);
+
+	//Simple Cache Management
+	wp_register_script(
+		'jvb-cache',
+		JVB_URL.'assets/js/min/cache.min.js',
+		[],
+		$version,
+		$strategy
+	);
+	//Data Store - IndexedDB utility
+	wp_register_Script(
+		'jvb-data-store',
+		JVB_URL.'assets/js/min/dataStore.min.js',
+		[],
+		$version,
+		$strategy
+	);
+
+	//SEO Admin
+	wp_register_script(
+		'jvb-schema',
+		JVB_URL.'assets/js/min/schema.min.js',
+		['jvb-a11y', 'jvb-form', 'jvb-tabs'],
+		$version,
+		$strategy
+	);
+
+	//Tabs functionality
+	wp_register_script(
+		'jvb-tabs',
+		JVB_URL.'assets/js/min/tabs.min.js',
+		[
+			'jvb-a11y'
+		],
+		$version,
+		$strategy
+	);
+
+	//Modal functionality
+	wp_register_script(
+		'jvb-modal',
+		JVB_URL.'assets/js/min/modal.min.js',
+		[
+			'jvb-a11y'
+		],
+		$version,
+		$strategy
+	);
+
+	//Central Queue Management
+	wp_register_script(
+		'jvb-queue',
+		JVB_URL.'assets/js/min/queue.min.js',
+		[
+			'jvb-a11y',
+			'jvb-error',
+			'jvb-data-store',
+			'jvb-utility',
+			'jvb-popup'
+		],
+		$version,
+		$strategy
+	);
+
+	//TaxonomySelector
+	wp_register_script(
+		'jvb-selector',
+		JVB_URL.'assets/js/min/selector.min.js',
+		[
+			'jvb-utility',
+			'jvb-a11y',
+			'jvb-error',
+			'jvb-data-store',
+			'jvb-modal',
+//            'jvb-loading'
+		],
+		$version,
+		$strategy
+	);
+
+	// Taxonomy creator - only for dashboard/users with permission
+	wp_register_script(
+		'jvb-creator',
+		JVB_URL.'assets/js/min/creator.min.js',
+		['jvb-selector'],
+		$version,
+		$strategy
+	);
+
+	//PostSelector.js
+	wp_register_script(
+		'jvb-post-selector',
+		JVB_URL.'assets/js/min/postSelector.min.js',
+		[
+			'jvb-selector'
+		],
+		'1.0.1',
+		[
+			'strategy'    => 'defer',
+			'in_footer'    => true,
+		]
+	);
+
+	//Upload Manager
+	wp_register_script(
+		'jvb-handle-selection',
+		JVB_URL.'assets/js/min/handleSelection.min.js',
+		[
+			'jvb-a11y',
+			'jvb-utility',
+		],
+		$version,
+		$strategy
+	);
+
+	//TODO: Likely don't need.
+	wp_register_script(
+		'jvb-drag-handler',
+		JVB_URL.'assets/js/min/dragHandler.min.js',
+		[
+			'jvb-a11y',
+			'jvb-utility',
+		],
+		$version,
+		$strategy
+	);
+
+
+	//Upload Manager
+	wp_register_script(
+		'jvb-uploader',
+		JVB_URL.'assets/js/min/uploader.min.js',
+		[
+			'sortable-multidrag',
+			'jvb-cache',
+			'jvb-a11y',
+			'jvb-utility',
+			'jvb-handle-selection',
+			'jvb-modal',
+//			'jvb-drag-handler',
+//            'jvb-loading',
+			'jvb-queue',
+//			'jvb-notifications'
+		],
+		$version,
+		$strategy
+	);
+
+
+	//Notifications
+	wp_register_script(
+		'jvb-notifications',
+		JVB_URL.'assets/js/min/notifications.min.js',
+		[
+			'jvb-utility',
+		],
+		$version,
+		$strategy
+	);
+
+	//Base Form Handler
+	wp_register_script(
+		'jvb-form',
+		JVB_URL.'assets/js/min/form.min.js',
+		[
+			'jvb-utility',
+			'jvb-tabs',
+			'jvb-selector',
+			'jvb-uploader',
+			'sortable-js',
+			'jvb-populate-form',
+			'jvb-quill',
+		],
+		$version,
+		$strategy
+	);
+
+
+	wp_register_script(
+		'jvb-populate-form',
+		JVB_URL.'assets/js/min/populate.min.js',
+		[],
+		$version,
+		$strategy
+	);
+
+	//CRUD Base Manager
+	wp_register_script(
+		'jvb-crud',
+		JVB_URL.'assets/js/min/crud.min.js',
+		[
+			'jvb-selector',
+			'jvb-settings',
+			'jvb-a11y',
+			'jvb-error',
+			'jvb-data-store',
+			'jvb-populate-form',
+			'jvb-queue',
+			'jvb-utility',
+			'jvb-quill',
+			'jvb-form',
+			'jvb-view',
+			'jvb-modal'
+		],
+		$version,
+		$strategy
+	);
+
+	wp_register_script(
+		'jvb-view',
+		JVB_URL.'assets/js/min/view.min.js',
+		[
+			'jvb-settings',
+			'jvb-a11y',
+			'jvb-utility',
+			'jvb-data-store',
+			'jvb-error',
+			'jvb-populate-form'
+		],
+		$version,
+		$strategy,
+	);
+
+	//Bio Manager TODO: Replace with Form Handler
+	wp_register_script(
+		'jvb-bio',
+		JVB_URL.'assets/js/min/bioManager.min.js',
+		[
+			'jvb-tabs',
+			'jvb-form',
+			'jvb-queue'
+		],
+		$version,
+		$strategy
+	);
+	//Shop Manager TODO: Replace with Form Handler
+	wp_register_script(
+		'jvb-shop',
+		JVB_URL.'assets/js/min/shopManager.min.js',
+		[
+			'jvb-tabs',
+			'jvb-form',
+			'jvb-queue'
+		],
+		$version,
+		$strategy
+	);
+	//Content Manager TODO: Replace with CRUD.js
+	wp_register_script(
+		'jvb-content',
+		JVB_URL.'assets/js/min/ContentManager.min.js',
+		[
+			'jvb-queue',
+			'jvb-cache',
+			'jvb-error',
+			'jvb-uploader',
+			'jvb-utility',
+			'jvb-modal',
+			'jvb-selector',
+			'jvb-post-selector',
+		],
+		$version,
+		$strategy
+	);
+
+	//Favourites Manager TODO: Replace with CRUD.js
+	wp_register_script(
+		'jvb-favourites',
+		JVB_URL.'assets/js/min/favouritesManager.min.js',
+		[
+			'jvb-a11y',
+			'jvb-queue',
+			'jvb-cache',
+			'jvb-error',
+			'jvb-utility',
+			'jvb-tabs',
+			'jvb-selector',
+			'jvb-notifications',
+		],
+		$version,
+		$strategy
+	);
+
+	//News Manager TODO: Replace with CRUD.js
+	wp_register_script(
+		'jvb-news',
+		JVB_URL.'assets/js/min/news.min.js',
+		[
+			'jvb-a11y',
+			'jvb-queue',
+			'jvb-cache',
+			'jvb-error',
+			'jvb-utility',
+			'jvb-modal',
+			'jvb-selector',
+			'jvb-tabs',
+		],
+		$version,
+		$strategy
+	);
+	//Notification Manager TODO: Replace with CRUD? Not quite...
+	wp_register_script(
+		'jvb-notification-manager',
+		JVB_URL.'assets/js/min/notificationManager.min.js',
+		[
+			'jvb-a11y',
+			'jvb-tabs',
+		],
+		$version,
+		$strategy
+	);
+
+	wp_register_script(
+		'jvb-navigation',
+		JVB_URL.'assets/js/min/navigation.min.js',
+		[],
+		$version,
+		$strategy
+	);
+
+	/*****************************************
+	Libraries
+	 *****************************************/
+
+	wp_register_script(
+		'quill-js',
+		'https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js',
+		[],
+		null,
+		true
+	);
+
+	wp_register_script(
+		'sortable-js',
+		'https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js',
+		array(),
+		null,
+		true
+	);
+
+	// Load MultiDrag plugin
+	wp_register_script(
+		'sortable-multidrag',
+		'https://cdn.jsdelivr.net/npm/sortablejs@latest/plugins/MultiDrag.min.js',
+		array('sortable-js'),
+		null,
+		true
+	);
+
+	/******************************************
+	Plugins
+	 ******************************************/
+	wp_register_script(
+		'jvb-quill',
+		JVB_URL.'assets/js/min/quill.min.js',
+		[
+			'quill-js'
+		],
+		$version,
+		$strategy
+	);
+}
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index 5124262..c29c326 100644
--- a/inc/managers/_setup.php
+++ b/inc/managers/_setup.php
@@ -1,9 +1,24 @@
 <?php
+
+use JVBase\managers\IconsManager;
 use JVBase\utility\Features;
 
 
+require(JVB_DIR . '/inc/managers/ScriptLoader.php');
 require(JVB_DIR . '/inc/managers/CacheManager.php');
 require(JVB_DIR . '/inc/managers/IconsManager.php');
+add_action('init', 'jvbInitIconsManager', 1); // Priority 1 - very early
+function jvbInitIconsManager(): void
+{
+	// Initialize base sources (this registers hooks and includes defaults)
+	IconsManager::for('icons');
+	IconsManager::for('forms');
+
+	// Only initialize dash if feature is enabled
+	if (Features::forSite()->has('dashboard')) {
+		IconsManager::for('dash');
+	}
+}
 require(JVB_DIR . '/inc/managers/ErrorHandler.php');
 require(JVB_DIR . '/inc/managers/OperationQueue.php');
 require(JVB_DIR . '/inc/managers/EmailManager.php');
@@ -45,9 +60,10 @@
 	require(JVB_DIR . '/inc/managers/NewsRelationships.php');
 }
 
-
-require(JVB_DIR . '/inc/managers/SchemaManager.php');
-require(JVB_DIR . '/inc/managers/SEOMetaManager.php');
+//
+//require(JVB_DIR . '/inc/managers/SchemaManager.php');
+//require(JVB_DIR . '/inc/managers/SEOMetaManager.php');
+require(JVB_DIR . '/inc/managers/SEO/_setup.php');
 require(JVB_DIR . '/inc/managers/DirectoryManager.php');
 require(JVB_DIR . '/inc/managers/ImageGenerator.php');
 require(JVB_DIR . '/inc/managers/AdminPages.php');
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 953450f..733e141 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -202,6 +202,12 @@
 		$validationAttrs = $this->buildValidationAttributes($field);
 		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
 
+		$customData = '';
+		if (array_key_exists('data', $field) && !empty($field['data'])) {
+			foreach ($field['data'] as $key => $v) {
+				$customData .= ($v === '') ? ' data-' . $key : ' data-' . $key . '="' . $v . '"';
+			}
+		}
 		?>
 		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
 			<?= $conditional ?>
@@ -217,6 +223,7 @@
 					name="<?= esc_attr($data['name']) ?>"
 					value="<?= esc_attr($data['value']) ?>"
 					<?= $inputAttrs ?>
+					<?= $customData?>
 				>
 				<span class="validation-icon success" hidden aria-hidden="true">
                     <?= jvbIcon('check-circle') ?>
@@ -475,8 +482,7 @@
 				<select
 					id="<?= esc_attr($data['id']) ?>"
 					name="<?= esc_attr($data['name']) ?>"
-					<?= $inputAttrs ?>
-				>
+					<?= $inputAttrs ?>>
 					<?php foreach ($field['options'] as $key => $label) : ?>
 						<option value="<?= esc_attr($key) ?>" <?php selected($value, $key); ?>>
 							<?= esc_html($label) ?>
@@ -988,6 +994,29 @@
 		<?php
 	}
 
+	private function renderExistingAttachment(int $attachmentId, string $subtype): string
+	{
+		ob_start();
+
+		switch ($subtype) {
+			case 'image':
+				$this->renderImagePreview($attachmentId);
+				break;
+			case 'video':
+				$this->renderVideoPreview($attachmentId);
+				break;
+			case 'document':
+			case 'file':
+				$this->renderFilePreview($attachmentId);
+				break;
+			default:
+				$this->renderImagePreview($attachmentId);
+				break;
+		}
+
+		return ob_get_clean();
+	}
+
 	/**
 	 * Get max file size for subtype
 	 */
@@ -1588,5 +1617,147 @@
 
 		return is_numeric($value) ? [$value] : [];
 	}
+	/**
+	 * Render tag list field - inline tag input interface
+	 */
+	protected function renderTagListField(string $name, mixed $value, array $field): void
+	{
+		$values = is_array($value) ? $value : [];
+		$conditional = $this->handleConditionalField($field);
+		$validationAttrs = $this->buildValidationAttributes($field);
 
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
+		// Tag display format - defaults to first field value
+		$tagFormat = $field['tag_format'] ?? 'first_field';
+		?>
+		<div class="field tag-list <?= esc_attr($name) ?>"
+			 data-field="<?= esc_attr($name) ?>"
+			 data-tag-format="<?= esc_attr($tagFormat) ?>"
+			<?= $describedBy ?>
+			<?= $conditional ?>
+			<?= $validationAttrs ?>>
+
+			<?php if (!empty($field['label'])): ?>
+				<h3><?= esc_html($field['label']) ?></h3>
+			<?php endif; ?>
+
+			<!-- Inline input row -->
+			<div class="tag-input-row">
+				<?php foreach ($field['fields'] as $subfield_name => $subfield_config): ?>
+					<?php
+					$subfield_config['label'] = $subfield_config['label'] ?? ucfirst($subfield_name);
+					$input_name = 'new_' . $subfield_name;
+
+					// Store required state but don't render it on the input
+					// This prevents form submission validation but allows JS validation
+
+					if (array_key_exists('required', $subfield_config)) {
+						$subfield_config['data']['required'] = true;
+						unset($subfield_config['required']); // Remove required for HTML rendering
+					}
+					$subfield_config['data']['ignore'] = true;
+
+					$this->render($input_name, '', $subfield_config, false, false);
+					?>
+				<?php endforeach; ?>
+
+				<button type="button" class="button add-tag-item">
+					<?= jvbIcon('plus') ?> <?= $field['add_label'] ?? 'Add' ?>
+				</button>
+			</div>
+
+			<!-- Tags display -->
+			<div class="tag-items">
+				<?php foreach ($values as $index => $item_data): ?>
+					<?php $this->renderTagItem($field['fields'], $item_data, $index, $name, $tagFormat); ?>
+				<?php endforeach; ?>
+			</div>
+
+			<!-- Template for new tags -->
+			<template class="tag-template">
+				<?php $this->renderTagItem($field['fields'], [], '', $name, $tagFormat); ?>
+			</template>
+
+			<?php if (!empty($field['hint'])): ?>
+				<?php $this->renderHint($field['hint']); ?>
+			<?php endif; ?>
+
+			<?php if (!empty($field['description'])): ?>
+				<?php $this->renderDescription($field['description'], $name); ?>
+			<?php endif; ?>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Render individual tag item
+	 */
+	protected function renderTagItem(array $fields, array $data, int|string $index, string $base_name, string $format): void
+	{
+		$tag_text = $this->getTagDisplayText($fields, $data, $format);
+		?>
+		<div class="tag-item" data-index="<?= esc_attr($index) ?>">
+			<span class="tag-label"><?= esc_html($tag_text) ?></span>
+
+			<!-- Hidden inputs for data -->
+			<?php foreach ($fields as $field_name => $field_config): ?>
+				<?php
+				$value = $data[$field_name] ?? '';
+				$full_name = is_string($index) ? $field_name : "{$base_name}:{$index}:{$field_name}";
+				?>
+				<input type="hidden"
+					   name="<?= esc_attr($full_name) ?>"
+					   value="<?= esc_attr($value) ?>"
+					   data-field="<?= esc_attr($field_name) ?>" />
+			<?php endforeach; ?>
+
+			<button type="button" class="remove-tag" aria-label="Remove">
+				<?= jvbIcon('x') ?>
+			</button>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Get tag display text based on format
+	 */
+	protected function getTagDisplayText(array $fields, array $data, string $format): string
+	{
+		if (empty($data)) {
+			return 'New Item';
+		}
+
+		switch ($format) {
+			case 'first_field':
+				// Use the first field's value
+				$first_key = array_key_first($fields);
+				return $data[$first_key] ?? 'New Item';
+
+			case 'all_fields':
+				// Show all field values separated by commas
+				$values = array_filter(array_values($data));
+				return implode(', ', $values) ?: 'New Item';
+
+			case 'custom':
+				// Custom format - would need callback
+				return 'New Item';
+
+			default:
+				// Format is a template string like "{name} ({email})"
+				if (strpos($format, '{') !== false) {
+					$text = $format;
+					foreach ($data as $key => $value) {
+						$text = str_replace('{' . $key . '}', $value, $text);
+					}
+					return $text;
+				}
+				// Use specific field name
+				return $data[$format] ?? 'New Item';
+		}
+	}
 }
diff --git a/inc/meta/MetaManager.php b/inc/meta/MetaManager.php
index a1d7dbc..57b8076 100644
--- a/inc/meta/MetaManager.php
+++ b/inc/meta/MetaManager.php
@@ -25,6 +25,8 @@
 	protected string|null $object_type;
 	protected int $max_file_size = 5242880;
 	protected ?string $content = null;
+
+	protected ?string $baseKey = null;
 	protected \wpdb $wpdb;
 	protected array $postFields = [
 		'post_title',
@@ -49,12 +51,11 @@
 		'description'
 	];
 
-	public function __construct(?int $ID = null, ?string $type = null, ?string $content = null)
+	public function __construct(int|string|null $ID = null, ?string $type = null, ?string $content = null)
 	{
 		global $wpdb;
 		$this->wpdb = $wpdb;
-		$this->object_id = $ID;
-
+		$this->object_id = is_int($ID) ? $ID : null;
 		$this->object_type = $type;
 		if ($ID) {
 			switch ($type) {
@@ -68,6 +69,10 @@
 				case 'integrations':
 					$this->data = get_user($ID);
 					break;
+				case 'options':
+					$this->baseKey = $ID;
+					$this->data = null;
+					break;
 				default:
 					$this->data = null;
 					break;
@@ -146,7 +151,10 @@
 			case 'integrations':
 				return get_user_meta($this->object_id, $meta_key, true);
 			case 'options':
-				return get_option($meta_key);
+				$key = $this->baseKey
+					? BASE . $this->baseKey . '_' . $name
+					: BASE . $name;
+				return get_option($key);
 			default:
 				return '';
 		}
@@ -354,7 +362,10 @@
 					$result = update_user_meta($this->object_id, $meta_key, $sanitized);
 					break;
 				case 'options':
-					$result = update_option($meta_key, $sanitized);
+					$key = $this->baseKey
+						? BASE . $this->baseKey . '_' . $name
+						: BASE . $name;
+					return update_option($key, $sanitized);
 			}
 
 			if ($result === false) {
diff --git a/inc/meta/MetaRenderer.php b/inc/meta/MetaRenderer.php
index fa70635..cbf8d4b 100644
--- a/inc/meta/MetaRenderer.php
+++ b/inc/meta/MetaRenderer.php
@@ -94,6 +94,72 @@
 		return jvbRenderTermList($terms, $field['label']);
     }
 
+	protected function renderTagListField(string $name, array|bool $value, array $field): string
+	{
+		if (empty($value) || !is_array($value)) {
+			return '';
+		}
+
+		if (!isset($field['fields']) || !is_array($field['fields'])) {
+			return '';
+		}
+
+		$tag_format = $field['tag_format'] ?? 'first_field';
+		$output = '<div class="tag-list-display">';
+
+		if (!empty($field['label']) && ($field['show_label'] ?? false)) {
+			$output .= '<h4 class="tag-list-label">' . esc_html($field['label']) . '</h4>';
+		}
+
+		$output .= '<div class="tag-list-items">';
+
+		foreach ($value as $item) {
+			if (!is_array($item) || empty($item)) {
+				continue;
+			}
+
+			$tag_text = $this->getTagDisplayText($item, $tag_format);
+			$output .= '<span class="tag-list-item">' . esc_html($tag_text) . '</span>';
+		}
+
+		$output .= '</div></div>';
+
+		return $output;
+	}
+
+	/**
+	 * Get display text for a tag based on format
+	 */
+	protected function getTagDisplayText(array $data, string $format): string
+	{
+		$values = array_filter(array_values($data));
+
+		if (empty($values)) {
+			return '';
+		}
+
+		switch ($format) {
+			case 'first_field':
+				return $values[0];
+
+			case 'all_fields':
+				return implode(', ', $values);
+
+			default:
+				// Template format like "{name} ({email})"
+				if (strpos($format, '{') !== false) {
+					$text = $format;
+					foreach ($data as $key => $value) {
+						$text = str_replace('{' . $key . '}', $value, $text);
+					}
+					return $text;
+				}
+
+				// Use specific field
+				return $data[$format] ?? $values[0];
+		}
+	}
+	
     protected function renderRepeaterField($name, $value, $field):string
     {
 //        jvbDump($value, 'Repeater Field:');
diff --git a/inc/meta/MetaSanitizer.php b/inc/meta/MetaSanitizer.php
index aeff742..f4f573f 100644
--- a/inc/meta/MetaSanitizer.php
+++ b/inc/meta/MetaSanitizer.php
@@ -67,6 +67,56 @@
         return implode(',', $values);
     }
 
+	protected function sanitizeTagList(array $values, array $field_config): array
+	{
+		if (!is_array($values)) {
+			return [];
+		}
+
+		if (empty(array_filter($values, fn($value) => !empty($value)))) {
+			return [];
+		}
+
+		if (!isset($field_config['fields']) || !is_array($field_config['fields'])) {
+			return [];
+		}
+
+		$sanitized = [];
+
+		foreach ($values as $row) {
+			if (!is_array($row)) {
+				continue;
+			}
+
+			// Clean up field names (remove prefixes like "fieldname:0:email")
+			$temp = [];
+			foreach ($row as $key => $value) {
+				$key_parts = explode(':', $key);
+				$clean_key = $key_parts[array_key_last($key_parts)];
+				$temp[$clean_key] = $value;
+			}
+			$row = $temp;
+
+			// Sanitize each field
+			$clean_row = [];
+			foreach ($field_config['fields'] as $key => $subfield_config) {
+				if (!array_key_exists($key, $row)) {
+					continue;
+				}
+
+				$subfield_config['name'] = $key; // For backwards compatibility
+				$clean_row[$key] = $this->sanitize($row[$key], $subfield_config);
+			}
+
+			// Only add row if it has at least one non-empty value
+			if (!empty(array_filter($clean_row))) {
+				$sanitized[] = $clean_row;
+			}
+		}
+
+		return $sanitized;
+	}
+
     protected function sanitizeRepeater(array $values, array $field_config):array
     {
         if (!is_array($values)) {
diff --git a/inc/meta/MetaTypeManager.php b/inc/meta/MetaTypeManager.php
index 63348d6..6fcc1ba 100644
--- a/inc/meta/MetaTypeManager.php
+++ b/inc/meta/MetaTypeManager.php
@@ -85,6 +85,11 @@
             'sanitize' => 'sanitizeRepeater',
 			'default'	=> [],
         ],
+		'tag_list' => [
+			'type' => 'object',
+			'sanitize' => 'sanitizeTagList',
+			'default' => []
+		],
 		'group' => [
 			'type' => 'object',
 			'sanitize' => 'sanitizeGroup',
diff --git a/inc/meta/MetaValidator.php b/inc/meta/MetaValidator.php
index 40b1725..1d0236a 100644
--- a/inc/meta/MetaValidator.php
+++ b/inc/meta/MetaValidator.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\meta;
 
+use DateTime;
 use JVBase\meta\MetaTypeManager;
 use WP_Error;
 
@@ -41,59 +42,61 @@
     }
 
 
-    protected function validateNumber(float $value, array $field_config):bool|WP_Error
-    {
-        if (empty($value)) {
-            if (!empty($field['required'])) {
-                return new WP_Error('required_field', 'This field is required');
-            }
-            return true;
-        }
-        if (!is_numeric($value)) {
-            $this->addError($field_config['name'], __('Must be a number', 'jvb'));
-            return false;
-        }
+	protected function validateNumber(float $value, array $field_config):bool|WP_Error
+	{
+		if (empty($value)) {
+			if (!empty($field_config['required'])) { // ✅ Correct variable
+				return new \WP_Error('required_field', 'This field is required');
+			}
+			return true;
+		}
 
-        if (array_key_exists('min', $field_config) && $value < $field_config['min']) {
-            $this->addError(
-                $field_config['name'],
-                sprintf(__('Must be at least %s', 'jvb'), $field_config['min'])
-            );
-            return false;
-        }
+		if (!is_numeric($value)) {
+			$this->addError($field_config['name'], __('Must be a number', 'jvb'));
+			return false;
+		}
 
-        if (array_key_exists('max', $field_config) && $value > $field_config['max']) {
-            $this->addError(
-                $field_config['name'],
-                sprintf(__('Must not exceed %s', 'jvb'), $field_config['max'])
-            );
-            return false;
-        }
+		if (array_key_exists('min', $field_config) && $value < $field_config['min']) {
+			$this->addError(
+				$field_config['name'],
+				sprintf(__('Must be at least %s', 'jvb'), $field_config['min'])
+			);
+			return false;
+		}
 
-        return true;
-    }
+		if (array_key_exists('max', $field_config) && $value > $field_config['max']) {
+			$this->addError(
+				$field_config['name'],
+				sprintf(__('Must not exceed %s', 'jvb'), $field_config['max'])
+			);
+			return false;
+		}
 
-    protected function validateEmail(string $value, array $config):bool|WP_Error
-    {
-        if (empty($value)) {
-            if (!empty($field['required'])) {
-                return new WP_Error('required_field', 'This field is required');
-            }
-            return true;
-        }
-        $check = is_email($value);
-        if (!$check) {
-            $this->addError(
-                $config['name'],
-                __('Must be a valid email', 'jvb')
-            );
-        }
-        return $check;
-    }
+		return true;
+	}
+
+	protected function validateEmail(string $value, array $config):bool|WP_Error
+	{
+		if (empty($value)) {
+			if (!empty($config['required'])) { // ✅ Correct variable
+				return new \WP_Error('required_field', 'This field is required');
+			}
+			return true;
+		}
+
+		// Validate email format
+		if (!is_email($value)) {
+			$this->addError($config['name'], __('Invalid email address', 'jvb'));
+			return false;
+		}
+
+		return true;
+	}
+
 
 	protected function validateGroup(array $value, array $config):bool
 	{
-		if (empty($value)) {
+		if (empty($value) || !is_array($value)) {
 			if (!empty($config['required'])) {
 				$this->addError($config['name'], __('This field is required', 'jvb'));
 				return false;
@@ -101,30 +104,20 @@
 			return true;
 		}
 
-		if (!is_array($value)) {
-			$this->addError($config['name'], __('Group field must be an array', 'jvb'));
-			return false;
-		}
-
-		if (!isset($config['fields']) || !is_array($config['fields'])) {
-			return true; // No subfields to validate
-		}
-
-		$all_valid = true;
-
-		foreach ($config['fields'] as $field_name => $field_config) {
-			if (!isset($value[$field_name])) {
-				continue; // Skip missing fields unless required
-			}
-
-			$field_config['name'] = $config['name'] . '[' . $field_name . ']'; // For error messages
-
-			if (!$this->validate($value[$field_name], $field_config)) {
-				$all_valid = false;
+		// Validate each sub-field
+		if (!empty($config['fields']) && is_array($config['fields'])) {
+			foreach ($config['fields'] as $subFieldName => $subFieldConfig) {
+				if (isset($value[$subFieldName])) {
+					$subFieldConfig['name'] = $subFieldName;
+					$isValid = $this->validate($value[$subFieldName], $subFieldConfig);
+					if (!$isValid) {
+						return false;
+					}
+				}
 			}
 		}
 
-		return $all_valid;
+		return true;
 	}
 
     protected function validateGallery(array|string $value, array $field):bool|WP_Error
@@ -299,6 +292,64 @@
 		return true;
 	}
 
+	protected function validateTagList(array $value, array $config): bool
+	{
+		if (empty($value)) {
+			if (!empty($config['required'])) {
+				$this->addError($config['name'], __('This field is required', 'jvb'));
+				return false;
+			}
+			return true;
+		}
+
+		if (!is_array($value)) {
+			$this->addError($config['name'], __('Invalid data format', 'jvb'));
+			return false;
+		}
+
+		// Check min/max items
+		if (isset($config['min_items']) && count($value) < $config['min_items']) {
+			$this->addError(
+				$config['name'],
+				sprintf(__('Minimum of %d items required', 'jvb'), $config['min_items'])
+			);
+			return false;
+		}
+
+		if (isset($config['max_items']) && count($value) > $config['max_items']) {
+			$this->addError(
+				$config['name'],
+				sprintf(__('Maximum of %d items allowed', 'jvb'), $config['max_items'])
+			);
+			return false;
+		}
+
+		// Validate each item's fields
+		if (!isset($config['fields']) || !is_array($config['fields'])) {
+			return true;
+		}
+
+		foreach ($value as $index => $row) {
+			if (!is_array($row)) {
+				continue;
+			}
+
+			foreach ($config['fields'] as $field_name => $field_config) {
+				if (!isset($row[$field_name])) {
+					continue;
+				}
+
+				$field_config['name'] = "{$config['name']}[{$index}][{$field_name}]";
+
+				if (!$this->validate($row[$field_name], $field_config)) {
+					return false;
+				}
+			}
+		}
+
+		return true;
+	}
+
     protected function validateRepeater(array $value, array $config):bool|WP_Error
     {
         if (empty($value)) {
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index 8980dd6..95745c4 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -497,25 +497,30 @@
         ];
     }
 
-    protected function errorLogTables():array
-    {
-
-        return [
-            'error_log'=> "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `error_type` varchar(50) NOT NULL,
-            `component` varchar(50) NOT NULL,
-            `message` text NOT NULL,
-            `context` JSON,
-            `severity` varchar(20) NOT NULL,
-            `user_id` {$this->userIDType} NOT NULL,
-            `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            KEY `error_lookup` (`error_type`, `severity`, `created_at`),
-            KEY `component_errors` (`component`, `created_at`)
-        )"
-        ];
-    }
+	protected function errorLogTables():array
+	{
+		return [
+			'error_log'=> "(
+				`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+				`error_type` varchar(50) NOT NULL,
+				`component` varchar(100) NOT NULL,
+				`method` varchar(100) DEFAULT NULL,
+				`page_url` varchar(255) DEFAULT NULL,
+				`message` text NOT NULL,
+				`context` JSON,
+				`severity` varchar(20) NOT NULL,
+				`user_id` {$this->userIDType} DEFAULT NULL,
+				`user_was_logged_in` tinyint(1) NOT NULL,
+				`source` enum('frontend','backend') NOT NULL,
+				`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
+				PRIMARY KEY (`id`),
+				KEY `created_at` (`created_at`),
+				KEY `component_severity_date` (`component`, `severity`, `created_at`),
+				KEY `error_type_date` (`error_type`, `created_at`),
+				KEY `severity_date` (`severity`, `created_at`)
+			)"
+		];
+	}
 
 	protected function userIntegrationsTable():array
 	{
diff --git a/inc/registry/FieldRegistry.php b/inc/registry/FieldRegistry.php
index 19eb792..996e99b 100644
--- a/inc/registry/FieldRegistry.php
+++ b/inc/registry/FieldRegistry.php
@@ -42,7 +42,6 @@
 		$this->addFieldProvider('common', new CommonFieldProvider());
 		$this->addFieldProvider('calendar', new CalendarFieldProvider());
 		$this->addFieldProvider('integration', new IntegrationFieldProvider());
-
 //		if (jvbSiteUsesHelcim()) {
 //			$this->addFieldProvider('helcim', new HelcimFieldProvider());
 //		}
@@ -116,6 +115,7 @@
 			unset($fields['common']);
 		}
 
+
 		// Apply integration fields
 		$fields = $this->applyIntegrationFields($fields, $config, $type);
 
diff --git a/inc/rest/RestRouteManager.php b/inc/rest/RestRouteManager.php
index 3d9b2a4..f017e9c 100644
--- a/inc/rest/RestRouteManager.php
+++ b/inc/rest/RestRouteManager.php
@@ -1,6 +1,8 @@
 <?php
 namespace JVBase\rest;
 
+use DateTime;
+use DateTimeZone;
 use JVBase\JVB;
 use JVBase\rest\RateLimiter;
 use JVBase\managers\OperationQueue;
@@ -128,6 +130,32 @@
 		return true;
 	}
 
+	/**
+	 * Convert MySQL datetime to ISO 8601 timestamp with proper timezone
+	 */
+	public function formatTimestamp(?string $mysql_datetime): ?string
+	{
+		if (empty($mysql_datetime)) {
+			return null;
+		}
+
+		try {
+			// Get WordPress timezone - dates are stored in this timezone
+			$wp_timezone = wp_timezone();
+
+			// Parse the datetime in WordPress timezone
+			$date = new DateTime($mysql_datetime, $wp_timezone);
+
+			// Convert to UTC for API consistency
+			$date->setTimezone(new DateTimeZone('UTC'));
+
+			// Return ISO 8601 format
+			return $date->format('c');
+		} catch (Exception $e) {
+			return null;
+		}
+	}
+
 	protected function checkContent(string $content, bool $bool = false):string|bool
 	{
 		$result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??'';
@@ -541,6 +569,7 @@
 	protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response
 	{
 		$data = [
+			'success'	=> false,
 			'message' => $message,
 			'code' => $code
 		];
@@ -556,6 +585,7 @@
 	 */
 	protected function success(array $data, int $status = 200): WP_REST_Response
 	{
+		$data['success'] = true;
 		return new WP_REST_Response($data, $status);
 	}
 
diff --git a/inc/rest/_setup.php b/inc/rest/_setup.php
index 14e88b5..e228477 100644
--- a/inc/rest/_setup.php
+++ b/inc/rest/_setup.php
@@ -24,6 +24,7 @@
 }
 
 require(JVB_DIR . '/inc/rest/routes/QueueRoutes.php');
+require(JVB_DIR . '/inc/rest/routes/SEORoutes.php');
 require(JVB_DIR . '/inc/rest/routes/ErrorRoutes.php');
 require(JVB_DIR . '/inc/rest/routes/UploadRoutes.php');
 require(JVB_DIR . '/inc/rest/routes/SettingsRoutes.php');
diff --git a/inc/rest/routes/AdminRoutes.php b/inc/rest/routes/AdminRoutes.php
index ee71211..cf58a34 100644
--- a/inc/rest/routes/AdminRoutes.php
+++ b/inc/rest/routes/AdminRoutes.php
@@ -393,7 +393,6 @@
         $key = $this->cache->generateKey($args);
 
         $cache = $this->cache->get($key);
-        $cache = false;
         if ($cache) {
             return new WP_REST_Response($cache);
         }
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index bbdc9a5..e323a70 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -297,7 +297,6 @@
 
 
         $cache = $this->cache->get($key);
-		$cache = false;
         if ($cache) {
             $response = new WP_REST_Response($cache);
 			return $this->addCacheHeaders($response);
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index 37bcfbc..29112b5 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -2864,10 +2864,10 @@
             $list_name,
             $inviteButton,
             $inviteUrl,
-            jvbSignature()
+            JVB()->email()->signature()
         );
 
-        return jvbMail($email, $subject, $message);
+        return JVB()->email()->sendEmail($email, $subject, $message);
     }
 
     /**
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index 198e702..bedbdd1 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -354,7 +354,7 @@
 		], $form_type, $form_data);
 
 		// Send the unified email
-		$email_sent = jvbMail($email_data['to'], $email_data['subject'], $email_data['body'], implode(';',$email_data['headers']));
+		$email_sent = JVB()->email()->sendEmail($email_data['to'], $email_data['subject'], $email_data['body'], implode(';',$email_data['headers']));
 
 		// Log the email sending for debugging
 		if ($email_sent) {
diff --git a/inc/rest/routes/Invitations.php b/inc/rest/routes/Invitations.php
index 8758609..4022790 100644
--- a/inc/rest/routes/Invitations.php
+++ b/inc/rest/routes/Invitations.php
@@ -758,9 +758,9 @@
 		}
 		$toContentTax = implode(' ', $toContentTax);
 
-        $button = jvbMailButton($signup_url, 'Join the Scene!');
-        $link = jvbEmailLink($signup_url);
-        $signature = jvbSignature();
+        $button = JVB()->email()->button($signup_url, 'Join the Scene!');
+        $link = JVB()->email()->link($signup_url);
+        $signature = JVB()->email()->signature();
 
 		$message = sprintf(
 			'<p>Hi %s!</p>
@@ -802,7 +802,7 @@
 		);
 
 
-        $success = jvbMail($email, $subject, $message);
+        $success = JVB()->email()->sendEmail($email, $subject, $message);
 
 
         if (!$success) {
@@ -850,7 +850,7 @@
 			$name
 		);
 
-        $success =  jvbMail($email, $subject, $content, 'INVITATION REVOKED');
+        $success =  JVB()->email()->sendEmail($email, $subject, $content, 'INVITATION REVOKED');
         if (!$success) {
             JVB()->error()->log(
                 'invitation_revoke_email',
@@ -1009,7 +1009,6 @@
 
         $key = $this->cache->generateKey($args);
         $cache = $this->cache->get($key);
-        $cache = false;
         if ($cache) {
             return new WP_REST_Response($cache);
         }
diff --git a/inc/rest/routes/LoginRoutes.php b/inc/rest/routes/LoginRoutes.php
index ad47bff..8c663c9 100644
--- a/inc/rest/routes/LoginRoutes.php
+++ b/inc/rest/routes/LoginRoutes.php
@@ -1,14 +1,12 @@
 <?php
 namespace JVBase\rest\routes;
 
-use JVBase\managers\EmailManager;
-use JVBase\managers\LoginManager;
-use JVBase\managers\MagicLinkManager;
 use JVBase\rest\RestRouteManager;
 use JVBase\utility\Features;
 use WP_REST_Request;
 use WP_REST_Response;
 use WP_Error;
+use WP_Session_Tokens;
 use WP_User;
 
 if (!defined('ABSPATH')) {
@@ -17,19 +15,12 @@
 
 class LoginRoutes extends RestRouteManager
 {
-	protected EmailManager $emailManager;
-	protected MagicLinkManager $magic_link;
-	protected LoginManager $loginManager;
 	protected ?string $requestId = null;
 
 	public function __construct()
 	{
 		$this->cache_name = 'auth';
 		$this->cache_ttl = WEEK_IN_SECONDS;
-		$this->emailManager = new EmailManager();
-		if (Features::forSite()->has('magicLink')) {
-			$this->magic_link = new MagicLinkManager();
-		}
 
 		parent::__construct();
 	}
@@ -113,7 +104,7 @@
 		]);
 
 		register_rest_route($this->namespace, '/auth/register', [
-			'method'	=> 'POST',
+			'methods'	=> 'POST',
 			'callback'	=> [$this, 'handleRegister'],
 			'permission_callback' => [$this, 'checkRateLimit'],
 		]);
@@ -153,7 +144,11 @@
 				429
 			);
 		}
+		return $this->login($username, $password, $remember, $request);
+	}
 
+	public function login(string $username, string $password, bool $remember, ?WP_REST_Request $request = null):WP_REST_Response|bool
+	{
 		// Attempt login
 		$user = wp_signon([
 			'user_login'	=> $username,
@@ -166,11 +161,11 @@
 			// Track failed attempt
 			$this->trackFailedLogin($username);
 
-			return $this->error(
+			return ($request) ? $this->error(
 				'Invalid username or password',
 				'login_failed',
 				401
-			);
+			) : false;
 		}
 
 		// Clear failed attempts on success
@@ -181,7 +176,9 @@
 		wp_set_auth_cookie($user->ID, $remember);
 
 		// Store session fingerprint for hijacking protection
-		$this->storeSessionFingerprint($user->ID, $request);
+		if ($request) {
+			$this->storeSessionFingerprint($user->ID, $request);
+		}
 
 		// Trigger WordPress login action
 		do_action('wp_login', $user->user_login, $user);
@@ -192,11 +189,33 @@
 			'remember' => $remember
 		]);
 
-		return $this->success([
+		return ($request) ? $this->success([
 			'message' => 'Login successful',
 			'user' => $this->formatUserData($user),
-			'redirect' => $this->getLoginRedirect($user)
-		]);
+			'redirect' => $this->getRedirect($user, $request->get_param('redirect_to')),
+			'auth' => $this->buildAuth($user->ID)
+		]) : true;
+	}
+
+	protected function getUserNonces(int $userID):array {
+		$nonces = [
+			'wp_rest'	=> wp_create_nonce('wp_rest'),
+		];
+		if (Features::forSite()->has('dashboard')) {
+			$nonces['dash'] = wp_create_nonce('dash-'.$userID);
+		}
+		if (Features::forSite()->has('favourites')) {
+			$nonces['favourites'] = wp_create_nonce('favourites-'.$userID);
+		}
+		if (Features::anyContentHas('karma') ||
+			Features::anyTaxonomyHas('karma') ||
+			Features::anyUserHas('karma')) {
+			$nonces['votes'] = wp_create_nonce('votes-'.$userID);
+		}
+		if (Features::forSite()->has('notifications')) {
+			$nonces['notifications'] = wp_create_nonce('notifications-'.$userID);
+		}
+		return $nonces;
 	}
 
 	/**
@@ -216,27 +235,65 @@
 		wp_logout();
 
 		return $this->success([
-			'message' => 'Logged out successfully'
+			'message' => 'Logged out successfully',
+			'redirect' => $this->getRedirect(get_userdata($user_id), $request->get_param('redirect_to'), 'logout')
 		]);
 	}
 
+	protected function buildAuth(?int $user = null): array
+	{
+		if (is_user_logged_in()) {
+			$user = ($user) ?: get_current_user_id();
+			return [
+				'authenticated' => true,
+				'user' => $user,
+				'nonces' => $this->getUserNonces($user),
+				'session_id' => $this->getSessionId($user)
+			];
+		}
+
+		return [
+			'authenticated' => false,
+			'currentUser' => false,
+			'nonces' => [
+				'wp_rest' => wp_create_nonce('wp_rest')
+			],
+			'session_id' => null
+		];
+	}
+
+	/**
+	 * Get unique session identifier that changes on login/logout
+	 */
+	protected function getSessionId(int $user_id): string
+	{
+		// Use WordPress session tokens
+		$sessions = WP_Session_Tokens::get_instance($user_id);
+		$token = wp_get_session_token(); // Current session token
+
+		if (!$token) {
+			// Fallback to user-specific hash that changes on password reset
+			return md5($user_id . get_user_meta($user_id, 'session_tokens', true));
+		}
+
+		return md5($token);
+	}
+
 	/**
 	 * Get current authentication status
 	 */
 	public function getAuthStatus(WP_REST_Request $request): WP_REST_Response
 	{
-		if (!is_user_logged_in()) {
-			return $this->success([
-				'authenticated' => false
-			]);
-		}
 
-		$user = wp_get_current_user();
+		$responseData = $this->buildAuth();
 
-		return $this->success([
-			'authenticated' => true,
-			'user' => $this->formatUserData($user)
-		]);
+		$response = $this->success($responseData);
+
+		// Add caching headers
+		$response->header('Cache-Control', 'private, max-age=300'); // 5 minutes
+		$response->header('Vary', 'Cookie'); // Important for nginx
+
+		return $response;
 	}
 
 	/**
@@ -365,6 +422,7 @@
 
 		$name = sanitize_text_field($data['name'] ?? '');
 		$email = sanitize_email($data['email'] ?? '');
+		$referral_code = $request->get_param('referral_code')??'';
 		$user_type = sanitize_text_field($data['user_select'] ?? 'subscriber');
 
 		// Validate fields
@@ -376,14 +434,6 @@
 			return $this->error('Email is required', 'missing_email', 400, 'email');
 		}
 
-		// Spam prevention
-		if ($user_type === 'subscriber' && count(JVB_USER) > 0) {
-			$registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false);
-			if (!empty($registerable)) {
-				return $this->error('Please select a valid account type', 'invalid_user_type', 400, 'user_select');
-			}
-		}
-
 		// Check if role can register
 		if ($user_type !== 'subscriber') {
 			if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) {
@@ -396,6 +446,19 @@
 			return $this->error('Email already registered', 'duplicate_email', 400, 'email');
 		}
 
+		// Validate referral code if provided
+		$referrer_id = null;
+		if ($referral_code) {
+			$code = strtoupper(sanitize_text_field($referral_code));
+			$referrer = JVB()->referrals()->getUserByReferralCode($code);
+
+			if (!$referrer) {
+				return $this->error('Invalid referral code', 'invalid_code', 400);
+			}
+
+			$referrer_id = $referrer->ID;
+		}
+
 		// Allow WP plugins to add registration errors
 		$errors = new WP_Error();
 		$errors = apply_filters('registration_errors', $errors, $email, $email);
@@ -408,27 +471,42 @@
 			);
 		}
 
-		// Create user
-		$user_id = wp_create_user($email, wp_generate_password(), $email);
+		// Update user data
+		$role = ($referrer_id) ? get_option(BASE . 'referral_role', BASE . 'client') : jvbCheckBase($user_type);
+		$userData = [
+			'user_login'	=> $email,
+			'user_email'	=> $email,
+			'display_name'	=> $name,
+			'first_name'	=> strtok($name, ' '),
+			'role'			=> $role
+		];
 
-		if (is_wp_error($user_id)) {
-			return $this->error($user_id->get_error_message(), 'user_creation_failed', 500);
+		// Add password if provided, otherwise generate one
+		$password = $request->get_param('password');
+		if ($password) {
+			$userData['user_pass'] = $password;
+		} else {
+			$userData['user_pass'] = wp_generate_password(20, true, true);
 		}
 
-		// Update user data
-		wp_update_user([
-			'ID' => $user_id,
-			'display_name' => $name,
-			'first_name' => strtok($name,' ')
-		]);
+		$user_id = wp_insert_user($userData);
+
+		if (is_wp_error($user_id)) {
+			return $this->error(
+				$user_id->get_error_message(),
+				'registration_failed',
+				500
+			);
+		}
+
+		// Process referral if code was provided
+		if ($referrer_id) {
+			update_user_meta($user_id, BASE . 'pending_referral_code', $referral_code);
+		}
 
 		// Set role
-		$user = new WP_User($user_id);
-		if ($user_type === 'subscriber') {
-			$user->set_role('subscriber');
-		} else {
-			$role = JVB_USER[$user_type]['role'] ?? 'subscriber';
-			$user->set_role($role);
+		$user = get_userdata($user_id);
+		if ($user_type !== 'subscriber') {
 
 			// Check if needs approval
 			if (Features::forMembership()->has('memberVerified') &&
@@ -446,9 +524,6 @@
 			wp_mkdir_p($target_dir);
 		}
 
-		// Save additional fields
-		update_user_meta($user_id, BASE . 'user_type', $user_type);
-
 		// Process additional fields from form
 		foreach ($data as $key => $value) {
 			if (in_array($key, ['name', 'email', 'action', 'request_id', 'user_select', 'cf-turnstile-response'])) {
@@ -457,13 +532,31 @@
 			update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
 		}
 
+		$redirect = $this->getRedirect($user, $request->get_param('redirect_to'), 'register');
+
 		// Handle token handlers
 		do_action('jvbUserRegistered', $user_id, $email, $data);
+		$magic_link_result = JVB()->magicLink()?->sendMagicLink(
+			$email,
+			'login',
+			[
+				'user_id' => $user_id,
+				'redirect' => $redirect
+			]
+		);
 
+		if (is_wp_error($magic_link_result)) {
+			return $this->error(
+				'Account created but failed to send verification email. Please use password reset.',
+				'magic_link_failed',
+				500
+			);
+		}
 
 		return $this->success([
 			'message' => 'Registration successful! Check your email.',
-			'user_id' => $user_id
+			'user_id' => $user_id,
+			'redirect' => $redirect
 		]);
 	}
 	/**************************************************************
@@ -488,18 +581,28 @@
 		];
 	}
 
-	/**
-	 * Get login redirect URL based on user role
-	 */
-	protected function getLoginRedirect(WP_User $user): string
+	protected function getRedirect(WP_User $user, string $url, string $context = 'login'):string
 	{
+		if (!empty($url)) {
+			$url = sanitize_url($url);
+			if (wp_validate_redirect($url)) {
+				return $url;
+			}
+		}
+
+		// Redirect to custom dashboard for members
+		if (function_exists('isOurPeople') && isOurPeople()) {
+			return home_url('/dash');
+		}
+
+		// Admins can go to wp-admin if they want (but only if not using custom dashboard)
 		if (user_can($user, 'manage_options')) {
 			return admin_url();
 		}
 
-		// Redirect to dashboard for members
-		if (function_exists('isOurPeople') && isOurPeople()) {
-			return home_url('/dash');
+		$custom_redirect = get_option(BASE . 'after_'.$context.'_redirect');
+		if ($custom_redirect) {
+			return $custom_redirect;
 		}
 
 		return home_url();
diff --git a/inc/rest/routes/MagicLinkRoutes.php b/inc/rest/routes/MagicLinkRoutes.php
index a422c6f..95addfd 100644
--- a/inc/rest/routes/MagicLinkRoutes.php
+++ b/inc/rest/routes/MagicLinkRoutes.php
@@ -18,11 +18,9 @@
  */
 class MagicLinkRoutes extends RestRouteManager
 {
-	protected MagicLinkManager $magic_link;
 
 	public function __construct()
 	{
-		$this->magic_link = new MagicLinkManager();
 		parent::__construct();
 	}
 
@@ -114,10 +112,6 @@
 		$type = sanitize_text_field($request->get_param('type')) ?? MagicLinkManager::TYPE_LOGIN;
 		$context = $request->get_param('context') ?? [];
 
-		error_log('SendMagicLink request: '.print_r($email, true));
-		error_log('Type: '.print_r($type, true));
-		error_log('Context: '.print_r($context, true));
-
 		// Validate email
 		if (!is_email($email)) {
 			return new WP_REST_Response([
@@ -141,8 +135,7 @@
 		}
 
 		// Send the magic link
-		$result = $this->magic_link->sendMagicLink($email, $type, $context);
-		error_log('Result: '.print_r($result, true));
+		$result = JVB()->magicLink()?->sendMagicLink($email, $type, $context);
 
 		if (is_wp_error($result)) {
 			return new WP_REST_Response([
@@ -184,7 +177,7 @@
 		$email = sanitize_email($request->get_param('email'));
 
 		// This returns array|WP_Error - check for error first
-		$token_data = $this->magic_link->verifyToken($token, $email);
+		$token_data = JVB()->magicLink()?->verifyToken($token, $email);
 
 		if (is_wp_error($token_data)) {
 			return new WP_REST_Response([
@@ -210,42 +203,5 @@
 		], 200);
 	}
 
-	protected function processReferralSignup(array $token_data): void
-	{
-		// Create user account
-		$user_id = wp_create_user(
-			$token_data['email'],
-			wp_generate_password(20, true, true),
-			$token_data['email']
-		);
 
-		if (is_wp_error($user_id)) {
-			wp_die('Failed to create account: ' . $user_id->get_error_message());
-		}
-
-		// Update user info
-		if (!empty($token_data['name'])) {
-			wp_update_user([
-				'ID' => $user_id,
-				'display_name' => $token_data['name'],
-				'first_name' => $token_data['name']
-			]);
-		}
-
-		// Store referral code in user meta (temporary)
-		// ReferralManager::processReferral will pick this up
-		update_user_meta($user_id, BASE . 'pending_referral_code', $token_data['referral_code']);
-
-		// Trigger registration actions (this calls processReferral)
-		do_action('user_register', $user_id);
-
-		// Log the user in
-		wp_set_current_user($user_id);
-		wp_set_auth_cookie($user_id, true);
-		do_action('wp_login', get_user_by('ID', $user_id)->user_login, get_user_by('ID', $user_id));
-
-		// Redirect with referral welcome message
-		wp_safe_redirect(home_url('/dash?referral_welcome=1'));
-		exit;
-	}
 }
diff --git a/inc/rest/routes/QueueRoutes.php b/inc/rest/routes/QueueRoutes.php
index 11b409d..0346eee 100644
--- a/inc/rest/routes/QueueRoutes.php
+++ b/inc/rest/routes/QueueRoutes.php
@@ -228,33 +228,6 @@
 	}
 
 	/**
-	 * Convert MySQL datetime to ISO 8601 timestamp with proper timezone
-	 */
-	protected function formatTimestamp(?string $mysql_datetime): ?string
-	{
-		if (empty($mysql_datetime)) {
-			return null;
-		}
-
-		try {
-			// Get WordPress timezone - dates are stored in this timezone
-			$wp_timezone = wp_timezone();
-
-			// Parse the datetime in WordPress timezone
-			$date = new DateTime($mysql_datetime, $wp_timezone);
-
-			// Convert to UTC for API consistency
-			$date->setTimezone(new DateTimeZone('UTC'));
-
-			// Return ISO 8601 format
-			return $date->format('c');
-
-		} catch (Exception $e) {
-			return null;
-		}
-	}
-
-	/**
 	 * Get human-readable operation title
 	 */
 	protected function getOperationTitle(string $type, array $data): string
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index b8f198c..aec6c55 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -3,6 +3,7 @@
 
 use JVBase\importers\JaneAppClientImporter;
 use JVBase\managers\JaneSalesImporter;
+use JVBase\managers\MagicLinkManager;
 use JVBase\rest\RestRouteManager;
 use WP_REST_Request;
 use WP_REST_Response;
@@ -19,8 +20,6 @@
 {
 	protected string $referrals_table;
 	protected string $rewards_table;
-	protected string $treatments_table;
-	protected string $jane_clients_table;
 	protected $wpdb;
 
 	public function __construct()
@@ -33,703 +32,618 @@
 		$this->wpdb = $wpdb;
 		$this->referrals_table = $wpdb->prefix . BASE . 'referrals';
 		$this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards';
-		$this->treatments_table = $wpdb->prefix . BASE . 'referral_treatments';
-		$this->jane_clients_table = $wpdb->prefix . BASE . 'jane_clients';
+
+		add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
 	}
 
 	public function registerRoutes(): void
 	{
-		// Get user's referrals
+		/**
+		 * Main referrals endpoint
+		 * GET: List referrals with filters
+		 * POST: Perform actions (invite, consulted, treated, remove, resend)
+		 */
 		register_rest_route($this->namespace, "/{$this->route}", [
-			'methods' => 'GET',
-			'callback' => [$this, 'getUserReferrals'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
-
-		register_rest_route($this->namespace, "/{$this->route}/register", [
-			'methods' => 'POST',
-			'callback' => [$this, 'registerWithReferral'],
-			'permission_callback' => [$this, 'checkRateLimit'],
-			'args' => [
-				'name' => [
-					'required' => true,
-					'type' => 'string',
-					'sanitize_callback' => 'sanitize_text_field'
-				],
-				'email' => [
-					'required' => true,
-					'type' => 'string',
-					'format' => 'email',
-					'validate_callback' => function($param) {
-						return is_email($param);
-					}
-				],
-				'code' => [
-					'required' => true,
-					'type' => 'string',
-					'sanitize_callback' => function($code) {
-						return strtoupper(sanitize_text_field($code));
-					}
+			[
+				'methods' => 'GET',
+				'callback' => [$this, 'getReferrals'],
+				'permission_callback' => [$this, 'checkPermission'],
+				'args' => [
+					'user' => ['type' => 'integer', 'sanitize_callback' => 'absint'],
+					'status' => ['type' => 'string', 'enum' => ['all', 'pending', 'consulted', 'treated', 'unused', 'registered', 'completed']],
+					'date_start' => ['type' => 'string'],
+					'date_end' => ['type' => 'string'],
+					'limit' => ['type' => 'integer', 'default' => 50],
+					'offset' => ['type' => 'integer', 'default' => 0],
+					'format' => ['type' => 'string', 'enum' => ['simple', 'formatted'], 'default' => 'formatted'],
+					'search' => ['type' => 'string']
+				]
+			],
+			[
+				'methods' => 'POST',
+				'callback' => [$this, 'handleAction'],
+				'permission_callback' => [$this, 'checkPermission'],
+				'args' => [
+					'action' => [
+						'required' => true,
+						'type' => 'string',
+						'enum' => ['invite', 'consulted', 'treated', 'remove', 'resend']
+					]
 				]
 			]
 		]);
 
-		register_rest_route($this->namespace, '/referrals/check-code', [
-			'methods' => 'POST',
-			'callback' => [$this, 'checkReferralCode'],
-			'permission_callback' => [$this, 'checkRateLimit'],
-			'args' => [
-				'code' => [
-					'required' => true,
-					'type' => 'string',
-					'sanitize_callback' => function($code) {
-						return strtoupper(sanitize_text_field($code));
-					}
-				]
-			]
-		]);
-
-		// Get or create referral code
+		/**
+		 * Referral code endpoint
+		 * GET: Get user's referral code
+		 * POST: Validate a referral code
+		 */
 		register_rest_route($this->namespace, "/{$this->route}/code", [
+			[
+				'methods' => 'GET',
+				'callback' => [$this, 'getCode'],
+				'permission_callback' => [$this, 'checkPermission'],
+				'args' => [
+					'user' => ['type' => 'integer', 'sanitize_callback' => 'absint']
+				]
+			],
+			[
+				'methods' => 'POST',
+				'callback' => [$this, 'validateCode'],
+				'permission_callback' => '__return_true', // Public endpoint
+				'args' => [
+					'code' => ['required' => true, 'type' => 'string']
+				]
+			]
+		]);
+
+		/**
+		 * Stats endpoint
+		 * GET: Get user's referral statistics
+		 */
+		register_rest_route($this->namespace, "/{$this->route}/stats", [
 			'methods' => 'GET',
-			'callback' => [$this, 'getReferralCode'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
-
-		// Track referral click (public endpoint)
-		register_rest_route($this->namespace, "/{$this->route}/track", [
-			'methods' => 'POST',
-			'callback' => [$this, 'trackReferralClick'],
-			'permission_callback' => [$this, 'checkRateLimit'],
-			'args' => [
-				'code' => [
-					'required' => true,
-					'type' => 'string'
-				]
-			]
-		]);
-
-		// Mark referral as treated
-		register_rest_route($this->namespace, "/{$this->route}/(?P<id>\d+)/treat", [
-			'methods' => 'POST',
-			'callback' => [$this, 'markAsTreated'],
-			'permission_callback' => function() {
-				return current_user_can('manage_options');
-			},
-			'args' => [
-				'id' => [
-					'required' => true,
-					'validate_callback' => function($param) {
-						return is_numeric($param);
-					}
-				]
-			]
-		]);
-
-		// Send referral invitation
-		register_rest_route($this->namespace, '/'.$this->route.'/invite', [
-			'methods' => 'POST',
-			'callback' => [$this, 'sendInvitation'],
+			'callback' => [$this, 'getStats'],
 			'permission_callback' => [$this, 'checkPermission'],
 			'args' => [
-				'email' => [
-					'required' => true,
-					'type' => 'string',
-					'format' => 'email',
-					'validate_callback' => function($param) {
-						return is_email($param);
-					}
-				],
-				'name' => [
-					'required' => true,
-					'type' => 'string',
-					'sanitize_callback' => 'sanitize_text_field'
-				]
+				'user' => ['type' => 'integer', 'sanitize_callback' => 'absint'],
 			]
 		]);
 
-		// Send batch invitations
-		register_rest_route($this->namespace, '/'.$this->route.'/invite/batch', [
-			'methods' => 'POST',
-			'callback' => [$this, 'sendBatchInvitations'],
-			'permission_callback' => [$this, 'checkPermission'],
-			'args' => [
-				'invitations' => [
-					'required' => true,
-					'type' => 'array',
-					'validate_callback' => function($param) {
-						return is_array($param) && !empty($param);
-					}
-				]
-			]
-		]);
-
-		// Get invitation stats for current user
-		register_rest_route($this->namespace, '/'.$this->route.'/invite/stats', [
-			'methods' => 'GET',
-			'callback' => [$this, 'getInvitationStats'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
-
-		// Export referrals for Jane App
-		register_rest_route($this->namespace, '/'.$this->route.'/export', [
-			'methods' => 'POST',
-			'callback' => [$this, 'exportReferrals'],
-			'permission_callback' => function() {
-				return current_user_can('manage_options');
-			},
-			'args' => [
-				'start_date' => [
-					'required' => true,
-					'type' => 'string',
-					'validate_callback' => function($param) {
-						return (bool) strtotime($param);
-					}
-				],
-				'end_date' => [
-					'required' => true,
-					'type' => 'string',
-					'validate_callback' => function($param) {
-						return (bool) strtotime($param);
-					}
-				]
-			]
-		]);
-
-
-
-		// Get top referrers (admin only)
-		register_rest_route($this->namespace, "/{$this->route}/leaderboard", [
-			'methods' => 'GET',
-			'callback' => [$this, 'getTopReferrers'],
-			'permission_callback' => function() {
-				return current_user_can('manage_options');
-			},
-			'args' => [
-				'period' => [
-					'default' => 'week',
-					'enum' => ['day', 'week', 'month', 'all']
-				],
-				'limit' => [
-					'default' => 10,
-					'type' => 'integer'
-				]
-			]
-		]);
-
-		// Get/Update referral settings (admin only)
+		/**
+		 * Settings endpoint (admin only)
+		 */
 		register_rest_route($this->namespace, "/{$this->route}/settings", [
 			[
 				'methods' => 'GET',
 				'callback' => [$this, 'getSettings'],
-				'permission_callback' => function() {
-					return current_user_can('manage_options');
-				}
+				'permission_callback' => [$this, 'checkAdminPermission']
 			],
 			[
 				'methods' => 'POST',
 				'callback' => [$this, 'updateSettings'],
-				'permission_callback' => function() {
-					return current_user_can('manage_options');
-				}
+				'permission_callback' => [$this, 'checkAdminPermission']
 			]
 		]);
 
-		register_rest_route($this->namespace, "/{$this->route}/add-code", [
-			'methods' 	=> 'POST',
-			'callback'	=> [$this, 'addReferralCodeAfterRegistration'],
-			'permission_callback'	=> [$this, 'checkRateLimit'],
-			'args'	=> [
-				'code'	=> [
-					'required'	=> true,
-					'type'		=> 'string',
-					'sanitize_callback'	=> function ($code) {
-						return strtoupper(sanitize_text_field($code));
-					}
-				]
-			]
-		]);
-
-
-		/***************************
-		 * ADDITIONAL
+		/**
+		 * CSV Upload endpoints (admin only)
 		 */
-// CSV Uploads
-		register_rest_route($this->namespace, '/referrals/upload-clients', [
+		register_rest_route($this->namespace, "/{$this->route}/upload-clients", [
 			'methods' => 'POST',
 			'callback' => [$this, 'handleClientUpload'],
 			'permission_callback' => [$this, 'checkAdminPermission']
 		]);
 
-		register_rest_route($this->namespace, '/referrals/upload-sales', [
+		register_rest_route($this->namespace, "/{$this->route}/upload-sales", [
 			'methods' => 'POST',
 			'callback' => [$this, 'handleSalesUpload'],
 			'permission_callback' => [$this, 'checkAdminPermission']
 		]);
-
-		// Referral List & Details
-		register_rest_route($this->namespace, '/referrals/list', [
-			'methods' => 'GET',
-			'callback' => [$this, 'getReferralsList'],
-			'permission_callback' => [$this, 'checkAdminPermission']
-		]);
-
-		register_rest_route($this->namespace, '/referrals/(?P<id>\d+)', [
-			'methods' => 'GET',
-			'callback' => [$this, 'getReferralDetails'],
-			'permission_callback' => [$this, 'checkAdminPermission']
-		]);
-
-		// Manual Status Updates
-		register_rest_route($this->namespace, '/referrals/mark-consulted', [
-			'methods' => 'POST',
-			'callback' => [$this, 'handleMarkConsulted'],
-			'permission_callback' => [$this, 'checkAdminPermission']
-		]);
-
-		register_rest_route($this->namespace, '/referrals/mark-treated', [
-			'methods' => 'POST',
-			'callback' => [$this, 'handleMarkTreated'],
-			'permission_callback' => [$this, 'checkAdminPermission']
-		]);
-
-		// User-facing endpoints
-		register_rest_route($this->namespace, '/referrals/my-stats', [
-			'methods' => 'GET',
-			'callback' => [$this, 'getMyStats'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
-
-		register_rest_route($this->namespace, '/referrals/my-referrals', [
-			'methods' => 'GET',
-			'callback' => [$this, 'getMyReferrals'],
-			'permission_callback' => [$this, 'checkPermission']
-		]);
 	}
 
 	/**
-	 * Check admin-only permission
+	 * GET /referrals
+	 * Get referrals with optional filters
+	 * - User gets their own referrals
+	 * - Admin with no user param gets all referrals
 	 */
-	public function checkAdminPermission(WP_REST_Request $request): bool
+	public function getReferrals(WP_REST_Request $request): WP_REST_Response
 	{
-		return current_user_can('manage_options') && parent::checkPermission($request);
-	}
+		$user_id = $request->get_param('user');
 
-	public function checkPermission(WP_REST_Request $request): bool
-	{
-		return is_user_logged_in();
-	}
+		// Determine scope
+		if (!$user_id) {
+			$current_user_id = get_current_user_id();
+			$is_admin = current_user_can('manage_options');
+			if ($is_admin) {
+				// Admin with no user param = get all referrals
+				return $this->getAllReferrals($request);
+			}
+			$user_id = $current_user_id;
+		}
 
-	/**
-	 * Get user's referrals
-	 */
-	public function getUserReferrals(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
 
+		// Get user's referrals
 		$args = [
 			'status' => $request->get_param('status') ?? 'all',
 			'limit' => $request->get_param('limit') ?? 50,
-			'offset' => $request->get_param('offset') ?? 0
+			'offset' => $request->get_param('offset') ?? 0,
+			'date_start' => $request->get_param('date_start'),
+			'date_end' => $request->get_param('date_end'),
 		];
 
+		$cache_key = "ref_{$user_id}_" . md5(serialize($args));
+		// Check headers for 304 Not Modified
+		$cache_check = $this->checkHeaders($request, $cache_key);
+		if ($cache_check instanceof WP_REST_Response) {
+			return $cache_check; // Returns 304 if not modified
+		}
+
 		$referrals = JVB()->referrals()->getUserReferrals($user_id, $args);
 
+		$data = [
+			'items' => $referrals,
+			'total' => count($referrals)
+		];
+
+		// Create response with cache headers
+		$response = $this->success($data);
+
+		// Add ETag and Last-Modified headers
+		return $this->addCacheHeaders($response, $cache_key, $data);
+	}
+
+	/**
+	 * POST /referrals
+	 * Handle various referral actions based on 'action' parameter
+	 */
+	public function handleAction(WP_REST_Request $request): WP_REST_Response
+	{
+		$action = $request->get_param('action');
+
+		return match($action) {
+			'invite' => $this->actionInvite($request),
+			'consulted' => $this->actionUpdateStatus($request, 'consulted'),
+			'treated' => $this->actionUpdateStatus($request, 'treated'),
+			'remove' => $this->actionRemove($request),
+			'resend' => $this->actionResend($request),
+			default => $this->error('Invalid action', 'invalid_action', 400)
+		};
+	}
+
+	/**
+	 * Action: Send batch referral invitations
+	 */
+	protected function actionInvite(WP_REST_Request $request): WP_REST_Response
+	{
+		$data = $request->get_params();
+		error_log('Send Referral Invitations:'.print_r($data, true));
+		$user = absint($request->get_param('user'));
+		if (!$this->checkUser($user)) {
+			return new WP_REST_Response([
+				'success'	=> false,
+				'message'	=> 'No user found'
+			]);
+		}
+		$subject = sanitize_text_field($request->get_param('subject'));
+		$message = sanitize_textarea_field($request->get_param('message'));
+		$invitations = $request->get_param('invite');
+
+		// Validate invitation format
+		foreach ($invitations as $key => $invite) {
+			if (!array_key_exists('name', $invite) || !array_key_exists('email', $invite)) {
+				unset($invitations[$key]);
+			} else {
+				$temp = [
+					'name'	=> sanitize_text_field($invite['name']),
+					'email'	=> sanitize_email($invite['email'])
+				];
+				$invitations[$key] = $temp;
+			}
+		}
+
+		$operationID = sanitize_text_field($request->get_param('id'));
+		$operation = JVB()->queue()->queueOperation(
+			'referral_invite',
+			$user,
+			[
+				'subject' => $subject,
+				'message' => $message,
+				'invitations' => $invitations
+			],
+			[
+				'operation_id'	=> $operationID
+			]
+		);
+
 		return new WP_REST_Response([
-			'success' => true,
-			'referrals' => $referrals
+			'success'	=> true,
+			'message'	=> 'Queued for Processing',
+			'operation'	=> $operationID
 		]);
 	}
 
 	/**
+	 * Action: Update referral status (admin only)
+	 */
+	protected function actionUpdateStatus(WP_REST_Request $request, string $status): WP_REST_Response
+	{
+		if (!current_user_can('manage_options')) {
+			return $this->error('Admin permission required', 'unauthorized', 403);
+		}
+
+		$referral_id = $request->get_param('referral_id');
+		if (!$referral_id) {
+			return $this->error('referral_id required', 'missing_id', 400);
+		}
+
+		$referral = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
+			$referral_id
+		));
+
+		if (!$referral) {
+			return $this->error('Referral not found', 'not_found', 404);
+		}
+
+		// Update status
+		$update_data = ['status' => $status];
+		$update_data["{$status}_at"] = current_time('mysql');
+
+		if ($status === 'treated') {
+			$update_data['treatment_count'] = ($referral->treatment_count ?? 0) + 1;
+		}
+
+		$updated = $this->wpdb->update(
+			$this->referrals_table,
+			$update_data,
+			['id' => $referral_id],
+			array_fill(0, count($update_data), '%s'),
+			['%d']
+		);
+
+
+
+		if ($updated) {
+			// Also create rewards if treated
+			if ($status === 'treated') {
+				$this->createRewards($referral);
+			}
+		}
+
+		$this->cache->clear();
+
+		return $this->success(['message' => "Referral marked as {$status}"]);
+	}
+
+	/**
+	 * Action: Remove referral
+	 */
+	protected function actionRemove(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = $request->get_param('referral_id');
+		if (!$referral_id) {
+			return $this->error('referral_id required', 'missing_id', 400);
+		}
+
+		$referral = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
+			$referral_id
+		));
+
+		if (!$referral) {
+			return $this->error('Referral not found', 'not_found', 404);
+		}
+
+		// Check ownership
+		$current_user_id = get_current_user_id();
+		if ($referral->referrer_id != $current_user_id && !current_user_can('manage_options')) {
+			return $this->error('Unauthorized', 'unauthorized', 403);
+		}
+
+		// Can only remove pending referrals
+		if ($referral->status !== 'pending') {
+			return $this->error('Can only remove pending referrals', 'invalid_status', 400);
+		}
+
+		$this->wpdb->delete($this->referrals_table, ['id' => $referral_id], ['%d']);
+		$this->cache->clear();
+
+		return $this->success(['message' => 'Referral removed']);
+	}
+
+	/**
+	 * Action: Resend invitation
+	 */
+	protected function actionResend(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = $request->get_param('referral_id');
+		if (!$referral_id) {
+			return $this->error('referral_id required', 'missing_id', 400);
+		}
+
+		$current_user_id = get_current_user_id();
+		$referral = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table} WHERE id = %d AND referrer_id = %d",
+			$referral_id,
+			$current_user_id
+		));
+
+		if (!$referral) {
+			return $this->error('Referral not found', 'not_found', 404);
+		}
+
+		// Check rate limit (once per week)
+		$transient_key = 'referral_last_invite_' . md5($referral->referee_email);
+		$last_invite = get_transient($transient_key);
+
+		if ($last_invite && (time() - $last_invite) < WEEK_IN_SECONDS) {
+			return $this->error(
+				'Can only resend once per week',
+				'rate_limit',
+				429
+			);
+		}
+
+		// Resend via referral manager
+		$result = JVB()->referrals()->sendReferralInvitation(
+			$current_user_id,
+			$referral->referee_email,
+			$referral->referee_name,
+			sprintf('Reminder: Join %s', get_bloginfo('name')),
+			'Just a friendly reminder about my invitation!'
+		);
+
+		if (is_wp_error($result)) {
+			return $this->error($result->get_error_message(), 'send_failed', 500);
+		}
+
+		// Set rate limit
+		set_transient($transient_key, time(), WEEK_IN_SECONDS);
+
+		return $this->success(['message' => 'Invitation resent']);
+	}
+
+	/**
+	 * GET /referrals/code
 	 * Get user's referral code
 	 */
-	public function getReferralCode(WP_REST_Request $request): WP_REST_Response
+	public function getCode(WP_REST_Request $request): WP_REST_Response
 	{
-		$user_id = get_current_user_id();
+		$user_id = $request->get_param('user') ?? get_current_user_id();
+
+		// Check permission
+		if ($user_id != get_current_user_id() && !current_user_can('manage_options')) {
+			return $this->error('Unauthorized', 'unauthorized', 403);
+		}
+
 		$code = JVB()->referrals()->getUserReferralCode($user_id);
 
 		if (is_wp_error($code)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => $code->get_error_message()
-			], 400);
+			return $this->error($code->get_error_message(), 'code_error', 400);
 		}
 
-		return new WP_REST_Response([
-			'success' => true,
+		return $this->success([
 			'code' => $code,
 			'share_url' => home_url('/?ref=' . $code)
 		]);
 	}
 
 	/**
-	 * Update user's referral code
+	 * POST /referrals/code
+	 * Validate a referral code
 	 */
-	public function updateReferralCode(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
-		$new_code = strtoupper(sanitize_text_field($request->get_param('code')));
-
-		$result = JVB()->referrals()->getUserReferralCode($user_id, $new_code);
-
-		if (is_wp_error($result)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => $result->get_error_message()
-			], 400);
-		}
-
-		return new WP_REST_Response([
-			'success' => true,
-			'code' => $result,
-			'message' => 'Referral code updated successfully'
-		]);
-	}
-
-	/**
-	 * Track referral click and store in session
-	 */
-	public function trackReferralClick(WP_REST_Request $request): WP_REST_Response
+	public function validateCode(WP_REST_Request $request): WP_REST_Response
 	{
 		$code = strtoupper(sanitize_text_field($request->get_param('code')));
 
-		// Start session if not already started
-		if (session_status() === PHP_SESSION_NONE) {
-			session_start();
+		if (empty($code)) {
+			return $this->error('Code required', 'missing_code', 400);
 		}
 
-		// Store referral code in both session and cookie (30 day expiry)
-		$_SESSION[BASE . 'referral_code'] = $code;
-		setcookie(BASE . 'referral_code', $code, time() + (30 * DAY_IN_SECONDS), '/');
+		$referrer = JVB()->referrals()->getUserByReferralCode($code);
 
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Referral tracked'
+		if (!$referrer) {
+			return $this->error('Invalid referral code', 'invalid_code', 404);
+		}
+
+		// Check self-referral
+		if (is_user_logged_in() && get_current_user_id() === $referrer->ID) {
+			return $this->error('Cannot use your own referral code', 'self_referral', 400);
+		}
+
+		return $this->success([
+			'valid' => true,
+			'code' => $code,
+			'referrer_name' => $referrer->display_name
 		]);
 	}
 
 	/**
-	 * Mark referral as treated
+	 * GET /referrals/stats
+	 * Get user's referral statistics
 	 */
-	public function markAsTreated(WP_REST_Request $request): WP_REST_Response
+	public function getStats(WP_REST_Request $request): WP_REST_Response
 	{
-		$referral_id = intval($request->get_param('id'));
+		$user_id = $request->get_param('user');
 
-		$result = JVB()->referrals()->markAsTreated($referral_id, true);
+		$cache_key = "stats_{$user_id}";
 
-		if (!$result) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Failed to update referral'
-			], 400);
+		// Check for 304 Not Modified
+		$cache_check = $this->checkHeaders($request, $cache_key);
+		if ($cache_check instanceof WP_REST_Response) {
+			return $cache_check;
 		}
 
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Referral marked as treated and rewards created'
-		]);
-	}
-
-	/**
-	 * Get user stats
-	 */
-	public function getUserStats(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
 		$stats = JVB()->referrals()->getUserStats($user_id);
 
-		return new WP_REST_Response([
-			'success' => true,
-			'stats' => $stats
-		]);
+		$response = $this->success(['items' => [$stats]]);
+
+		// Add cache headers (5 minutes for stats)
+		return $this->addCacheHeaders($response, $cache_key, $stats, 5 * MINUTE_IN_SECONDS);
 	}
 
 	/**
-	 * Get top referrers
-	 */
-	public function getTopReferrers(WP_REST_Request $request): WP_REST_Response
-	{
-		$period = $request->get_param('period') ?? 'week';
-		$limit = $request->get_param('limit') ?? 10;
-
-		$top_referrers = JVB()->referrals()->getTopReferrers($limit, $period);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'period' => $period,
-			'referrers' => $top_referrers
-		]);
-	}
-
-	/**
-	 * Get referral settings
+	 * GET /referrals/settings
 	 */
 	public function getSettings(WP_REST_Request $request): WP_REST_Response
 	{
-		$settings = get_option(BASE . 'referral_settings', []);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'settings' => $settings
-		]);
+		$settings = JVB()->referrals()->getRewardSettings();
+		return $this->success(['settings' => $settings]);
 	}
 
 	/**
-	 * Update referral settings
+	 * POST /referrals/settings
 	 */
 	public function updateSettings(WP_REST_Request $request): WP_REST_Response
 	{
 		$settings = [
-			'referrer_reward_type' => $request->get_param('referrer_reward_type') ?? 'per_user',
+			'referrer_reward_type' => $request->get_param('referrer_reward_type') ?? 'fixed',
 			'referrer_reward_amount' => floatval($request->get_param('referrer_reward_amount') ?? 25),
+			'referrer_reward_applies_to' => $request->get_param('referrer_reward_applies_to') ?? 'per_user',
 			'referee_reward_type' => $request->get_param('referee_reward_type') ?? 'percentage',
 			'referee_reward_amount' => floatval($request->get_param('referee_reward_amount') ?? 20),
 			'referee_reward_applies_to' => $request->get_param('referee_reward_applies_to') ?? 'first_order'
 		];
 
 		update_option(BASE . 'referral_settings', $settings);
+		$this->cache->clear();
 
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Settings updated successfully',
+		return $this->success([
+			'message' => 'Settings updated',
 			'settings' => $settings
 		]);
 	}
 
 	/**
-	 * Send a single referral invitation
-	 *
-	 * @param WP_REST_Request $request
-	 * @return WP_REST_Response
+	 * Helper: Get all referrals (admin only)
 	 */
-	public function sendInvitation(WP_REST_Request $request): WP_REST_Response
+	protected function getAllReferrals(WP_REST_Request $request): WP_REST_Response
 	{
-		$user_id = get_current_user_id();
-		$email = sanitize_email($request->get_param('email'));
-		$name = sanitize_text_field($request->get_param('name'));
+		$where = ['1=1'];
+		$where_params = [];
 
-		// Send invitation via ReferralManager
-		$referral_manager = JVB()->referrals();
-		$result = $referral_manager->sendReferralInvitation($user_id, $email, $name);
-
-		if (is_wp_error($result)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => $result->get_error_message(),
-				'code' => $result->get_error_code()
-			], 400);
+		$status = $request->get_param('status');
+		if ($status && $status !== 'all') {
+			$where[] = 'status = %s';
+			$where_params[] = $status;
 		}
 
-		return new WP_REST_Response($result, 200);
+		if ($date_start = $request->get_param('date_start')) {
+			$where[] = 'referred_at >= %s';
+			$where_params[] = $date_start;
+		}
+
+		if ($date_end = $request->get_param('date_end')) {
+			$where[] = 'referred_at <= %s';
+			$where_params[] = $date_end;
+		}
+
+		$search = $request->get_param('search');
+		if (!empty($search)) {
+			$search_term = '%' . $this->wpdb->esc_like($search) . '%';
+			$where[] = '(r.referee_name LIKE %s OR r.referee_email LIKE %s OR r.referral_code LIKE %s OR u.display_name LIKE %s OR ru.display_name LIKE %s OR ru.user_email LIKE %s)';
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+			$where_params[] = $search_term;
+		}
+
+		$limit = $request->get_param('limit') ?? 50;
+		$offset = $request->get_param('offset') ?? 0;
+
+		$where_params[] = $limit;
+		$where_params[] = $offset;
+
+		$query = "SELECT r.*, u.display_name as referrer_name
+			FROM {$this->referrals_table} r
+			LEFT JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
+			WHERE " . implode(' AND ', $where) . "
+			ORDER BY referred_at DESC
+			LIMIT %d OFFSET %d";
+
+		$items = $this->wpdb->get_results($this->wpdb->prepare($query, $where_params));
+
+		error_log('All Referrals result: '.print_r($items, true));
+		return $this->success([
+			'items' => $items,
+			'total' => count($items)
+		]);
 	}
 
 	/**
-	 * Send batch referral invitations
-	 *
-	 * @param WP_REST_Request $request
-	 * @return WP_REST_Response
+	 * Helper: Create rewards for completed referral
 	 */
-	public function sendBatchInvitations(WP_REST_Request $request): WP_REST_Response
+	protected function createRewards(object $referral): void
 	{
-		$user_id = get_current_user_id();
-		$invitations = $request->get_param('invitations');
+		$settings = JVB()->referrals()->getRewardSettings();
 
-		// Validate invitation format
-		foreach ($invitations as $invite) {
-			if (empty($invite['email']) || empty($invite['name'])) {
-				return new WP_REST_Response([
-					'success' => false,
-					'message' => 'Each invitation must have email and name'
-				], 400);
-			}
-		}
-
-		// Send batch via ReferralManager
-		$referral_manager = JVB()->referrals();
-		$result = $referral_manager->sendBatchReferralInvitations($user_id, $invitations);
-
-		return new WP_REST_Response($result, 200);
-	}
-
-	/**
-	 * Get invitation stats for current user
-	 *
-	 * @param WP_REST_Request $request
-	 * @return WP_REST_Response
-	 */
-	public function getInvitationStats(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
-
-		$referral_manager = JVB()->referrals();
-		$stats = $referral_manager->getUserInvitationStats($user_id);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'stats' => $stats
-		], 200);
-	}
-
-	/**
-	 * Export referrals for Jane App cross-reference
-	 * Admin only
-	 *
-	 * @param WP_REST_Request $request
-	 * @return WP_REST_Response
-	 */
-	public function exportReferrals(WP_REST_Request $request): WP_REST_Response
-	{
-		$start_date = sanitize_text_field($request->get_param('start_date'));
-		$end_date = sanitize_text_field($request->get_param('end_date'));
-
-		$referral_manager = JVB()->referrals();
-		$csv_content = $referral_manager->exportReferrals($start_date, $end_date);
-
-		// Return CSV for download
-		return new WP_REST_Response([
-			'success' => true,
-			'csv' => $csv_content,
-			'filename' => sprintf('referrals_%s_to_%s.csv', $start_date, $end_date)
-		], 200);
-	}
-
-	public function registerWithReferral(WP_REST_Request $request): WP_REST_Response
-	{
-		$name = sanitize_text_field($request->get_param('name'));
-		$email = sanitize_email($request->get_param('email'));
-		$code = strtoupper(sanitize_text_field($request->get_param('code')));
-
-		// Validate email
-		if (!is_email($email)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Invalid email address'
-			], 400);
-		}
-
-		// Check if user exists
-		if (email_exists($email)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'An account with this email already exists'
-			], 400);
-		}
-
-		// Validate referral code
-		$referral_manager = JVB()->referrals();
-		$referrer = $referral_manager->getUserByReferralCode($code);
-
-		if (!$referrer) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Invalid referral code'
-			], 404);
-		}
-
-		// Get reward text
-		$settings = $referral_manager->getRewardSettings();
-		$reward_amount = $settings['referee_reward_amount'] ?? 20;
-		$reward_type = $settings['referee_reward_type'] ?? 'percentage';
-		$reward_text = $reward_type === 'percentage'
-			? "{$reward_amount}% off your first treatment!"
-			: "\${$reward_amount} off your first treatment!";
-
-		// Send magic link with referral context via MagicLinkManager
-		$magic_link_manager = new \JVBase\managers\MagicLinkManager();
-
-		$result = $magic_link_manager->sendMagicLink(
-			$email,
-			\JVBase\managers\MagicLinkManager::TYPE_REFERRAL,
+		// Referrer reward
+		$this->wpdb->insert(
+			$this->rewards_table,
 			[
-				'name' => $name,
-				'referral_code' => $code,
-				'referrer_id' => $referrer->ID,
-				'referrer_name' => $referrer->display_name,
-				'reward_text' => $reward_text
-			]
+				'referral_id' => $referral->id,
+				'user_id' => $referral->referrer_id,
+				'reward_type' => 'referrer',
+				'amount' => $settings['referrer_reward_amount'],
+				'reward_calculation' => $settings['referrer_reward_type'],
+				'status' => 'available',
+				'created_at' => current_time('mysql')
+			],
+			['%d', '%d', '%s', '%f', '%s', '%s', '%s']
 		);
 
-		if (is_wp_error($result)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Failed to send registration link. Please try again.'
-			], 500);
+		// Referee reward
+		if ($referral->referee_id) {
+			$this->wpdb->insert(
+				$this->rewards_table,
+				[
+					'referral_id' => $referral->id,
+					'user_id' => $referral->referee_id,
+					'reward_type' => 'referee',
+					'amount' => $settings['referee_reward_amount'],
+					'reward_calculation' => $settings['referee_reward_type'],
+					'status' => 'available',
+					'created_at' => current_time('mysql')
+				],
+				['%d', '%d', '%s', '%f', '%s', '%s', '%s']
+			);
 		}
-
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Check your email! We sent you a link to complete your registration.',
-			'email' => $email
-		], 200);
 	}
 
-	public function checkReferralCode(WP_REST_Request $request): WP_REST_Response
-	{
-		$code = strtoupper(sanitize_text_field($request->get_param('code')));
-
-		if (empty($code)) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Code is required'
-			], 400);
-		}
-
-		$referral_manager = JVB()->referrals();
-		$referrer = $referral_manager->getUserByReferralCode($code);
-
-		if (!$referrer) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Invalid referral code'
-			], 404);
-		}
-		if (is_user_logged_in() && get_current_user_id() === $referrer->ID) {
-			return $this->error('You cannot use your own referral code', 'self_referral', 400);
-		}
-
-		// Return basic referrer info (no sensitive data)
-		return new WP_REST_Response([
-			'success' => true,
-			'code' => $code,
-			'referrer_name' => $referrer->display_name,
-		], 200);
-	}
-
-	public function addReferralCodePostRegistration(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
-		$code = $request->get_param('code');
-
-		// Check if user already has a referral (can't change)
-		$existing = JVB()->referrals()->getReferralByReferee($user_id);
-		if ($existing) {
-			return $this->error('You already have a referral code applied', 'already_referred', 400);
-		}
-
-		// Validate the code exists
-		$referrer = JVB()->referrals()->getUserByReferralCode($code);
-		if (!$referrer) {
-			return $this->error('Invalid referral code', 'invalid_code', 400);
-		}
-
-		// Create the referral
-		$user = wp_get_current_user();
-		$result = JVB()->referrals()->createReferral($referrer->ID, $user_id, $code);
-
-		if ($result) {
-			return $this->success([
-				'message' => 'Referral code applied successfully!',
-				'referrer_name' => $referrer->display_name
-			]);
-		}
-
-		return $this->error('Failed to apply referral code', 'creation_failed', 500);
-	}
-
-	/**********************************
-	 * ADDITIONAL
+	/**
+	 * Check admin permission
 	 */
+	public function checkAdminPermission(WP_REST_Request $request): bool
+	{
+		return current_user_can('manage_options') && parent::checkPermission($request);
+	}
+
+	/**
+	 * Process queued referral operations
+	 */
+	public function processOperation(WP_Error|array $result, object $operation, array $data): array|WP_Error
+	{
+		if ($operation->type !== 'referral_invite') {
+			return $result;
+		}
+
+		$result = JVB()->referrals()->sendBatchReferralInvitations(
+			$operation->user_id,
+			$data['invitations'],
+			$data['subject'],
+			$data['message']
+		);
+		if ($result['success']) {
+			$this->cache->clear();
+		}
+		error_log('Result: '.print_r($result, true));
+		return $result;
+	}
+
 	/**
 	 * Handle client CSV upload
 	 */
@@ -802,7 +716,7 @@
 		return new WP_REST_Response([
 			'success' => true,
 			'message' => $message,
-			'stats' => $result,
+			'items' => $result,
 			'skipped_details' => $details
 		]);
 	}
@@ -864,321 +778,4 @@
 			'stats' => $result
 		]);
 	}
-
-	/**
-	 * Get referrals list for table display
-	 */
-	public function getReferralsList(WP_REST_Request $request): WP_REST_Response
-	{
-		$page = $request->get_param('page') ?: 1;
-		$per_page = $request->get_param('per_page') ?: 20;
-		$orderby = $request->get_param('orderby') ?: 'referred_at';
-		$order = strtoupper($request->get_param('order')) === 'ASC' ? 'ASC' : 'DESC';
-		$status = $request->get_param('status') ?: '';
-		$search = $request->get_param('search') ?: '';
-
-		$offset = ($page - 1) * $per_page;
-
-		// Build WHERE clause
-		$where_clauses = [];
-		$where_params = [];
-
-		if (!empty($status)) {
-			$where_clauses[] = "r.status = %s";
-			$where_params[] = $status;
-		}
-
-		if (!empty($search)) {
-			$where_clauses[] = "(r.referee_name LIKE %s OR r.referee_email LIKE %s OR referrer.display_name LIKE %s)";
-			$search_term = '%' . $this->wpdb->esc_like($search) . '%';
-			$where_params[] = $search_term;
-			$where_params[] = $search_term;
-			$where_params[] = $search_term;
-		}
-
-		$where = !empty($where_clauses) ? ' WHERE ' . implode(' AND ', $where_clauses) : '';
-
-		// Sanitize orderby to prevent SQL injection
-		$allowed_orderby = ['referred_at', 'consulted_at', 'treated_at', 'status', 'referee_name', 'referrer_name'];
-		if (!in_array($orderby, $allowed_orderby)) {
-			$orderby = 'referred_at';
-		}
-
-		// Get referrals with user info
-		$query = "SELECT
-			r.*,
-			referrer.display_name as referrer_name,
-			referrer.user_email as referrer_email,
-			referee.display_name as referee_display_name,
-			referee.user_email as referee_display_email,
-			(SELECT COUNT(*) FROM {$this->referrals_table} WHERE referrer_id = r.referrer_id) as referrer_total_referrals,
-			(SELECT SUM(amount) FROM {$this->rewards_table} WHERE user_id = r.referrer_id AND status = 'available') as referrer_available_rewards
-		FROM {$this->referrals_table} r
-		LEFT JOIN {$this->wpdb->users} referrer ON r.referrer_id = referrer.ID
-		LEFT JOIN {$this->wpdb->users} referee ON r.referee_id = referee.ID
-		{$where}
-		ORDER BY {$orderby} {$order}
-		LIMIT %d OFFSET %d";
-
-		$where_params[] = $per_page;
-		$where_params[] = $offset;
-
-		$prepared_query = $this->wpdb->prepare($query, $where_params);
-		$referrals = $this->wpdb->get_results($prepared_query);
-
-		// Get total count
-		$count_query = "SELECT COUNT(*) FROM {$this->referrals_table} r
-			LEFT JOIN {$this->wpdb->users} referrer ON r.referrer_id = referrer.ID
-			{$where}";
-
-		$total = $this->wpdb->get_var(
-			!empty($where_params) && count($where_params) > 2 ?
-				$this->wpdb->prepare($count_query, array_slice($where_params, 0, -2)) :
-				$count_query
-		);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'referrals' => $referrals,
-			'total' => (int)$total,
-			'page' => (int)$page,
-			'per_page' => (int)$per_page,
-			'total_pages' => ceil($total / $per_page)
-		]);
-	}
-
-	/**
-	 * Get details for a specific referral
-	 */
-	public function getReferralDetails(WP_REST_Request $request): WP_REST_Response
-	{
-		$referral_id = $request->get_param('id');
-
-		$referral = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT r.*,
-				referrer.display_name as referrer_name,
-				referee.display_name as referee_display_name
-			FROM {$this->referrals_table} r
-			LEFT JOIN {$this->wpdb->users} referrer ON r.referrer_id = referrer.ID
-			LEFT JOIN {$this->wpdb->users} referee ON r.referee_id = referee.ID
-			WHERE r.id = %d",
-			$referral_id
-		));
-
-		if (!$referral) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Referral not found'
-			], 404);
-		}
-
-		// Get associated treatments
-		$treatments = $this->wpdb->get_results($this->wpdb->prepare(
-			"SELECT * FROM {$this->treatments_table} WHERE referral_id = %d ORDER BY treatment_date DESC",
-			$referral_id
-		));
-
-		// Get associated rewards
-		$rewards = $this->wpdb->get_results($this->wpdb->prepare(
-			"SELECT * FROM {$this->rewards_table} WHERE referral_id = %d",
-			$referral_id
-		));
-
-		return new WP_REST_Response([
-			'success' => true,
-			'referral' => $referral,
-			'treatments' => $treatments,
-			'rewards' => $rewards
-		]);
-	}
-
-	/**
-	 * Handle manual mark as consulted
-	 */
-	public function handleMarkConsulted(WP_REST_Request $request): WP_REST_Response
-	{
-		$referral_id = $request->get_param('referral_id');
-
-		if (!$referral_id) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Referral ID required'
-			], 400);
-		}
-
-		$referral = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
-			$referral_id
-		));
-
-		if (!$referral) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Referral not found'
-			], 404);
-		}
-
-		if ($referral->status !== 'pending') {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Referral is not pending'
-			], 400);
-		}
-
-		// Update to consulted
-		$this->wpdb->update(
-			$this->referrals_table,
-			[
-				'status' => 'consulted',
-				'consulted_at' => current_time('mysql')
-			],
-			['id' => $referral_id],
-			['%s', '%s'],
-			['%d']
-		);
-
-		// Create consultation reward (20% off)
-		$this->wpdb->insert(
-			$this->rewards_table,
-			[
-				'referral_id' => $referral_id,
-				'user_id' => $referral->referee_id,
-				'reward_type' => 'referee',
-				'amount' => 20,
-				'reward_calculation' => 'percentage',
-				'status' => 'available',
-				'created_at' => current_time('mysql'),
-				'notes' => 'Consultation reward - 20% off first treatment'
-			],
-			['%d', '%d', '%s', '%f', '%s', '%s', '%s', '%s']
-		);
-
-		// Clear cache
-		$this->cache->clear();
-
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Marked as consulted and reward created'
-		]);
-	}
-
-	/**
-	 * Handle manual mark as treated
-	 */
-	public function handleMarkTreated(WP_REST_Request $request): WP_REST_Response
-	{
-		$referral_id = $request->get_param('referral_id');
-
-		if (!$referral_id) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Referral ID required'
-			], 400);
-		}
-
-		$referral = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
-			$referral_id
-		));
-
-		if (!$referral) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Referral not found'
-			], 404);
-		}
-
-		if ($referral->status === 'treated') {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'Referral already marked as treated'
-			], 400);
-		}
-
-		// Update to treated
-		$this->wpdb->update(
-			$this->referrals_table,
-			[
-				'status' => 'treated',
-				'treated_at' => current_time('mysql'),
-				'treatment_count' => ($referral->treatment_count ?? 0) + 1
-			],
-			['id' => $referral_id],
-			['%s', '%s', '%d'],
-			['%d']
-		);
-
-		// Create full rewards for both parties
-		$settings = JVB()->referrals()->getRewardSettings();
-
-		// Referrer reward
-		$this->wpdb->insert(
-			$this->rewards_table,
-			[
-				'referral_id' => $referral_id,
-				'user_id' => $referral->referrer_id,
-				'reward_type' => 'referrer',
-				'amount' => $settings['referrer_reward_amount'],
-				'reward_calculation' => $settings['referrer_reward_type'],
-				'status' => 'available',
-				'created_at' => current_time('mysql'),
-				'notes' => 'Referral reward for completed treatment'
-			],
-			['%d', '%d', '%s', '%f', '%s', '%s', '%s', '%s']
-		);
-
-		// Referee reward
-		$this->wpdb->insert(
-			$this->rewards_table,
-			[
-				'referral_id' => $referral_id,
-				'user_id' => $referral->referee_id,
-				'reward_type' => 'referee',
-				'amount' => $settings['referee_reward_amount'],
-				'reward_calculation' => $settings['referee_reward_type'],
-				'status' => 'available',
-				'created_at' => current_time('mysql'),
-				'notes' => 'Treatment completion reward'
-			],
-			['%d', '%d', '%s', '%f', '%s', '%s', '%s', '%s']
-		);
-
-		// Clear cache
-		$this->cache->clear();
-
-		return new WP_REST_Response([
-			'success' => true,
-			'message' => 'Marked as treated and rewards created'
-		]);
-	}
-
-	/**
-	 * Get current user's referral stats
-	 */
-	public function getMyStats(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
-		$stats = JVB()->referrals()->getUserStats($user_id);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'stats' => $stats
-		]);
-	}
-
-	/**
-	 * Get current user's referrals
-	 */
-	public function getMyReferrals(WP_REST_Request $request): WP_REST_Response
-	{
-		$user_id = get_current_user_id();
-		$limit = $request->get_param('limit') ?: 20;
-
-		$referrals = JVB()->referrals()->getUserReferrals($user_id, ['limit' => $limit]);
-
-		return new WP_REST_Response([
-			'success' => true,
-			'referrals' => $referrals
-		]);
-	}
 }
diff --git a/inc/rest/routes/SEORoutes.php b/inc/rest/routes/SEORoutes.php
new file mode 100644
index 0000000..f75dc1a
--- /dev/null
+++ b/inc/rest/routes/SEORoutes.php
@@ -0,0 +1,297 @@
+<?php
+namespace JVBase\rest\routes;
+
+use JVBase\rest\RestRouteManager;
+use JVBase\managers\CacheManager;
+use JVBase\managers\SEO\ConfigManager;
+use JVBase\managers\SEO\SchemaBuilder;
+use WP_REST_Request;
+use WP_REST_Response;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * SEO Routes Class
+ *
+ * Handles REST API endpoints for SEO configuration
+ * Works with FormController.js for unified form handling
+ */
+class SEORoutes extends RestRouteManager
+{
+	protected SchemaBuilder $registry;
+
+	public function __construct()
+	{
+		$this->cache_name = 'schema';
+		parent::__construct();
+		$this->registry = SchemaBuilder::getInstance();
+	}
+
+	/**
+	 * Register REST routes
+	 */
+	public function registerRoutes(): void
+	{
+		// Main SEO endpoint - handles save, reset, preview
+		register_rest_route($this->namespace, '/seo', [
+			[
+				'methods' => 'POST',
+				'callback' => [$this, 'handleSEO'],
+				'permission_callback' => fn() => current_user_can('manage_options'),
+				'args' => [
+					'action' => [
+						'required' => false,
+						'type' => 'string',
+						'default' => 'save',
+						'enum' => ['save', 'reset', 'preview']
+					],
+					'context' => [
+						'required' => true,
+						'type' => 'string',
+						'description' => 'site, business, or content/taxonomy/user type'
+					]
+				]
+			]
+		]);
+
+		// Get fields for a schema type (for dynamic type switching)
+		register_rest_route($this->namespace, '/seo/fields', [
+			[
+				'methods' => 'GET',
+				'callback' => [$this, 'getFields'],
+				'permission_callback' => fn() => current_user_can('manage_options'),
+				'args' => [
+					'type' => [
+						'required' => true,
+						'type' => 'string'
+					]
+				]
+			]
+		]);
+	}
+
+	/**
+	 * Main SEO handler - routes to appropriate action
+	 */
+	public function handleSEO(WP_REST_Request $request): WP_REST_Response
+	{
+		$action = $request->get_param('action') ?? 'save';
+		$context = $request->get_param('context');
+
+		// Verify context is valid
+		if (!$this->isValidContext($context)) {
+			return $this->validationError([
+				'context' => "Invalid context: {$context}"
+			]);
+		}
+
+		return match($action) {
+			'save' => $this->saveSEO($request),
+			'reset' => $this->resetSEO($request),
+			'preview' => $this->previewSchema($request),
+			default => $this->validationError(['action' => 'Invalid action'])
+		};
+	}
+
+	/**
+	 * Save SEO configuration
+	 */
+	protected function saveSEO(WP_REST_Request $request): WP_REST_Response
+	{
+		$context = $request->get_param('context');
+		$data = $request->get_json_params();
+
+		// Remove action and context from data
+		unset($data['action'], $data['context']);
+
+		// Handle site-wide settings
+		if (in_array($context, ['site', 'business'])) {
+			return $this->saveSiteSettings($context, $data);
+		}
+
+		// Handle content/taxonomy/user type settings
+		return $this->saveTypeSettings($context, $data);
+	}
+
+	/**
+	 * Save site-wide settings (WebSite or Organization)
+	 */
+	protected function saveSiteSettings(string $context, array $data): WP_REST_Response
+	{
+		$errors = [];
+
+		$config = ConfigManager::for($context);
+		$result = $config->updateConfig($data);
+
+		if (is_wp_error($result)) {
+			$errors[$context] = [
+				'message' => $result->get_error_message(),
+				'errors' => $result->get_error_data()
+			];
+		}
+
+		if (!empty($errors)) {
+			return $this->validationError($errors);
+		}
+
+		// Invalidate cache
+		$this->cache->invalidate();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'status'	=> 'completed',
+			'message' => ucfirst($context) . ' settings saved successfully'
+		]);
+	}
+
+	/**
+	 * Save content/taxonomy/user type settings
+	 */
+	protected function saveTypeSettings(string $type, array $data): WP_REST_Response
+	{
+		$config = ConfigManager::for($type);
+
+		// Separate meta and schema data if needed
+		$meta = $data['meta'] ?? [];
+		$schema = $data['schema'] ?? $data; // If no separation, treat all as schema
+
+		$errors = [];
+
+		// Save meta configuration
+		if (!empty($meta)) {
+			$metaResult = $config->updateMeta($meta);
+			if (is_wp_error($metaResult)) {
+				$errors['meta'] = [
+					'message' => $metaResult->get_error_message(),
+					'errors' => $metaResult->get_error_data()
+				];
+			}
+		}
+
+		// Save schema configuration
+		if (!empty($schema)) {
+			$schemaResult = $config->updateConfig($schema);
+			if (is_wp_error($schemaResult)) {
+				$errors['schema'] = [
+					'message' => $schemaResult->get_error_message(),
+					'errors' => $schemaResult->get_error_data()
+				];
+			}
+		}
+
+		if (!empty($errors)) {
+			return $this->validationError($errors);
+		}
+
+		// Invalidate cache
+		$this->cache->invalidate();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'status'	=> 'completed',
+			'message' => 'Configuration saved successfully'
+		]);
+	}
+
+	/**
+	 * Reset SEO configuration to defaults
+	 */
+	protected function resetSEO(WP_REST_Request $request): WP_REST_Response
+	{
+		$context = $request->get_param('context');
+		$data = $request->get_json_params();
+
+		$resetMeta = $data['resetMeta'] ?? false;
+		$resetSchema = $data['resetSchema'] ?? false;
+
+		$config = ConfigManager::for($context);
+
+		// Reset based on what was requested
+		if ($resetMeta) {
+			$config->resetMeta();
+		}
+
+		if ($resetSchema) {
+			$config->resetConfig();
+		}
+
+		if (!$resetMeta && !$resetSchema) {
+			// Default: reset everything
+			$config->resetAll();
+		}
+
+		// Invalidate cache
+		$this->cache->invalidate();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'status'	=> 'completed',
+			'message' => 'Reset to defaults successfully',
+			'meta' => $config->meta(),
+			'schema' => $config->schema()
+		]);
+	}
+
+	/**
+	 * Preview schema output
+	 */
+	protected function previewSchema(WP_REST_Request $request): WP_REST_Response
+	{
+		$data = $request->get_json_params();
+		$schemaType = $data['type'] ?? 'Thing';
+
+		// Get field definitions from registry
+		$fieldDefinitions = $this->registry->getMetaConfigForType($schemaType);
+
+		// Build schema with actual form values
+		$schema = [
+			'@type' => $schemaType,
+			'@id'   => get_home_url() . '/#' . strtolower($schemaType),
+		];
+
+		// Add fields with their actual values
+		foreach ($data['fields'] ?? [] as $fieldName => $value) {
+			if (empty($value)) {
+				continue;
+			}
+
+			$schema[$fieldName] = $value;
+		}
+
+		return new WP_REST_Response([
+			'success' => true,
+			'schema' => $schema
+		]);
+	}
+
+	/**
+	 * Get fields for a schema type
+	 * Used for dynamic type switching
+	 */
+	public function getFields(WP_REST_Request $request): WP_REST_Response
+	{
+		$type = $request->get_param('type');
+
+		// Get MetaManager field definitions from registry
+		$fields = $this->registry->getMetaConfigForType($type);
+
+		return new WP_REST_Response($fields);
+	}
+
+	/**
+	 * Validate context is a valid type
+	 */
+	protected function isValidContext(string $context): bool
+	{
+		// Site-wide contexts
+		if (in_array($context, ['site', 'business'])) {
+			return true;
+		}
+
+		// Check if it's a valid content/taxonomy/user type
+		return $this->checkContent($context, true);
+	}
+}
diff --git a/inc/rest/routes/ShopRoutes.php b/inc/rest/routes/ShopRoutes.php
index 4c52ae1..1290231 100644
--- a/inc/rest/routes/ShopRoutes.php
+++ b/inc/rest/routes/ShopRoutes.php
@@ -735,7 +735,7 @@
             esc_url($invitation_url)
         );
 
-        return jvbMail($email, $subject, $message);
+        return JVB()->email()->sendEmail($email, $subject, $message);
     }
 
     /**
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
new file mode 100644
index 0000000..8390fc3
--- /dev/null
+++ b/inc/ui/CRUDSkeleton.php
@@ -0,0 +1,1731 @@
+<?php
+namespace JVBase\ui;
+
+use JVBase\managers\UserTermsManager;
+use JVBase\meta\MetaForm;
+use JVBase\meta\MetaManager;
+use WP_User;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * CRUDSkeleton - Fluent builder for flexible CRUD interfaces
+ *
+ * Provides a reusable HTML skeleton for CRUD operations on any data type,
+ * not just WP posts. Can be used for custom tables, user data, etc.
+ *
+ * @example
+ * $crud = new CRUDSkeleton();
+ * $crud->title('Your Referrals', 'Track and manage your referral links')
+ *      ->addFilter('date')
+ *      ->addFilter('status', ['active', 'pending', 'expired'])
+ *      ->addView('grid')
+ *      ->addView('table')
+ *      ->dataSource([$this, 'getReferrals'])
+ *      ->render();
+ */
+class CRUDSkeleton {
+	protected WP_User $user;
+	protected int $user_id;
+
+	// Core configuration
+	protected string $title = '';
+	protected string $description = '';
+	protected string $dataType = '';
+	protected string $singular = '';
+	protected string $plural = '';
+	protected string $icon = 'triangle';
+
+	// Capabilities
+	protected array $caps = [];
+	private array $allowedCaps = ['view','edit', 'create', 'delete'];
+	protected bool $userCanPublish = false;
+
+	// Features
+	protected array $filters = [];
+	protected array $taxonomies = [];
+	protected array $views = [];
+	protected array $defaultViews = ['grid', 'list', 'table'];
+	protected string $defaultView = 'grid';
+	protected bool $hasSearch = false;
+
+	protected array $itemActions = [];
+	protected array $defaultItemActions = [
+		'edit' => [
+			'title'	=> 'Edit',
+			'icon'	=> 'pencil-simple'
+		],
+		'trash'=> [
+			'title'	=> 'Scrap',
+			'icon'	=> 'trash'
+		]
+	];
+	protected bool $isTimeline = false;
+	protected array $nonTimelineFields = [];
+	protected array $timelineSharedFields = [];
+	protected array $timelineUniqueFields = [];
+	protected bool $isCalendar = false;
+	protected bool $useCRUDjs = true;
+	//Bulk Actions
+	protected array $bulkActions = [];
+	private array $allowedBulkActions = ['edit', 'publish', 'draft', 'copy', 'trash'];
+	protected array $defaultBulkActions = [
+		'edit'	=> 'Edit',
+		'publish'	=> 'Show',
+		'draft'	=> 'Hide',
+		'copy'	=> 'Duplicate',
+		'trash'	=> 'Scrap'
+	];
+	protected array $fields = [];
+	protected array $sections = [];
+	protected array $statuses = [];
+	protected array $allowedStatuses = [
+		'all'	=> [
+			'label'	=> 'Everything',
+			'icon'	=> 'infinity'
+		],
+		'publish'	=> [
+			'label'	=> 'Visible',
+			'icon'	=> 'eye',
+		],
+		'draft'	=> [
+			'label'	=> 'Hidden',
+			'icon'	=> 'eye-slash'
+		],
+		'trash'	=> [
+			'label'	=> 'Deleted',
+			'icon'	=> 'trash'
+		],
+
+		'future'	=> [
+			'label'	=> 'Upcoming',
+			'icon'	=> 'clock-clockwise',
+		],
+		'past'	=> [
+			'label'	=> 'Past',
+			'icon'	=> 'clock-counter-clockwise',
+		],
+		'repeat'	=> [
+			'label'	=> 'Recurring',
+			'icon'	=> 'repeat',
+		]
+	];
+	protected array $defaultStatus = ['all', 'publish', 'draft', 'trash'];
+	protected array $defaultCalendarStatus = ['all', 'future', 'past', 'repeat', 'draft', 'trash'];
+
+	protected ?array $uploaderConfig = null;
+
+	// Data
+	protected $dataSourceCallback = null;
+	protected array $templates = [];
+
+	// Metadata handling
+	protected ?MetaManager $meta = null;
+	protected ?MetaForm $form = null;
+
+	// UI Options
+	protected array $stuck = []; // Fields that stick when scrolling
+	protected bool $showHeader = true;
+	protected bool $showBulkControls = true;
+	protected bool $showFilters = true;
+	protected array $customDateRanges = [];
+	protected array $additionalClasses = [];
+
+	public function __construct() {
+		$this->user = wp_get_current_user();
+		$this->user_id = $this->user->ID;
+	}
+
+	/**
+	 * Set the title and optional description
+	 */
+	public function title(string $title, string $description = ''): self {
+		$this->title = $title;
+		$this->description = $description;
+		return $this;
+	}
+
+	/**
+	 * Set content type information
+	 */
+	public function content(string $type, string $singular, string $plural): self {
+		$this->dataType = $type;
+		$this->singular = $singular;
+		$this->plural = $plural;
+		return $this;
+	}
+
+	/**
+	 * Add a filter to the interface
+	 *
+	 * @param string $type Built-in types: 'status', 'date', 'author', or custom
+	 * @param mixed $config Configuration array or callable
+	 */
+	public function addFilter(string $type, $config = []): self {
+		if ($type === 'status' && empty($config)) {
+			$config = $this->getDefaultStatuses();
+		} elseif ($type === 'date' && empty($config)) {
+			$config = [
+				'label' => 'Date',
+				'icon' => 'calendar'
+			];
+		}
+
+		$this->filters[$type] = $config;
+		return $this;
+	}
+
+	/**
+	 * Add a date filter
+	 *
+	 * @param string $field The field to filter on (default: 'post_date')
+	 * @param ?array $ranges Available date ranges
+	 */
+	public function addDateFilter(string $field = 'post_date', ?array $ranges = null): self {
+		if ($ranges === null) {
+			$ranges = ['today' => 'Today', 'week' => 'This Week', 'this-month' => 'This Month', 'last-month' => 'Last Month', 'quarter' => 'The Last 3 Months', 'past-year' => 'the Last Year', 'custom' => 'Custom Range'];
+		}
+
+		$this->filters['date'] = [
+			'type' => 'date',
+			'field' => $field,
+			'ranges' => $ranges,
+			'label' => 'Date',
+			'icon' => 'calendar'
+		];
+
+		return $this;
+	}
+
+	public function addCustomDateRange(array $ranges):self {
+		$ranges = array_filter($ranges);
+		$ranges = array_filter($ranges, function($range) {
+			return is_string($range);
+		});
+		$this->customDateRanges = $ranges;
+
+		return $this;
+	}
+
+	/**
+	 * Add taxonomy filters
+	 *
+	 * @param array $taxonomies Array of taxonomy slugs to filter by
+	 * @param string|null $limit 'user' to limit to current user's terms, null for all
+	 */
+	public function addTaxonomyFilter(array $taxonomies, ?string $limit = null): self {
+		foreach($taxonomies as $taxonomy) {
+			$this->taxonomies[$taxonomy] = [
+				'type'	=> 'taxonomy',
+				'taxonomy'=> $taxonomy,
+				'limit'	=> $limit,
+				'label'	=> JVB_TAXONOMY[$taxonomy]['plural']??'',
+				'icon'	=> JVB_TAXONOMY[$taxonomy]['icon']??''
+			];
+		}
+
+		return $this;
+	}
+
+	public function addSearch():self
+	{
+		$this->hasSearch = true;
+		return $this;
+	}
+
+	/**
+	 * Add a view type (grid, table, list, timeline)
+	 */
+	public function addViews(?array $views):self
+	{
+		if (!$views) {
+			$views = $this->defaultViews;
+		}
+		$this->views = $views;
+		return $this;
+	}
+
+	/**
+	 * Set the default view
+	 */
+	public function defaultView(string $type): self {
+		$this->defaultView = $type;
+		return $this;
+	}
+
+	/*****************************************************
+	 * ITEM ACTIONS
+	*****************************************************/
+	public function addItemActions(array $actions = ['edit', 'trash']):self
+	{
+		if(!empty($actions)) {
+			$this->itemActions = $actions;
+		}
+		return $this;
+	}
+	public function defineItemAction(string $action, array $definition):self
+	{
+		$config = array_key_exists($action, $this->defaultItemActions) ? $this->defaultItemActions[$action] : [];
+		$config = array_merge($config, $definition);
+		$this->defaultItemActions[$action] = $config;
+
+		return $this;
+	}
+	/**
+	 * Configure the uploader
+	 */
+	public function addUploader(array $config): self {
+		$this->uploaderConfig = array_merge([
+			'type' => 'upload',
+			'subtype' => 'image',
+			'mode' => 'selection',
+			'multiple' => true,
+			'label' => 'Upload Files',
+		], $config);
+		return $this;
+	}
+
+	public function useCRUDjs(bool $use = true):self
+	{
+		$this->useCRUDjs = false;
+		return $this;
+	}
+
+	public function setCalendar():self
+	{
+		$this->isCalendar = true;
+		return $this;
+	}
+
+	public function setDefaultStatus():self
+	{
+		if ($this->isCalendar) {
+			$this->statuses = $this->defaultCalendarStatus;
+		}else {
+			$this->statuses = $this->defaultStatus;
+		}
+
+		return $this;
+	}
+	/**************************************************
+	 * TIMELINE SHORTCUTS
+	**************************************************/
+	public function setTimeline():self
+	{
+		$this->isTimeline = true;
+
+		return $this;
+	}
+
+	public function maybeSetupTimeline():void
+	{
+		if (!$this->isTimeline) {
+			return;
+		}
+
+		$this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
+			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+				return true;
+			}
+			return false;
+		}));
+		array_unshift($this->timelineSharedFields, 'post_thumbnail');
+		array_unshift($this->timelineSharedFields, 'post_title');
+		array_unshift($this->timelineSharedFields, 'post_status');
+
+		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
+			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
+				return true;
+			}
+			return false;
+		}));
+
+		$all = array_merge($this->timelineUniqueFields, $this->timelineSharedFields);
+		$this->nonTimelineFields = array_filter($this->fields, function ($field) use ($all) {
+			return !in_array($field, $all);
+		}, ARRAY_FILTER_USE_KEY);
+	}
+	/**************************************************
+	 * CAPABILITIES
+	 * Changes output depends on capabilities.
+	 * View 	-> only lists data
+	 * Edit 	-> can edit data
+	 * Create 	-> can create data
+	 * delete 	-> can delete data
+	 *************************************************/
+	public function addCapabilities(?array $capabilities = null):self
+	{
+		if (!$capabilities) {
+			$capabilities = $this->allowedCaps;
+		}
+		$capabilities = array_filter($capabilities, function ($cap) {
+			return in_array($cap, $this->allowedCaps);
+		});
+		$this->caps = $capabilities;
+		return $this;
+	}
+	public function userCanPublish(bool $can = false):self
+	{
+		$this->userCanPublish = $can;
+		return $this;
+	}
+	/**************************************************
+	 * BULK ACTIONS
+	 * addBulkActions()						-> adds default bulk actions
+	 * addBulkActions(['edit','delete']) 	-> adds edit/delete
+	 * setActionLabel('edit', 'Modify') 	-> change the edit action's label to 'Modify'
+	 *************************************************/
+	public function addBulkActions(?array $actions = null):self
+	{
+		if ($actions === null) {
+			$actions = array_keys($this->defaultBulkActions);
+		}
+		$actions = array_filter($actions, function($item) {
+			return in_array($item, $this->allowedBulkActions);
+		});
+		$temp =[];
+		foreach ($actions as $action) {
+			$temp[$action] = $this->defaultBulkActions[$action];
+		}
+		$this->bulkActions = $temp;
+		return $this;
+	}
+	public function setActionLabel(string $key, string $label): self {
+		if (array_key_exists($key, $this->bulkActions)) {
+			$this->bulkActions[$key] = $label;
+		}
+		return $this;
+	}
+
+	/**
+	 * Add a single field
+	 */
+	public function addField(string $name, array $config): self {
+		$this->fields[$name] = $config;
+		return $this;
+	}
+
+	/**
+	 * Set all fields at once
+	 */
+	public function setFields(array $fields): self {
+		$this->fields = $fields;
+		$this->maybeSetupTimeline();
+		return $this;
+	}
+
+	/**
+	 * Add a section for organizing fields
+	 */
+	public function addSection(string $id, array $config): self {
+		$this->sections[$id] = $config;
+		return $this;
+	}
+
+	/**
+	 * Set custom statuses
+	 */
+	public function setStatuses(array $statuses): self {
+		$this->statuses = $statuses;
+		return $this;
+	}
+
+
+	/**
+	 * Mark fields that should stick when scrolling
+	 */
+	public function stickFields(array $fieldNames): self {
+		$this->stuck = array_merge($this->stuck, $fieldNames);
+		return $this;
+	}
+
+	/**
+	 * Set the data source callback
+	 * Callback should accept filters and return array of items
+	 */
+	public function dataSource(callable $callback): self {
+		$this->dataSourceCallback = $callback;
+		return $this;
+	}
+
+	/**
+	 * Add a custom template
+	 */
+	public function addTemplate(string $name, string $template): self {
+		$this->templates[$name] = $template;
+		return $this;
+	}
+
+	/**
+	 * Add CSS classes to the wrapper
+	 */
+	public function addClasses(array $classes): self {
+		$this->additionalClasses = array_merge($this->additionalClasses, $classes);
+		return $this;
+	}
+
+	/**
+	 * Toggle UI elements
+	 */
+	public function showHeader(bool $show = true): self {
+		$this->showHeader = $show;
+		return $this;
+	}
+
+	public function showBulkControls(bool $show = true): self {
+		$this->showBulkControls = $show;
+		return $this;
+	}
+
+	public function showFilters(bool $show = true): self {
+		$this->showFilters = $show;
+		return $this;
+	}
+
+	/**
+	 * Initialize meta handling
+	 */
+	public function initMeta(string $objectType = 'post', ?string $content = null): self {
+		$this->meta = new MetaManager(null, $objectType, $content ?? $this->dataType);
+		$this->form = new MetaForm();
+		return $this;
+	}
+
+	/**
+	 * Build the configuration array
+	 */
+	public function build(): array {
+		return [
+			'title' => $this->title,
+			'description' => $this->description,
+			'dataType' => $this->dataType,
+			'singular' => $this->singular,
+			'plural' => $this->plural,
+			'filters' => $this->filters,
+			'views' => $this->views,
+			'defaultView' => $this->defaultView,
+			'bulkActions' => $this->bulkActions,
+			'fields' => $this->fields,
+			'sections' => $this->sections,
+			'statuses' => $this->statuses,
+			'uploaderConfig' => $this->uploaderConfig,
+			'stuck' => $this->stuck,
+			'showHeader' => $this->showHeader,
+			'showBulkControls' => $this->showBulkControls,
+			'showFilters' => $this->showFilters,
+			'additionalClasses' => $this->additionalClasses,
+		];
+	}
+
+	/**
+	 * Render the CRUD interface
+	 */
+	public function render(): void {
+		$config = $this->build();
+		$classes = array_merge(['dashboard-page', $this->dataType], $this->additionalClasses);
+
+		ob_start();
+		?>
+		<div class="<?= esc_attr(implode(' ', $classes)) ?>" data-type="<?= esc_attr($this->dataType) ?>">
+			<?php
+			if ($this->showHeader) {
+				$this->renderHeader();
+			}
+			$this->renderContent();
+			$this->renderModals();
+			$this->renderTemplates();
+			?>
+		</div>
+		<?php
+		echo ob_get_clean();
+	}
+
+	/**
+	 * Render the header section
+	 */
+	protected function renderHeader(): void {
+		?>
+		<h1><?= esc_html($this->title) ?></h1>
+		<?php
+		if (!empty($this->description)) {
+			?>
+			<p class="page-description"><?= esc_html($this->description) ?></p>
+			<?php
+		}
+
+		if ($this->uploaderConfig) {
+			$this->renderUploader();
+		}
+
+		do_action('jvb_crud_after_header', $this->dataType, $this);
+	}
+
+	/**
+	 * Render uploader section
+	 */
+	protected function renderUploader(): void {
+		if (!$this->meta) {
+			return;
+		}
+		?>
+		<details open class="uploader">
+			<summary class="row btw"><?= esc_html($this->uploaderConfig['label'] ?? 'Upload Files') ?></summary>
+			<?php
+			$this->meta->render(
+				'form',
+				'new_' . $this->dataType,
+				$this->uploaderConfig
+			);
+			?>
+		</details>
+		<?php
+	}
+
+	/**
+	 * Render the main content area
+	 */
+	protected function renderContent(): void {
+		$dataIgnore = $this->useCRUDjs ? '' : ' data-ignore';
+		?>
+		<section class="items-list <?= esc_attr($this->dataType) ?> crud" data-content="<?= esc_attr($this->dataType) ?>" data-view="<?= $this->defaultView?>"<?=$dataIgnore?>>
+			<?php
+			$this->renderControlsAndFilters();
+
+			if ($this->showBulkControls) {
+				$this->renderBulkActions();
+			}
+			?>
+
+			<div class="<?= esc_attr($this->dataType) ?> item-grid" role="grid"></div>
+			<div class="scroll-sentinel" aria-hidden="true"></div>
+		</section>
+		<?php
+	}
+
+	/**
+	 * Render filters
+	 */
+	protected function renderControlsAndFilters(): void {
+		if (!$this->showFilters) {
+			return;
+		}
+		?>
+		<div class="all-filters col start" data-ignore>
+			<?php
+
+			$this->renderSearch();
+			$this->renderViewControls();
+			$this->renderStatusControls();
+			$this->renderOrderControls();
+			$this->renderFilters();
+			if (in_array('table', $this->views)) {
+				$this->renderColumnSelector();
+			}
+			?>
+		</div>
+		<?php
+	}
+
+	protected function renderSearch():void
+	{
+		if (!$this->hasSearch){
+			return;
+		}
+		?>
+		<div class="search row start nowrap">
+			<span class="label">Search:</span>
+			<?= jvbSearch() ?>
+		</div>
+		<?php
+	}
+
+	protected function renderViewControls():void
+	{
+		if (empty($this->views) || count($this->views) === 1){
+			return;
+		}
+		?>
+		<div class="radio-options view row">
+			<span class="label">View:</span>
+			<?php
+			$views = [
+				'grid'	=> ['icon' => 'squares-four', 'label' => 'Grid View'],
+				'list'	=> ['icon' => 'rows', 'label' => 'List View'],
+				'table'	=> ['icon' => 'table', 'label' => 'Table View'],
+			];
+			foreach ($this->views as $index => $view) {
+				$first = $index === 0;
+				?>
+				<input type="radio"
+				   data-view="<?=$view?>"
+				   value="<?=$view?>"
+				   class="btn"
+				   name="view"
+				   id="view-<?=$view?>"
+				   <?= $first ? ' checked':''?>>
+				<label for="view-<?=$view?>"
+				   title="<?=$views[$view]['label']?>">
+					<?= jvbDashIcon($views[$view]['icon']) ?>
+					<span class="screen-reader-text"><?=$views[$view]['label']?></span>
+				</label>
+				<?php
+			}
+			?>
+		</div>
+		<?php
+	}
+
+	protected function renderStatusControls():void
+	{
+		if (empty($this->statuses) || count($this->statuses) === 1) {
+			return;
+		}
+		?>
+		<div class="radio-options status row">
+			<span class="label">Status:</span>
+			<?php
+			$i = 1;
+			foreach ($this->statuses as $status) {
+				if (!array_key_exists($status, $this->allowedStatuses)) {
+					continue;
+				}
+				$config = $this->allowedStatuses[$status];
+
+				$checked = ($i == 1) ? ' checked' : '';
+				?>
+				<input type="radio" class="btn" data-filter="status" value="<?=$status?>" name="status" id="<?=$status?>"<?=$checked?>>
+				<label for="<?=$status?>" title="<?=$config['label']?>">
+					<?= jvbDashIcon($config['icon']) ?>
+				</label>
+				<?php
+				$i++;
+			}
+			?>
+		</div>
+		<?php
+	}
+
+	protected function renderOrderControls():void
+	{
+		?>
+		<div class="radio-options order row btw w-full">
+			<?php
+			$order = [
+				'orderby' => [
+					'date' => 'Order by date created',
+					'alphabetical' => 'Order alphabetically'
+				],
+				'order' => [
+					'sort-ascending' => 'In ascending order (Z-A, oldest to newest)',
+					'sort-descending' => 'In descending order (A-Z, newest to oldest)'
+				]
+			];
+
+			foreach ($order as $o => $option) {
+				?>
+				<div class="row start">
+					<span class="label"><?= ucfirst($o)?>:</span>
+					<?php
+					$i = 0;
+					foreach ($option as $opt => $label) {
+						$icon = $opt === 'date' ? 'calendar' : $opt;
+						?>
+						<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
+
+						<label for="<?=$opt?>" title="<?=$label?>"><?=jvbDashIcon($icon)?></label>
+						<?php
+						$i++;
+					}
+					?>
+				</div>
+				<?php
+			}
+			?>
+		</div>
+		<?php
+	}
+
+	protected function renderFilters(): void {
+		if (!$this->showFilters || empty($this->filters)) {
+			return;
+		}
+		?>
+		<div class="filters row start">
+			<span class="label">Filters:</span>
+			<?php
+			foreach ($this->filters as $key => $config) {
+				$type = $config['type'] ?? $key;
+
+				switch ($type) {
+					case 'date':
+						$this->renderDateFilter($config);
+						break;
+
+					default:
+						// Custom filter - allow override
+						do_action('jvb_crud_render_filter_' . $type, $config, $this);
+						break;
+				}
+			}
+			foreach ($this->taxonomies as $config) {
+				$this->renderTaxonomyFilter($config);
+			}
+			?>
+
+			<button type="button" class="clear-filters row" hidden>
+				<?= jvbDashIcon('x', ['title' => 'Clear Filters']) ?>
+				Clear All Filters
+			</button>
+		</div>
+		<?php
+	}
+
+	protected function renderDateFilter(array $config): void {
+		$field = $config['field'] ?? 'post_date';
+		$ranges = $config['ranges'] ?? [];
+		$label = $config['label'] ?? 'Date';
+		$icon = $config['icon'] ?? 'calendar';
+		?>
+		<div class="row nowrap">
+			<label for="filter-date"><?= jvbDashIcon($icon) ?> <span class="screen-reader-text">By <?= esc_html($label) ?>:</span></label>
+			<select id="filter-date" name="date-filter" data-filter="date">
+				<option value="">All Time</option>
+				<?php foreach ($ranges as $range => $rangeLabel):
+					?>
+					<option value="<?= esc_attr($range) ?>"><?= esc_html($rangeLabel) ?></option>
+				<?php endforeach; ?>
+			</select>
+
+			<?php
+			if (array_key_exists('custom', $ranges) && !empty($this->customDateRanges)) {
+				ob_start();
+				?>
+				<div class="custom-range row">
+					<label for="date-start" class="col">
+						From
+					</label>
+					<input type="date" id="date-start" class="date-start">
+					<label for="date-end" class="col">
+					   To
+					</label>
+					<input type="date" id="date-end" class="date-end">
+				</div>
+				<div class="month-picker">
+					<label>
+						<span>Or select month</span>
+						<select class="month-select">
+							<?php
+								foreach ($this->customDateRanges as $name=> $label) {
+									echo sprintf(
+										'<option value="%s">%s</option>',
+										$name,
+										$label
+									);
+								}
+							?>
+						</select>
+					</label>
+				</div>
+				<?php
+				$form = ob_get_clean();
+				echo jvbNewModal(
+					'date-range',
+				'Filter Results by Date:',
+					$form
+				);
+			}
+			?>
+		</div>
+		<?php
+	}
+
+	protected function renderTaxonomyFilter(array $config): void {
+		$taxonomy = $config['taxonomy']??false;
+
+		if (!$taxonomy) {
+			return;
+		}
+		$limit = $config['limit'] ?? null;
+		$icon = $config['icon'] ?? 'folder';
+		$terms = $this->getCommonTerms($taxonomy, $limit);
+		$label = $config['label'] ?? 'Categories';
+		$out = '';
+		if (!empty($terms)) {
+			$out .= sprintf(
+				'<div class="row nowrap"><label for="filter-%s">%s<span class="screen-reader-text">Filter by %s</span></label>
+                <select id="filter-%s" class="filter %s" name="%s" data-filter="taxonomies" data-taxonomy="%s">
+                <option value="">by %s</option>',
+				$taxonomy,
+				jvbDashIcon($icon, ['title'    => $label]),
+				$label,
+				$taxonomy,
+				$taxonomy,
+				$taxonomy,
+				$taxonomy,
+				$label
+			);
+
+
+			foreach ($terms as $term) {
+				$out .= sprintf(
+					'<option value="%s">%s</option>',
+					esc_attr($term['term_id']),
+					esc_html($term['name'])
+				);
+			}
+			$out .= '</select></div>';
+		}
+		echo $out;
+	}
+
+	/**
+	 * Get common terms for taxonomy
+	 * @param string $taxonomy
+	 * @return array
+	 */
+	protected function getCommonTerms(string $taxonomy, ?string $limit = null):array {
+		if ($limit) {
+			if ($limit === 'user') {
+				$manager = new UserTermsManager();
+				return $manager->getUserTerms($this->user_id, $taxonomy);
+			} else {
+				$limit = (int)$limit;
+			}
+		}
+
+		$args = [
+			'taxonomy'		=> jvbCheckBase($taxonomy),
+			'hide_empty'	=> true,
+			'orderby'		=> 'name',
+		];
+		if ($limit) {
+			$args['number'] = $limit;
+		}
+		return get_terms($args);
+	}
+
+	protected function renderColumnSelector():void {
+		ob_start();
+		?>
+		<details class="multi-select" title="Select columns" hidden>
+			<summary class="row start nowrap">
+				<?= jvbDashIcon('columns') ?>
+				<span class="labels">Toggle Columns</span>
+			</summary>
+			<div class="column-list">
+				<?php foreach ($this->fields as $fieldName => $config):
+					if (array_key_exists('hidden', $config)){
+						continue;
+					}
+					?>
+					<input type="checkbox"
+						   id="show-<?= esc_attr($fieldName) ?>"
+						   class="column-toggle ch"
+						   name="show-<?= esc_attr($fieldName) ?>"
+						   checked>
+					<label for="show-<?= esc_attr($fieldName) ?>">
+						<?= esc_html($config['label']) ?>
+					</label>
+				<?php endforeach; ?>
+			</div>
+		</details>
+		<?php
+		echo ob_get_clean();
+	}
+
+	/**
+	 * Render bulk controls
+	 */
+	protected function renderBulkActions(): void {
+		if (empty($this->bulkActions)) {
+			return;
+		}
+		?>
+		<div class="bulk-controls row nowrap btw">
+			<div class="bulk-select">
+				<input type="checkbox" id="select-all" class="select-all">
+				<label for="select-all" class="row"><span>Select All</span><span class="selected-count" hidden></span></label>
+			</div>
+			<div class="bulk-actions row nowrap" hidden>
+				<label for="bulk-action-select" class="screen-reader-text">
+					Select what to do with this selection.
+				</label>
+				<select class="bulk-action-select" id="bulk-action-select">
+
+				</select>
+			</div>
+		</div>
+
+		<template class="notTrashOptions">
+			<select class="wrap">
+				<option value="">Bulk Actions...</option>
+				<?php
+				foreach ($this->bulkActions as $control => $label) {
+					$disabled = ($control === 'publish' && !$this->userCanPublish) ? ' disabled' : '';
+					?>
+					<option value="<?=$control?>"<?=$disabled?>><?=$label?></option>
+					<?php
+				}
+				foreach ($this->taxonomies as $taxonomy => $config) {
+					?>
+					<option value="tax-<?=$taxonomy?>">Add to <?= JVB_TAXONOMY[$taxonomy]['singular']??$config['label'] ?></option>
+					<?php
+				}
+				?>
+			</select>
+
+		</template>
+		<template class="trashOptions">
+			<select class="wrap">
+				<option value="">Bulk Actions...</option>
+				<option value="restore">Restore</option>
+				<option value="delete">Permanently Delete</option>
+			</select>
+		</template>
+		<?php
+	}
+
+	/**
+	 * Render modals (can be overridden)
+	 */
+	protected function renderModals(): void {
+		foreach ($this->caps as $cap) {
+			switch ($cap) {
+				case 'create':
+					$this->renderCreateModal();
+					break;
+				case 'edit':
+					$this->renderEditModal();
+					if (!empty($this->bulkActions)) {
+						$this->renderBulkEditModal();
+					}
+					break;
+			}
+		}
+		do_action('jvb_crud_render_modals', $this->dataType, $this);
+	}
+
+	/**
+	 * Render templates (can be overridden)
+	 */
+	protected function renderTemplates(): void
+	{
+		$templates = $this->templates;
+		foreach ($this->views as $view) {
+			if (array_key_exists($view, $templates)) {
+				echo $templates[$view];
+				unset($templates[$view]);
+			} else {
+				switch ($view) {
+					case 'table':
+						$this->renderTableTemplate();
+						$this->renderTableRowTemplate();
+						break;
+					case 'grid':
+						$this->renderGridTemplate();
+						break;
+					case 'list':
+						$this->renderListTemplate();
+						break;
+				}
+			}
+		}
+		if ($this->isTimeline && !array_key_exists('timeline', $templates)) {
+			$temp = array_filter($this->fields, function ($field) {
+				return in_array($field, $this->timelineUniqueFields);
+			}, ARRAY_FILTER_USE_KEY);
+			$form = new MetaForm();
+			echo '<template class="timelineItem">';
+			$form->renderImagePreview(null,['fields' => $temp]);
+			echo '</template>';
+		}
+		if (!array_key_exists('empty', $templates)) {
+			$state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->dataType);
+			echo '<template class="emptyState">' . $state . '</template>';
+		}
+		if (!array_key_exists('galleryPreview', $templates)) {
+			$this->renderGalleryPreviewTemplate();
+		}
+		foreach ($templates as $name => $template) {
+			echo $template;
+		}
+		do_action('jvb_crud_render_templates', $this->dataType, $this);
+	}
+
+	protected function renderEmptyStateTemplate():void
+	{
+		$state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->dataType);
+		echo '<template class="emptyState">'.$state.'</template>';
+	}
+	protected function renderEmptyState():string
+	{
+		ob_start();
+		?>
+		<div class="empty-state">
+			<h3><?=jvbDashIcon($this->icon)?>Nothing here<?=jvbDashIcon($this->icon)?></h3>
+			<p>It doesn't look like you have any <?=$this->plural ?> yet.</p>
+			<p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbDashIcon('plus-square')?>" button to add one at a time.</small></p>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+	protected function renderGalleryPreviewTemplate():void
+	{
+		echo '<template class="galleryPreview">
+			<div class="preview-item" draggable="true">
+				<img \>
+				<div class="upload-status">
+					<div class="upload-progress"></div>
+				</div>
+				<button type="button" class="remove-preview" title="Remove Image">'.jvbIcon('trash').'</button>
+				<button type="button" class="move-image" title="Reorder Image">'.jvbIcon('dots-six-vertical').'</button>
+			</div>
+		</template>';
+	}
+
+	protected function renderItemSelect():string
+	{
+		ob_start();
+		?>
+		<div class="item-select">
+			<input type="checkbox" class="select-item">
+			<label class="select-item-label">
+				<span class="screen-reader-text">Select this <?= $this->singular ?></span>
+			</label>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+	protected function renderImage():string
+	{
+		return '<img loading="lazy" alt="">';
+	}
+
+	protected function renderItemActions():string
+	{
+		if (empty($this->itemActions)) {
+			return '';
+		}
+		ob_start();
+		?>
+		<div class="item-actions">
+			<?php
+			foreach ($this->itemActions as $action) {
+				$config = $this->defaultItemActions[$action];
+				$title = (array_key_exists('title', $config)) ? ' title="'.$config['title'].' '.$this->singular.'"' : '';
+				$icon = (array_key_exists('icon', $config)) ? jvbIcon($config['icon']) : '';
+				?>
+				<button type ="button" data-action="<?=$action?>"<?=$title?>>
+					<?=$icon?>
+					<span class="screen-reader-text"><?=$title?></span>
+				</button>
+				<?php
+			}
+			?>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+	protected function renderGridTemplate():void
+	{
+		?>
+		<template class="gridView">
+			<div class="item <?= $this->dataType ?>">
+				<input type="checkbox" class="select-item" name="select-item">
+				<label title="Select this <?= $this->singular?>" class="select-item-label">
+					<?= $this->renderImage() ?>
+				</label>
+				<?= $this->renderItemActions(); ?>
+			</div>
+		</template>
+		<?php
+	}
+
+	protected function renderListTemplate():void
+	{
+		?>
+		<template class="listView">
+			<div class="item <?=esc_attr($this->dataType)?> row nowrap">
+				<?= $this->renderItemSelect()?>
+				<?=$this->renderImage() ?>
+				<div class="col start w-full">
+					<?= $this->renderItemActions()?>
+					<h3 data-field="post_title"></h3>
+					<p data-attr="date"></p>
+					<p data-field="price"></p>
+					<div data-field="post_excerpt"></div>
+				</div>
+			</div>
+		</template>
+		<?php
+	}
+
+	protected function renderTableTemplate():void
+	{
+		if ($this->isTimeline) {
+			$this->renderTimelineTableView();
+			return;
+		}
+		$permissions = '';
+		foreach ($this->caps as $cap) {
+			$permissions .= ' data-'.$cap;
+		}
+		?>
+		<template class="contentTable">
+			<form class="table"
+				data-save="content"
+				data-content="<?= esc_attr($this->dataType) ?>"
+				data-form-id="content-table-<?= esc_attr($this->dataType) ?>"
+				<?= $permissions?>>
+
+				<?= jvbFormStatus() ?>
+				<?= $this->renderTableActions() ?>
+
+				<table>
+					<thead>
+					<?= $this->renderTableHeader() ?>
+					</thead>
+					<tbody>
+					<!-- Rows will be inserted here -->
+					</tbody>
+					<tfoot>
+					<?= $this->renderTableHeader() ?>
+					</tfoot>
+				</table>
+			</form>
+		</template>
+		<?php
+	}
+	/**
+	 * Render table row template
+	 */
+	protected function renderTableRowTemplate(): void {
+		if ($this->isTimeline) {
+			$this->renderTimelineTableGroup();
+			return;
+		}
+		?>
+		<template class="tableView">
+			<tr class="item">
+				<td class="select">
+					<?= $this->renderItemSelect() ?>
+				</td>
+				<?php
+					if (array_key_exists('status', $this->fields)){
+						?>
+						<td class="status" data-field="post_status">
+							<?= $this->renderStatusRadios() ?>
+						</td>
+						<?php
+					}
+				?>
+				<?php
+				$makeDetails = [
+					'group',
+					'repeater',
+					'checkbox',
+					'radio'
+				];
+				foreach ($this->fields as $name => $config):
+					if (array_key_exists('hidden', $config) || $name === 'status'){
+						continue;
+					}
+					$makeThisDetailed = (in_array($config['type'], $makeDetails));
+					?>
+					<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+						<?php
+						if (in_array('edit', $this->caps)) {
+							echo $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '';
+							echo $this->meta->render('form', $name, $config);
+							echo $makeThisDetailed ? '</details>' : '';
+						} else {
+							echo '<p></p>';
+						}
+						?>
+					</td>
+				<?php endforeach;
+				if (!empty($this->itemActions)) {
+					?>
+					<td class="field show-actions">
+						<?= $this->renderItemActions(); ?>
+					</td>
+					<?php
+				}
+				?>
+
+			</tr>
+		</template>
+		<?php
+	}
+
+	protected function renderTimelineTableView():void
+	{
+		?>
+		<template class="contentTable">
+			<form class="table"
+				  data-save="content"
+				  data-content="<?= esc_attr($this->dataType) ?>"
+				  data-form-id="content-table-<?= esc_attr($this->dataType) ?>">
+				<?= jvbFormStatus() ?>
+				<?= $this->renderTableActions() ?>
+
+				<table>
+					<thead>
+					<?= $this->renderTimelineTableHeader() ?>
+					</thead>
+					<!-- Rows are inserted as tbody groups -->
+					<tfoot>
+					<?= $this->renderTimelineTableHeader() ?>
+					</tfoot>
+				</table>
+			</form>
+		</template>
+		<?php
+	}
+
+	protected function renderTimelineTableGroup():void
+	{
+		$makeDetails = [
+			'group',
+			'repeater',
+			'checkbox',
+			'radio'
+		];
+		?>
+		<template class="tableView">
+			<tbody class="item">
+			<tr class="shared">
+				<td class="select">
+					<?= $this->renderItemSelect() ?>
+				</td>
+				<td class="show-post_status field" data-field="post_status">
+					<?= $this->renderStatusRadios() ?>
+				</td>
+				<?php
+				foreach ($this->fields as $name => $config) {
+					if(array_key_exists('hidden', $config) || $name === 'post_status') {
+						continue;
+					}
+					if (!in_array($name, $this->timelineSharedFields)) {
+						echo '<td></td>';
+						continue;
+					}
+					$makeThisDetailed = (in_array($config['type'], $makeDetails));
+					?>
+					<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+						<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+						<?php $this->meta->render('form', $name, $config); ?>
+						<?= $makeThisDetailed ? '</details>' : '' ?>
+					</td>
+					<?php
+				}
+
+				?>
+			</tr>
+			<tr class="timeline-point">
+				<td class="select">
+					<button class="drag-handle" title="Drag to reorder" aria-label="Drag to reorder this timeline point"><?= jvbDashIcon('dots-six') ?></button>
+				</td>
+				<td class="show-post_status field" data-field="post_status">
+					<?= $this->renderStatusRadios() ?>
+				</td>
+				<?php
+				foreach ($this->fields as $name => $config) {
+					if(array_key_exists('hidden', $config) || $name === 'post_status') {
+						continue;
+					}
+					if (!in_array($name, $this->timelineUniqueFields)) {
+						echo '<td></td>';
+						continue;
+					}
+					$makeThisDetailed = (in_array($config['type'], $makeDetails));
+					?>
+					<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+						<?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+						<?php $this->meta->render('form', $name, $config); ?>
+						<?= $makeThisDetailed ? '</details>' : '' ?>
+					</td>
+					<?php
+				}
+				?>
+			</tr>
+			</tbody>
+		</template>
+		<?php
+	}
+
+	/**
+	 * Render table header
+	 */
+	protected function renderTableHeader(): string {
+		ob_start();
+
+		?>
+		<tr>
+			<th scope="col" class="select-header">
+				<input type="checkbox" id="select-all" name="select-all">
+				<label for="select-all">All</label>
+			</th>
+			<?php if (array_key_exists('status', $this->fields)) { ?>
+				<th scope="col" class="status-header">Status</th>
+			<?php } ?>
+			<?php foreach ($this->fields as $name => $config):
+				if (array_key_exists('hidden', $config) || $name === 'status'){
+					continue;
+				}
+				?>
+				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+					<?= esc_html($config['label']) ?>
+				</th>
+			<?php endforeach;
+			if (!empty($this->itemActions)) {
+				?>
+				<th scope="col" class="show-actions">
+					Actions
+				</th>
+				<?php
+			}
+			?>
+
+		</tr>
+		<?php
+		return ob_get_clean();
+	}
+
+	protected function renderTimelineTableHeader(): string {
+		ob_start();
+
+		?>
+		<tr>
+			<th scope="col" class="select-header">
+				<input type="checkbox" id="select-all" name="select-all">
+				<label for="select-all">All</label>
+			</th>
+			<th scope="col" class="show-post_status">
+				Status
+			</th>
+			<?php foreach ($this->fields as $name => $config):
+				if (array_key_exists('hidden', $config) || $name === 'post_status'){
+					continue;
+				}
+				?>
+				<th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+					<?= esc_html($config['label']) ?>
+				</th>
+			<?php endforeach; ?>
+		</tr>
+		<?php
+		return ob_get_clean();
+	}
+
+	/**
+	 * Render table action controls
+	 */
+	protected function renderTableActions(): string {
+		ob_start();
+		?>
+		<div class="table-actions row btw nowrap">
+			<?php if (count(array_intersect(['create', 'edit'], $this->caps)) > 0) { ?>
+				<?= jvbRenderToggleTextField(
+					'vertical',
+					'TAB NAV:',
+					'',
+					jvbDashIcon('caret-double-down'),
+					jvbDashIcon('caret-double-right')
+				) ?>
+				<button type="button" class="add-row" title="Add new row">
+					<?= jvbDashIcon('plus-square') ?>
+					<span>Add Row</span>
+				</button>
+			<?php } ?>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+	protected function renderStatusRadios(): string {
+		ob_start();
+		?>
+		<div class="radio-options status-options row">
+			<?php foreach ($this->statuses as $status):
+				if ($status === 'all') continue;
+				if (!in_array($status, $this->allowedStatuses)) continue;
+				$config = $this->allowedStatuses[$status];
+				?>
+
+				<input type="radio"
+					   name="post_status"
+					   id="status-<?= esc_attr($status) ?>"
+					   value="<?= esc_attr($status) ?>">
+				<label for="status-<?= esc_attr($status) ?>">
+					<?= jvbDashIcon($config['icon']) ?>
+					<span class="screen-reader-text"><?= esc_html($config['label']) ?></span>
+				</label>
+			<?php endforeach; ?>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+
+
+	/**
+	 * Get default statuses
+	 */
+	protected function getDefaultStatuses(): array {
+		return [
+			'all' => [
+				'icon' => 'infinity',
+				'label' => 'All',
+			],
+			'active' => [
+				'icon' => 'check-circle',
+				'label' => 'Active',
+			],
+			'inactive' => [
+				'icon' => 'x-circle',
+				'label' => 'Inactive',
+			],
+		];
+	}
+
+	/**
+	 * Get field configuration
+	 */
+	public function getFields(): array {
+		return $this->fields;
+	}
+
+	/**
+	 * Get configuration value
+	 */
+	public function get(string $key) {
+		return $this->$key ?? null;
+	}
+
+	/***************************************************
+	 * MODALS
+	***************************************************/
+	protected function renderCreateModal():void
+	{
+		echo jvbNewModal(
+			'create',
+			'Creating <span class="count"></span> New '.$this->singular,
+			str_replace('edit-form"', 'create-form" data-noautosave', $this->editForm())
+		);
+	}
+
+	protected function editForm():string
+	{
+		ob_start();
+		?>
+		<form class="edit-form" data-save="content" data-form-id="edit-<?=$this->dataType?>" data-autosave<?= ($this->isTimeline) ? ' data-timeline' : ''?>>
+			<?= jvbFormStatus() ?>
+			<input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
+			<input type="hidden" name="content" value="<?=$this->dataType?>" />
+			<div class="fields">
+				<div class="field-group radio-options row">
+					<span>Status:</span>
+					<?php
+					$this->getApplicableStatuses('edit');
+					?>
+				</div>
+				<?php if (!$this->userCanPublish) { ?>
+					<p class="description">Your account needs to be verified before you can publish content.</p>
+				<?php }
+
+				if (!empty($this->sections)) {
+					$tabs = [];
+					foreach ($this->sections as $slug => $config) {
+						$section = [];
+						if (array_key_exists('icon', $config)) {
+							$section = [
+								'icon'	=> $config['icon']
+							];
+						}
+						$tabs[$slug] = array_merge([
+							'title'	=> $config['label'],
+							'content' => '',
+							'description' => $config['description']??'',
+						], $section);
+						$icon = jvbSectionIcon($slug);
+						if ($icon !== '') {
+							$tabs[$slug]['icon'] = $icon;
+						}
+					}
+				} else {
+					$tabs = false;
+				}
+
+
+				$fields = $this->fields;
+				if (!$this->isTimeline) {
+					$first = ['post_thumbnail', 'post_title', 'price'];
+
+					foreach ($first as $f) {
+						if (array_key_exists($f, $fields)) {
+							if ($tabs) {
+								$tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true);
+							} else {
+								$this->meta->render('form', $f, $fields[$f]);
+							}
+
+							unset($fields[$f]);
+						}
+					}
+				}
+
+				if ($this->isTimeline) {
+					$temp = array_filter($fields, function ($field) {
+						return in_array($field, $this->timelineUniqueFields);
+					}, ARRAY_FILTER_USE_KEY);
+
+					$config = [
+						'type'		=> 'gallery',
+						'subtype'	=> 'timeline',
+						'data'		=> 'timeline',
+						'label'		=> 'Progression',
+						'fields'	=> $temp
+					];
+					$content = '';
+					foreach ($fields as $slug=> $field) {
+						if (in_array($slug, $this->timelineSharedFields)) {
+							$content .= $this->form->render($slug, null, $field, false, true);
+						}
+					}
+
+
+					$content .= $this->meta->render('form', 'timeline', $config, false,true);
+
+					$tabs['progression']['content'] = $content;
+					$fields = $this->nonTimelineFields;
+				}
+				foreach ($fields as $n => $config) {
+					if ($tabs) {
+						$section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
+						$tabs[$section]['content'] .= $this->meta->render('form', $n, $config, false, true);
+					} else {
+						$this->meta->render('form', $n, $config);
+					}
+				}
+
+				if ($tabs) {
+					jvbRenderTabs($tabs);
+				}
+				?>
+			</div>
+		</form>
+		<?php
+		return ob_get_clean();
+	}
+
+	protected function renderEditModal():void
+	{
+		echo jvbNewModal(
+			'edit',
+			'Edit your '.$this->singular,
+			$this->editForm()
+		);
+	}
+
+	protected function renderBulkEditModal():void
+	{
+		if (empty($this->bulkActions)) return;
+		ob_start();
+		?>
+		<form class="bulk-edit-form" data-save="content" data-form-id="bulk-edit-<?=$this->dataType?>">
+			<?= jvbFormStatus() ?>
+			<div class="selected"></div>
+			<p class="description">You can unselect items by clicking the image here.</p>
+			<p class="hint"><strong>IMPORTANT: </strong> Whatever changes you make here will be applied to all selected <?=$this->plural?>.</p>
+			<div class="fields">
+				<div class="field-group radio-options row">
+					<?php
+					$this->getApplicableStatuses('bulk-');
+					?>
+				</div>
+				<?php
+				if (!empty($this->taxonomies)) {
+					?>
+					<div class="taxonomies">
+						<?php
+						foreach ($this->taxonomies as $taxonomy => $config) {
+							$this->meta->render(
+								'form',
+								'bulk-edit-'.$taxonomy,
+								[
+									'type'		=> 'taxonomy',
+									'label'		=> $config['label'],
+									'taxonomy'	=> $taxonomy,
+									'createNew'	=> jvbUserIsVerified(),
+									'multiple'	=> true,
+									'mode'		=> 'append'
+								]
+							);
+						}
+						?>
+					</div>
+					<?php
+				}
+				$fields = $this->fields;
+				$fields = array_filter($fields, function ($field) {
+					return array_key_exists('bulkEdit', $field);
+				});
+				foreach ($fields as $fieldName => $config) {
+					$this->meta->render('form', $fieldName, $config);
+				}
+				?>
+			</div>
+		</form>
+		<template class="bulkItem">
+			<label>
+				<input type="checkbox">
+				<img>
+			</label>
+		</template>
+		<?php
+		$form = ob_get_clean();
+		echo jvbNewModal(
+			'bulkEdit',
+			'Bulk Edit <span class="selected"></span> '.$this->plural,
+			$form
+		);
+	}
+
+	protected function getApplicableStatuses(string $prefix) {
+		foreach ($this->statuses as $status) {
+			if ($status === 'all' || !in_array($status, $this->allowedStatuses)) {
+				continue;
+			}
+			$config = $this->allowedStatuses[$status];
+			if (in_array($status, ['future', 'past'])) {
+				if ($status === 'future') {
+					$status = 'publish';
+					$config = [
+						'icon'	=> 'eye',
+						'label'	=> 'Live',
+					];
+				} else {
+					continue;
+				}
+			}
+			$disabled = ($status === 'publish' && !$this->userCanPublish) ? ' disabled' : '';
+			?>
+			<input type ="radio"
+				   name="post_status"
+				   class="btn"
+				   value="<?= esc_attr($status)?>"
+				   id="<?=$prefix?>set-<?= esc_attr($status) ?>"
+				<?= $disabled?>>
+			<label for="<?=$prefix?>set-<?=esc_attr($status)?>">
+				<?= jvbDashIcon($config['icon'], ['title' => $config['label']]) ?>
+				<span><?= esc_html($config['label'])?></span>
+			</label>
+			<?php
+		}
+	}
+}
diff --git a/inc/ui/Modal.php b/inc/ui/Modal.php
new file mode 100644
index 0000000..33dd840
--- /dev/null
+++ b/inc/ui/Modal.php
@@ -0,0 +1,175 @@
+<?php
+namespace JVBase\ui;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Modal UI component with fluent interface
+ *
+ * Usage:
+ * $modal = new Modal('edit-profile');
+ * $modal->title('Edit Profile')
+ *       ->content($formHtml)
+ *       ->addAction('cancel', 'Cancel', 'x')
+ *       ->addAction('save', 'Save', 'floppy-disk', 'submit')
+ *       ->size('large');
+ * echo $modal->render();
+ */
+class Modal {
+	private string $class;
+	private string $title = '';
+	private string $content = '';
+	private array $actions = [];
+	private ?string $size = null;
+	private array $attributes = [];
+	private bool $defaultActions = true;
+
+	public function __construct(string $class) {
+		$this->class = $class;
+	}
+
+	/**
+	 * Set the modal title
+	 *
+	 * @param string $title
+	 * @return self
+	 */
+	public function title(string $title): self {
+		$this->title = $title;
+		return $this;
+	}
+
+	/**
+	 * Set the modal content
+	 *
+	 * @param string $content
+	 * @return self
+	 */
+	public function content(string $content): self {
+		$this->content = $content;
+		return $this;
+	}
+
+	/**
+	 * Add a custom action button
+	 *
+	 * @param string $class CSS class for the button
+	 * @param string $label Button label
+	 * @param string|null $icon Icon name (optional)
+	 * @param string $type Button type (button or submit)
+	 * @return self
+	 */
+	public function addAction(string $class, string $label, ?string $icon = null, string $type = 'button'): self {
+		$this->defaultActions = false;
+		$this->actions[] = [
+			'class' => $class,
+			'label' => $label,
+			'icon' => $icon,
+			'type' => $type
+		];
+		return $this;
+	}
+
+	/**
+	 * Use default cancel/save actions
+	 *
+	 * @param bool $use
+	 * @return self
+	 */
+	public function useDefaultActions(bool $use = true): self {
+		$this->defaultActions = $use;
+		return $this;
+	}
+
+	/**
+	 * Set modal size
+	 *
+	 * @param string $size Size class (e.g., 'small', 'large', 'full')
+	 * @return self
+	 */
+	public function size(string $size): self {
+		$this->size = $size;
+		return $this;
+	}
+
+	/**
+	 * Add custom attributes to the dialog element
+	 *
+	 * @param string $key Attribute name
+	 * @param string $value Attribute value
+	 * @return self
+	 */
+	public function attribute(string $key, string $value): self {
+		$this->attributes[$key] = $value;
+		return $this;
+	}
+
+	/**
+	 * Render the modal HTML
+	 *
+	 * @param bool $return Whether to return or echo
+	 * @return string
+	 */
+	public function render(bool $return = true): string {
+		$classes = $this->class;
+		if ($this->size) {
+			$classes .= ' ' . $this->size;
+		}
+
+		$attrs = '';
+		foreach ($this->attributes as $key => $value) {
+			$attrs .= ' ' . esc_attr($key) . '="' . esc_attr($value) . '"';
+		}
+
+		$html = '<dialog class="' . esc_attr($classes) . '"' . $attrs . '>
+			<div class="wrap">
+				<h2>' . esc_html($this->title) . '</h2>
+				' . $this->content;
+
+		// Add actions
+		if ($this->defaultActions || !empty($this->actions)) {
+			$html .= $this->renderActions();
+		}
+
+		$html .= '
+			</div>
+		</dialog>';
+
+		if ($return) {
+			return $html;
+		}
+
+		echo $html;
+		return $html;
+	}
+
+	/**
+	 * Render action buttons
+	 *
+	 * @return string
+	 */
+	private function renderActions(): string {
+		$html = '<div class="m-actions row">';
+
+		if ($this->defaultActions) {
+			// Default cancel and save buttons
+			$html .= '<button type="button" class="cancel">' . jvbIcon('x') . '<span class="screen-reader-text">Cancel</span></button>';
+			$html .= '<button type="submit" class="save">' . jvbIcon('floppy-disk') . '<span class="screen-reader-text">Save</span></button>';
+		} else {
+			// Custom actions
+			foreach ($this->actions as $action) {
+				$html .= '<button type="' . esc_attr($action['type']) . '" class="' . esc_attr($action['class']) . '">';
+				if ($action['icon']) {
+					$html .= jvbIcon($action['icon']);
+				}
+				$html .= '<span class="screen-reader-text">' . esc_html($action['label']) . '</span>';
+				$html .= '</button>';
+			}
+		}
+
+		$html .= '</div>';
+		return $html;
+	}
+}
diff --git a/inc/ui/Navigation.php b/inc/ui/Navigation.php
new file mode 100644
index 0000000..7988917
--- /dev/null
+++ b/inc/ui/Navigation.php
@@ -0,0 +1,326 @@
+<?php
+namespace JVBase\ui;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Menu/Navigation UI component with fluent interface
+ *
+ * Usage:
+ * $menu = new Menu('primary-nav');
+ * $menu->addItem()->text('Home')->url('/')->icon('house');
+ *
+ * $menu->addItem()->text('About')
+ *      ->url('/about/')
+ *      ->submenu(function($submenu) {
+ *          $submenu->addItem()->text('Team')->url('/about/team/');
+ *          $submenu->addItem()->text('History')->url('/about/history/');
+ *      });
+ *
+ * echo $menu->render();
+ */
+class Navigation {
+	private string $id;
+	private array $items = [];
+	private array $classes = [];
+	protected array $defaultMenuClasses = [];
+	private bool $isNav = true;
+	private bool $hasToggle = false;
+	protected array $defaultItemClasses = [];
+	private int $counter = 0;
+
+	public function __construct(string $id = '') {
+		$this->id = $id ?: 'menu-' . uniqid();
+	}
+
+	public function getID():string
+	{
+		return $this->id;
+	}
+
+
+	/**
+	 * Add a menu item
+	 *
+	 * @return MenuItem
+	 */
+	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;
+	}
+
+	/**
+	 * Add CSS class to the nav element
+	 *
+	 * @param string $class
+	 * @return self
+	 */
+	public function addClass(string $class): self {
+		$this->classes[] = $class;
+		return $this;
+	}
+	public function addMenuClass(string $class):self {
+		$this->menuClasses[] = $class;
+		return $this;
+	}
+
+	public function defaultMenuClasses(array $classes):self {
+		$classes = array_filter($classes, fn ($class) => is_string($class));
+		$this->defaultMenuClasses = $classes;
+		return $this;
+	}
+
+	public function defaultItemClasses(array $classes): self {
+		$classes = array_filter($classes, fn ($class) => is_string($class));
+		$this->defaultItemClasses = $classes;
+		return $this;
+	}
+
+	/**
+	 * Set whether this nav has a toggle button
+	 *
+	 * @param bool $hasToggle
+	 * @return self
+	 */
+	public function hasToggle(bool $hasToggle = true): self {
+		$this->hasToggle = $hasToggle;
+		return $this;
+	}
+
+
+	public function isNav(bool $isNav = true):self {
+		$this->isNav = $isNav;
+		return $this;
+	}
+
+	/**
+	 * Render the menu HTML
+	 *
+	 * @param bool $return Whether to return or echo
+	 * @return string
+	 */
+	public function render(bool $return = true): string {
+		if (empty($this->items)) {
+			return '';
+		}
+
+		$classStr = !empty($this->classes) ? ' class="' . esc_attr(implode(' ', array_merge([$this->id],$this->classes))) . '"' : '';
+
+		$html = '';
+		if ($this->isNav) {
+			$html = '<nav id="' . esc_attr($this->id).'"' . $classStr.'>';
+
+			if ($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>';
+			}
+		}
+		if (!$this->isNav) {
+			$classStr = (empty($this->defaultMenuClasses)) ? '' : ' class="'.implode(' ', $this->defaultMenuClasses).'"';
+		}
+
+		$html .= '<ul id="' . esc_attr($this->id) . '-list" '.$classStr.'>';
+
+		foreach ($this->items as $item) {
+			$html .= $item->render();
+		}
+
+		$html .= '</ul>';
+		if ($this->isNav) {
+			$html .= '</nav>';
+		}
+
+
+		if ($return) {
+			return $html;
+		}
+
+		echo $html;
+		return $html;
+	}
+}
+
+/**
+ * Individual menu item with support for submenus
+ */
+class MenuItem {
+	private int $id;
+	private string $text = '';
+	private ?string $url = null;
+	private ?string $icon = null;
+	private ?Navigation $submenu = null;
+	private array $classes = [];
+	private array $menuClasses = [];
+	private array $attributes = [];
+	private bool $current = false;
+
+	public function __construct(int $id) {
+		$this->id = $id;
+	}
+
+	/**
+	 * Set the menu item text
+	 *
+	 * @param string $text
+	 * @return self
+	 */
+	public function text(string $text): self {
+		$this->text = $text;
+		return $this;
+	}
+
+	/**
+	 * Set the menu item URL
+	 *
+	 * @param string $url
+	 * @return self
+	 */
+	public function url(string $url): self {
+		$this->url = $url;
+		return $this;
+	}
+
+	/**
+	 * Set the menu item icon
+	 *
+	 * @param string $icon
+	 * @return self
+	 */
+	public function icon(string $icon): self {
+		$this->icon = (str_starts_with($icon, '<i')) ? $icon : jvbIcon($icon);
+		return $this;
+	}
+
+	protected function renderClasses(array $classes):string {
+		return empty($classes) ? '' : ' class="'.implode(' ', array_filter($classes, fn($class) => is_string($class))).'"';
+	}
+
+	/**
+	 * Add a submenu
+	 *
+	 * @param ?string $id
+	 * @return Navigation
+	 */
+	public function submenu(?string $id = null): Navigation {
+		if (!$id) {
+			$id = 'submenu-' . uniqid();
+		}
+		$submenu = new Navigation($id);
+		$submenu->isNav(false);
+
+		if (!empty($this->defaultMenuClasses)) {
+			foreach ($this->defaultMenuClasses as $class) {
+				$submenu->addClass($class);
+			}
+		}
+		$this->submenu = $submenu;
+		return $submenu;
+	}
+
+	/**
+	 * Add CSS class to the list item
+	 *
+	 * @param string $class
+	 * @return self
+	 */
+	public function addClass(string $class): self {
+		$this->classes[] = $class;
+		return $this;
+	}
+
+	/**
+	 * Mark this item as current/active
+	 *
+	 * @param bool $current
+	 * @return self
+	 */
+	public function current(bool $current = true): self {
+		$this->current = $current;
+		if ($current) {
+			$this->addClass('current');
+		}
+		return $this;
+	}
+
+	/**
+	 * Add custom attribute to the link element
+	 *
+	 * @param string $key
+	 * @param string $value
+	 * @return self
+	 */
+	public function attribute(string $key, string $value): self {
+		$this->attributes[$key] = $value;
+		return $this;
+	}
+
+	/**
+	 * Render the menu item HTML
+	 *
+	 * @return string
+	 */
+	public function render(): string {
+		$classes = $this->classes;
+		if ($this->submenu) {
+			$classes[] = 'has-submenu';
+		}
+
+		$classStr = $this->renderClasses($classes);
+
+		$html = '<li' . $classStr . '>';
+		$html .= '<div class="row nowrap">';
+		// Render link or button
+		if ($this->url) {
+			$attrs = '';
+			foreach ($this->attributes as $key => $value) {
+				$attrs .= ' ' . esc_attr($key) . '="' . esc_attr($value) . '"';
+			}
+
+			$html .= '<a href="' . esc_url($this->url) . '"' . $attrs . '>';
+		} else {
+			$html .= '<span class="a">';
+		}
+
+		if ($this->icon) {
+			$html .= $this->icon;
+		}
+		$html .= '<span class="title">'.esc_html($this->text) . '</span>';
+
+
+		$html .= ($this->url) ? '</a>' : '</span>';
+
+		// Render submenu if exists
+		if ($this->submenu) {
+			$html .= '<button class="toggle"
+				data-action="toggle-submenu"
+				title="Toggle Submenu"
+				aria-label="Open '.$this->submenu->getID().' Submenu" aria-expanded="false" aria-controls="'.$this->submenu->getID().'">'.
+				jvbIcon('caret-down', ['title'=>'Toggle Submenu']).
+				'</button>';
+			$html .= '</div>';
+			$html .= $this->submenu->render();
+		}else {
+			$html .= '</div>';
+		}
+
+		$html .= '</li>';
+
+		return $html;
+	}
+}
diff --git a/inc/ui/Tabs.php b/inc/ui/Tabs.php
new file mode 100644
index 0000000..370ffdf
--- /dev/null
+++ b/inc/ui/Tabs.php
@@ -0,0 +1,210 @@
+<?php
+namespace JVBase\ui;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Tabs UI component with fluent interface
+ *
+ * Usage:
+ * $tabs = new Tabs();
+ * $tabs->addTab('tab-slug')
+ *      ->title('Tab Title')
+ *      ->icon('iconName')
+ *      ->description('Description text')
+ *      ->content($content);
+ * echo $tabs->render();
+ */
+class Tabs {
+	private array $tabs = [];
+	private int $counter = 0;
+
+	/**
+	 * Add a new tab and return it for chaining
+	 *
+	 * @param string $slug Unique identifier for the tab
+	 * @return Tab
+	 */
+	public function addTab(string $slug = ''): Tab {
+		if (empty($slug)) {
+			$slug = 'tab-' . ++$this->counter;
+		}
+
+		$tab = new Tab($slug);
+		$this->tabs[$slug] = $tab;
+		return $tab;
+	}
+
+	/**
+	 * Render all tabs as HTML
+	 *
+	 * @param bool $return Whether to return or echo the output
+	 * @return string
+	 */
+	public function render(bool $return = true): string {
+		if (empty($this->tabs)) {
+			return '';
+		}
+
+		$header = '<nav class="tabs row start" role="tablist">';
+		$content = '';
+		$i = 0;
+
+		foreach ($this->tabs as $slug => $tab) {
+			if (!$tab->hasContent()) {
+				error_log('No content for tab: ' . $slug);
+				continue;
+			}
+
+			// Header
+			$active = ($i === 0) ? ' active' : '';
+			$selected = ($i === 0) ? 'true' : 'false';
+			$hidden = $tab->isHidden() ? ' hidden' : '';
+
+			$header .= '<button type="button" class="button tab' . $active . '" data-tab="' . $slug . '" role="tab" aria-selected="' . $selected . '"' . $hidden . '>
+				<h2 class="row">';
+
+			if ($tab->getIcon()) {
+				$header .= jvbIcon($tab->getIcon());
+			}
+
+			$header .= $tab->getTitle() . '</h2>
+			</button>';
+
+			// Content
+			$ariaHidden = ($i === 0) ? 'false' : 'true';
+			$content .= '<div class="tab-content' . $active . '" data-tab="' . $slug . '" role="tabpanel" aria-hidden="' . $ariaHidden . '"';
+
+			if ($i !== 0) {
+				$content .= ' hidden';
+			}
+
+			$content .= '>
+				<h2>' . $tab->getTitle() . '</h2>';
+
+			// Description
+			if ($tab->getDescription()) {
+				$description = $tab->getDescription();
+				if (!is_array($description)) {
+					$content .= apply_filters('the_content', $description);
+				} else {
+					$content .= implode('', array_map(function ($paragraph) {
+						return apply_filters('the_content', $paragraph);
+					}, $description));
+				}
+			}
+
+			$content .= $tab->getContent() . '
+			</div>';
+			$i++;
+		}
+
+		$header .= '</nav>';
+		$out = $header . $content;
+
+		if ($return) {
+			return $out;
+		}
+
+		echo $out;
+		return $out;
+	}
+}
+
+/**
+ * Individual tab with fluent interface for configuration
+ */
+class Tab {
+	private string $slug;
+	private string $title = '';
+	private ?string $icon = null;
+	private string|array|null $description = null;
+	private string $content = '';
+	private bool $hidden = false;
+
+	public function __construct(string $slug) {
+		$this->slug = $slug;
+	}
+
+	/**
+	 * Set the tab title
+	 *
+	 * @param string $title
+	 * @return self
+	 */
+	public function title(string $title): self {
+		$this->title = $title;
+		return $this;
+	}
+
+	/**
+	 * Set the tab icon
+	 *
+	 * @param string $icon Icon name (used with jvbIcon())
+	 * @return self
+	 */
+	public function icon(string $icon): self {
+		$this->icon = $icon;
+		return $this;
+	}
+
+	/**
+	 * Set the tab description (can be string or array)
+	 *
+	 * @param string|array $description
+	 * @return self
+	 */
+	public function description(string|array $description): self {
+		$this->description = $description;
+		return $this;
+	}
+
+	/**
+	 * Set the tab content
+	 *
+	 * @param string $content
+	 * @return self
+	 */
+	public function content(string $content): self {
+		$this->content = $content;
+		return $this;
+	}
+
+	/**
+	 * Mark tab as hidden
+	 *
+	 * @param bool $hidden
+	 * @return self
+	 */
+	public function hidden(bool $hidden = true): self {
+		$this->hidden = $hidden;
+		return $this;
+	}
+
+	// Getters
+	public function getTitle(): string {
+		return $this->title;
+	}
+
+	public function getIcon(): ?string {
+		return $this->icon;
+	}
+
+	public function getDescription(): string|array|null {
+		return $this->description;
+	}
+
+	public function getContent(): string {
+		return $this->content;
+	}
+
+	public function isHidden(): bool {
+		return $this->hidden;
+	}
+
+	public function hasContent(): bool {
+		return !empty($this->content);
+	}
+}
diff --git a/inc/ui/_setup.php b/inc/ui/_setup.php
new file mode 100644
index 0000000..29cfcc0
--- /dev/null
+++ b/inc/ui/_setup.php
@@ -0,0 +1,5 @@
+<?php
+require(JVB_DIR.'/inc/ui/Modal.php');
+require(JVB_DIR.'/inc/ui/Navigation.php');
+require(JVB_DIR.'/inc/ui/Tabs.php');
+require(JVB_DIR.'/inc/ui/CRUDSkeleton.php');
diff --git a/inc/utility/Image.php b/inc/utility/Image.php
index 8ecbd83..d9a9515 100644
--- a/inc/utility/Image.php
+++ b/inc/utility/Image.php
@@ -21,41 +21,74 @@
 
 	public function formatImage(int $ID, string $start = 'tiny', string $replace = 'large', bool $addLink = true, ?string $postSlug = null):string
 	{
-		$return =  $this->cache->remember(
+		$return = $this->cache->remember(
 			['ID' => $ID, 'start' => $start, 'replace' => $replace],
 			function() use ($ID, $start, $replace) {
-				$img = wp_get_attachment_image_src($ID, $start);
-				if (!$img) {
-					return'';
+				// Define size order for progressive enhancement
+				$sizeOrder = ['tiny', 'medium', 'large', 'full'];
+				$startIndex = array_search($start, $sizeOrder);
+				$replaceIndex = array_search($replace, $sizeOrder);
+
+				// Fallback if invalid sizes provided
+				if ($startIndex === false) $startIndex = 0;
+				if ($replaceIndex === false) $replaceIndex = 2;
+
+				// Get all images up to the replace size
+				$images = [];
+				for ($i = $startIndex; $i <= $replaceIndex; $i++) {
+					$img = wp_get_attachment_image_src($ID, $sizeOrder[$i]);
+					if ($img) {
+						$images[$sizeOrder[$i]] = $img;
+					}
 				}
-				$img = $img[0];
 
-				$data = $this->getGallerySizes($ID, $replace);
+				if (empty($images)) return '';
 
-
-
+				// Use first available image as src
+				$firstImage = reset($images);
 				$alt = get_post_meta($ID, '_wp_attachment_image_alt', true);
-				$alt = ($alt=='')? '' : ' alt="'.$alt.'" ';
-				return '<img width="100%" height="auto" src="'.$img.'"'.$alt.$data.' loading="lazy" decoding="async">';
+				$alt = ($alt=='')? '' : ' alt="'.esc_attr($alt).'" ';
+
+				// Build srcset only with images from start to replace
+				$srcsetParts = [];
+				foreach ($images as $img) {
+					$srcsetParts[] = sprintf('%s %dw', $img[0], $img[1]);
+				}
+				$srcset = implode(', ', $srcsetParts);
+
+				return sprintf(
+					'<img src="%s"%s srcset="%s" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
+					$firstImage[0],
+					$alt,
+					$srcset
+				);
 			}
 		);
 
-		$aOpen = $aClose = '';
 		if ($addLink) {
 			if (!$postSlug) {
 				global $post;
 				$postSlug = $post->post_name;
 			}
+			$full = wp_get_attachment_image_src($ID, 'full');
 
 			$imgPost = get_post($ID);
-			if (!$imgPost) {
-				return $return;
-			}
+			if (!$imgPost) return $return;
+
 			$imgSlug = $imgPost->post_name;
-			$aOpen = '<a class="open-gallery" target="_blank" rel="nofollow" data-opens="gallery-'.$postSlug.'" data-focus="'.$postSlug.'-'.$imgSlug.'">';
-			$aClose = '</a>';
+			$galleryAttrs = sprintf(
+				' data-gallery="gallery-%s" data-focus="%s-%s" data-full="%s"',
+				$postSlug,
+				$postSlug,
+				$imgSlug,
+				$full[0]
+			);
+
+			// Add gallery attributes to img tag
+			$return = str_replace('<img', '<img'.$galleryAttrs, $return);
 		}
-		return $aOpen.$return.$aClose;
+
+		return $return;
 	}
 
 	public function getGallerySizes(int $ID, string $replace):string
diff --git a/inc/utility/Validator.php b/inc/utility/Validator.php
index 6cd291f..d6505b2 100644
--- a/inc/utility/Validator.php
+++ b/inc/utility/Validator.php
@@ -12,6 +12,24 @@
 {
 	private array $errors = [];
 	private array $warnings = [];
+	protected array $validSchemaTypes = [
+		'content' => [
+			'Article', 'NewsArticle', 'BlogPosting', 'VisualArtwork',
+			'Product', 'Service', 'Event', 'Person', 'CreativeWork',
+			'MedicalProcedure', 'HowTo', 'Recipe', 'Review',
+		],
+		'taxonomy' => [
+			'CollectionPage', 'DefinedTerm', 'ItemList',
+		],
+		'user' => [
+			'Person',
+		],
+	];
+
+	protected array $validModifiers = [
+		'first', 'last', 'join', 'truncate', 'strip', 'lower', 'upper',
+		'title', 'count', 'get', 'default', 'date', 'image_url', 'excerpt', 'plural'
+	];
 
 	public function validateAll():array
 	{
@@ -20,6 +38,8 @@
 		$success['terms'] = $this->validateTaxonomyConfig(JVB_TAXONOMY);
 		$success['user'] = $this->validateUserConfig(JVB_USER);
 		$success['crossReference'] = $this->validateCrossReferences(JVB_CONTENT, JVB_TAXONOMY, JVB_USER);
+		$success['seo'] = $this->validateSEOConfig();
+		$success['schema'] = $this->validateSchemaConfig(JVB_SCHEMA ?? []);
 		return $success;
 	}
 	/**
@@ -499,4 +519,225 @@
 			}
 		}
 	}
+
+	/**
+	 * Validate SEO configurations across all types
+	 */
+	public function validateSEOConfig(): bool
+	{
+		$this->errors = [];
+		$this->warnings = [];
+
+		foreach (JVB_CONTENT ?? [] as $slug => $config) {
+			if (isset($config['seo'])) {
+				$this->validateTypeSEOConfig($slug, $config['seo'], 'content', $config);
+			}
+		}
+
+		foreach (JVB_TAXONOMY ?? [] as $slug => $config) {
+			if (isset($config['seo'])) {
+				$this->validateTypeSEOConfig($slug, $config['seo'], 'taxonomy', $config);
+			}
+		}
+
+		foreach (JVB_USER ?? [] as $slug => $config) {
+			if (isset($config['seo'])) {
+				$this->validateTypeSEOConfig($slug, $config['seo'], 'user', $config);
+			}
+		}
+
+		$this->logResults();
+		return empty($this->errors);
+	}
+
+	/**
+	 * Validate SEO config for a specific type
+	 */
+	private function validateTypeSEOConfig(string $slug, array $seo, string $objectType, array $fullConfig): void
+	{
+		$path = "{$objectType}.{$slug}.seo";
+		$availableFields = $this->getAvailableSEOFields($slug, $objectType, $fullConfig);
+
+		if (isset($seo['schema_type'])) {
+			$validTypes = $this->validSchemaTypes[$objectType] ?? $this->validSchemaTypes['content'];
+			if (!in_array($seo['schema_type'], $validTypes)) {
+				$this->addWarning("{$path}.schema_type", "'{$seo['schema_type']}' may not be valid. Common types: " . implode(', ', array_slice($validTypes, 0, 5)));
+			}
+		}
+
+		if (isset($seo['field_map'])) {
+			foreach ($seo['field_map'] as $prop => $source) {
+				$this->validateFieldSource($source, $availableFields, "{$path}.field_map.{$prop}");
+			}
+		}
+
+		if (isset($seo['meta']['title'])) {
+			$this->validatePatternString($seo['meta']['title'], $availableFields, "{$path}.meta.title");
+		}
+
+		if (isset($seo['meta']['description'])) {
+			$this->validatePatternString($seo['meta']['description'], $availableFields, "{$path}.meta.description");
+		}
+	}
+
+	/**
+	 * Validate a field source reference
+	 */
+	private function validateFieldSource(string $source, array $availableFields, string $path): void
+	{
+		if (empty($source)) {
+			return;
+		}
+
+		if (str_contains($source, '{{')) {
+			$this->validatePatternString($source, $availableFields, $path);
+			return;
+		}
+
+		$field = explode('|', $source)[0];
+		$field = explode('.', $field)[0];
+
+		if (!in_array($field, $availableFields) && !in_array($field, ['site', 'author', 'meta', 'terms'])) {
+			$this->addWarning($path, "Field '{$field}' may not exist");
+		}
+	}
+
+	/**
+	 * Validate pattern string syntax
+	 */
+	private function validatePatternString(string $pattern, array $availableFields, string $path): void
+	{
+		preg_match_all('/\{\{([^}]+)\}\}/', $pattern, $matches);
+
+		foreach ($matches[1] as $token) {
+			$token = trim($token);
+
+			if (empty($token)) {
+				$this->addError($path, "Empty placeholder {{}} found");
+				continue;
+			}
+
+			$parts = explode('|', $token);
+			$field = trim(explode('.', $parts[0])[0]);
+
+			if (!in_array($field, $availableFields) && !in_array($field, ['site', 'author', 'meta', 'terms'])) {
+				$this->addWarning($path, "Field '{$field}' in pattern may not exist");
+			}
+
+			if (isset($parts[1])) {
+				$modifier = trim(explode(':', $parts[1])[0]);
+				if (!in_array($modifier, $this->validModifiers)) {
+					$this->addWarning($path, "Unknown modifier '|{$modifier}'");
+				}
+			}
+		}
+	}
+
+	/**
+	 * Validate JVB_SCHEMA configuration
+	 */
+	public function validateSchemaConfig(array $schema): bool
+	{
+		$this->errors = [];
+		$this->warnings = [];
+
+		if (isset($schema['business'])) {
+			$this->validateBusinessSchema($schema['business']);
+		}
+
+		if (isset($schema['faqs']['items'])) {
+			foreach ($schema['faqs']['items'] as $i => $faq) {
+				if (empty($faq['question'])) {
+					$this->addError("schema.faqs.items[{$i}].question", "FAQ question required");
+				}
+				if (empty($faq['answer'])) {
+					$this->addError("schema.faqs.items[{$i}].answer", "FAQ answer required");
+				}
+			}
+		}
+
+		$this->logResults();
+		return empty($this->errors);
+	}
+
+	/**
+	 * Validate business schema
+	 */
+	private function validateBusinessSchema(array $config): void
+	{
+		$path = 'schema.business';
+
+		if (empty($config['name'])) {
+			$this->addError("{$path}.name", "Business name required");
+		}
+
+		if (isset($config['url']) && !filter_var($config['url'], FILTER_VALIDATE_URL)) {
+			$this->addError("{$path}.url", "Invalid URL");
+		}
+
+		if (isset($config['email']) && !filter_var($config['email'], FILTER_VALIDATE_EMAIL)) {
+			$this->addError("{$path}.email", "Invalid email");
+		}
+
+		if (isset($config['geo'])) {
+			$lat = $config['geo']['lat'] ?? null;
+			$lng = $config['geo']['lng'] ?? null;
+
+			if ($lat !== null && (!is_numeric($lat) || $lat < -90 || $lat > 90)) {
+				$this->addError("{$path}.geo.lat", "Latitude must be -90 to 90");
+			}
+			if ($lng !== null && (!is_numeric($lng) || $lng < -180 || $lng > 180)) {
+				$this->addError("{$path}.geo.lng", "Longitude must be -180 to 180");
+			}
+		}
+
+		if (isset($config['opening_hours'])) {
+			$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
+			foreach ($config['opening_hours'] as $day => $data) {
+				if (!in_array(strtolower($day), $days)) {
+					$this->addWarning("{$path}.opening_hours.{$day}", "Invalid day");
+				}
+				if (is_array($data) && empty($data['closed'])) {
+					if (isset($data['open']) && !preg_match('/^\d{2}:\d{2}$/', $data['open'])) {
+						$this->addWarning("{$path}.opening_hours.{$day}.open", "Use HH:MM format");
+					}
+				}
+			}
+		}
+
+		if (isset($config['aggregate_rating'])) {
+			$value = $config['aggregate_rating']['value'] ?? null;
+			if ($value !== null && (!is_numeric($value) || $value < 0 || $value > 5)) {
+				$this->addError("{$path}.aggregate_rating.value", "Rating must be 0-5");
+			}
+		}
+
+		if (isset($config['same_as'])) {
+			foreach ($config['same_as'] as $i => $link) {
+				$url = is_array($link) ? ($link['url'] ?? '') : $link;
+				if (!empty($url) && !filter_var($url, FILTER_VALIDATE_URL)) {
+					$this->addError("{$path}.same_as[{$i}]", "Invalid URL: {$url}");
+				}
+			}
+		}
+	}
+
+	/**
+	 * Get available fields for SEO validation
+	 */
+	private function getAvailableSEOFields(string $slug, string $objectType, array $config): array
+	{
+		$fields = match($objectType) {
+			'content' => ['post_title', 'post_excerpt', 'post_content', 'post_date', 'post_modified', 'post_thumbnail', 'permalink'],
+			'taxonomy' => ['term_name', 'term_description', 'term_slug', 'permalink', 'count'],
+			'user' => ['display_name', 'first_name', 'last_name', 'user_email', 'description', 'permalink'],
+			default => []
+		};
+
+		if (!empty($config['fields'])) {
+			$fields = array_merge($fields, array_keys($config['fields']));
+		}
+
+		return $fields;
+	}
 }
diff --git a/jvb.php b/jvb.php
index fab005a..7989497 100644
--- a/jvb.php
+++ b/jvb.php
@@ -129,18 +129,69 @@
 require(JVB_DIR . '/activate.php');
 
 require(JVB_DIR . '/inc/helpers/all.php');
+require(JVB_DIR . '/inc/ui/_setup.php');
 require(JVB_DIR . '/inc/meta/_setup.php');
 require(JVB_DIR . '/inc/importers/_setup.php');
 require(JVB_DIR . '/inc/managers/_setup.php');
 
-function jvbIcon($name, $options = []) {
-	return IconsManager::getInstance()->getIcon($name, $options);
+/**
+ * Get an icon element
+ *
+ * @param string $name Icon name
+ * @param array $options Options array:
+ *   - 'source' => 'icons'|'dash'|'forms'|etc. (default: 'icons')
+ *   - 'style' => 'regular'|'bold'|'fill'|etc.
+ *   - 'label' => 'Accessible label'
+ *   - 'decorative' => true
+ *   - 'class' => 'additional classes'
+ *   - 'size' => 24
+ * @return string HTML icon element
+ */
+function jvbIcon(string $name, array $options = []): string
+{
+	$source = $options['source'] ?? 'icons';
+
+	// Remove source from options before passing to IconsManager
+	unset($options['source']);
+
+	return IconsManager::for($source)->get($name, $options);
 }
 
-function jvbCSSIcon($name, $options = []) {
-	$style = array_key_exists('style', $options) ? $options['style'] : null;
-	return IconsManager::getInstance()->getCSSIcon($name, $style);
+/**
+ * Get a CSS data URI for an icon
+ *
+ * @param string $name Icon name
+ * @param array $options Options array:
+ *   - 'style' => 'regular'|'bold'|'fill'|etc.
+ *   - 'source' => 'icons'|'dash'|'forms'|etc. (for tracking purposes)
+ * @return string data:image/svg+xml;base64,... URL
+ */
+function jvbCSSIcon(string $name, array $options = []): string
+{
+	$style = $options['style'] ?? null;
+	$source = $options['source'] ?? 'icons';
+
+	return IconsManager::for($source)->getCSSIcon($name, $style);
 }
+
+/**
+ * Get a dashboard icon
+ */
+function jvbDashIcon(string $name, array $options = []): string
+{
+	$options['source'] = 'dash';
+	return jvbIcon($name, $options);
+}
+
+/**
+ * Get a form editor icon
+ */
+function jvbFormIcon(string $name, array $options = []): string
+{
+	$options['source'] = 'forms';
+	return jvbIcon($name, $options);
+}
+
 require(JVB_DIR . '/inc/integrations/_setup.php');
 require(JVB_DIR . '/inc/rest/_setup.php');
 
@@ -245,696 +296,49 @@
 
 function jvbScripts():void
 {
-    /**
-     * Register Scripts
-     */
-    //Helper functions used by other classes
-    wp_register_script(
-        'jvb-utility',
-        JVB_URL.'assets/js/min/utility.min.js',
-        [],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-	wp_register_script(
-		'jvb-favourites',
-		JVB_URL.'assets/js/min/favourites.min.js',
-		[
-			'jvb-queue',
-			'jvb-data-store'
-		],
-		'1.0.1',
-		[
-			'strategy'    => 'defer',
-			'in_footer'    => true,
-		]
-	);
-	wp_register_script(
-		'jvb-votes',
-		JVB_URL.'assets/js/min/votes.min.js',
-		[
-			'jvb-queue',
-			'jvb-data-store'
-		],
-		'1.0.1',
-		[
-			'strategy'    => 'defer',
-			'in_footer'    => true,
-		]
-	);
-
-	wp_register_script(
-		'jvb-settings',
-		JVB_URL.'assets/js/min/settings.min.js',
-		[
-//			'jvb-queue',
-			'jvb-utility',
-			'jvb-data-store'
-		],
-		'1.0.1',
-		[
-			'strategy'    => 'defer',
-			'in_footer'    => true,
-		]
-	);
-
-	wp_register_script(
-		'jvb-popup',
-		JVB_URL.'assets/js/min/popup.min.js',
-		[
-			'jvb-a11y'
-		],
-		'1.0.1',
-		[
-			'strategy'	=> 'defer',
-			'in_footer'	=> true
-		]
-	);
-
-    //Main js image resizing, and gallery
-    //TODO: lots of overlap between modals and this, utilize a11y for trapFocus,
-    wp_register_script(
-        'jvb-media',
-        JVB_URL.'assets/js/min/media.min.js',
-        [],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer' => true,
-        ]
-    );
-
-	wp_register_script(
-		'jvb-copy-hours',
-		JVB_URL.'assets/js/min/hours.min.js',
-		[
-			'jvb-form',
-			'jvb-utility',
-			'jvb-modal',
-			'jvb-a11y'
-		],
-		'1.0.1',
-		[
-			'strategy'	=> 'defer',
-			'in_footer' => true,
-		]
-	);
-
-    //Includes the escape and outside click listeners and intersection observers
-    //TODO: make the Modals class use this?
-//    wp_register_script(
-//        'jvb-ui',
-//        JVB_URL.'assets/js/min/ui.min.js',
-//        [],
-//        '1.0.0',
-//        [
-//            'strategy'    => 'defer',
-//            'in_footer'    => true,
-//        ]
-//    );
-
-    wp_register_script(
-        'jvb-gallery',
-        JVB_URL.'assets/js/min/gallery.min.js',
-        [
-			'jvb-utility',
-//			'jvb-queue',
-			'jvb-modal',
-//			'jvb-swiper',
-		],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-	wp_register_script(
-		'jvb-swiper',
-		JVB_URL.'assets/js/min/swiper.min.js',
-		[
-			'jvb-utility',
-		],
-		'1.0.1',
-		[
-			'strategy'    => 'defer',
-			'in_footer'    => true,
-		]
-	);
-
-	wp_register_script(
-		'jvb-integrations',
-		JVB_URL.'assets/js/min/integrations.min.js',
-		[],
-		'1.0.1',
-		[
-			'strategy'	=> 'defer',
-			'in_footer'	=> true
-		]
-	);
-
-
-	$integration_nonces = [
-		'jvb_square_sync' => wp_create_nonce('jvb_square_sync'),
-		'jvb_gmb_sync_reviews' => wp_create_nonce('jvb_gmb_sync'),
-		'jvb_gmb_test_api' => wp_create_nonce('jvb_gmb_test'),
-		'jvb_bluesky_test_post' => wp_create_nonce('jvb_bluesky_test'),
-		'jvb_facebook_test_post' => wp_create_nonce('jvb_facebook_test'),
-		'jvb_instagram_test_post' => wp_create_nonce('jvb_instagram_test'),
-		'jvb_instagram_sync_media' => wp_create_nonce('jvb_instagram_sync'),
-		'jvb_umami_refresh_data' => wp_create_nonce('jvb_umami_refresh'),
-		'jvb_export_integration_settings' => wp_create_nonce('jvb_integration_export'),
-	];
-	$data = [
-		'ajaxUrl' => admin_url('admin-ajax.php'),
-		'nonce' => wp_create_nonce('jvb_integrations'),
-		'nonces' => $integration_nonces,
-//		'services' => array_keys(jvbConnect()->getAvailableServices()),
-		'userId' => get_current_user_id(),
-		'baseUrl' => admin_url('admin.php?page=' . BASE)
-	];
-	wp_localize_script(
-		'jvb-integrations',
-		'jvbIntegrationsConfig',
-		$data
-	);
-
-    //The On-This-Page menu. TODO: just include in ui?
-    wp_register_script(
-        'jvb-page-nav',
-        JVB_URL.'assets/js/min/page-nav.min.js',
-        [],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //A11y accessibility
-    wp_register_script(
-        'jvb-a11y',
-        JVB_URL.'assets/js/min/a11y.min.js',
-        [],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Central Error Management
-    wp_register_script(
-        'jvb-error',
-        JVB_URL.'assets/js/min/error.min.js',
-        [
-
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Cache Management
-    wp_register_script(
-        'jvb-cache',
-        JVB_URL.'assets/js/min/cache.min.js',
-        [],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-	wp_register_Script(
-		'jvb-data-store',
-		JVB_URL.'assets/js/min/dataStore.min.js',
-		[],
-		'1.0.1',
-		[
-			'strategy'	=> 'defer',
-			'in_footer'	=> true,
-		]
-	);
-
-    //Tabs functionality
-    wp_register_script(
-        'jvb-tabs',
-        JVB_URL.'assets/js/min/tabs.min.js',
-        [
-            'jvb-a11y'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Modal functionality
-    wp_register_script(
-        'jvb-modal',
-        JVB_URL.'assets/js/min/modal.min.js',
-        [
-            'jvb-a11y'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Central Queue Management
-    wp_register_script(
-        'jvb-queue',
-        JVB_URL.'assets/js/min/queue.min.js',
-        [
-            'jvb-a11y',
-            'jvb-error',
-            'jvb-data-store',
-            'jvb-utility',
-			'jvb-popup'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //TaxonomySelector.js
-    wp_register_script(
-        'jvb-selector',
-        JVB_URL.'assets/js/min/selector.min.js',
-        [
-            'jvb-utility',
-            'jvb-a11y',
-            'jvb-error',
-            'jvb-data-store',
-            'jvb-modal',
-//            'jvb-loading'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-	wp_register_script(
-		'jvb-creator',
-		JVB_URL.'assets/js/min/creator.min.js',
-		['jvb-selector'],
-		'1.0.1',
-		[
-			'strategy'    => 'defer',
-			'in_footer'		=> true
-		]
-	);
-
-    //PostSelector.js
-    wp_register_script(
-        'jvb-post-selector',
-        JVB_URL.'assets/js/min/postSelector.min.js',
-        [
-            'jvb-selector'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-//    //Central Loading Manager
-//    wp_register_script(
-//        'jvb-loading',
-//        JVB_URL.'assets/js/min/loading.min.js',
-//        [],
-//        '1.0.0',
-//        [
-//            'strategy'    => 'defer',
-//            'in_footer'    => true,
-//        ]
-//    );
-//    wp_localize_script(
-//        'jvb-loading',
-//        'loadingQuips',
-//        [
-//            'quips' => json_encode(apply_filters(
-//                'jvbLoadingQuips',
-//                []
-//            ))
-//        ]
-//    );
-
-    //Upload Manager
-    wp_register_script(
-        'jvb-handle-selection',
-        JVB_URL.'assets/js/min/handleSelection.min.js',
-        [
-			'jvb-a11y',
-            'jvb-utility',
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-    wp_register_script(
-        'jvb-drag-handler',
-        JVB_URL.'assets/js/min/dragHandler.min.js',
-        [
-			'jvb-a11y',
-            'jvb-utility',
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Upload Manager
-    wp_register_script(
-        'jvb-uploader',
-        JVB_URL.'assets/js/min/uploader.min.js',
-        [
-			'sortable-multidrag',
-			'jvb-cache',
-			'jvb-a11y',
-            'jvb-utility',
-			'jvb-handle-selection',
-			'jvb-modal',
-//			'jvb-drag-handler',
-//            'jvb-loading',
-            'jvb-queue',
-            'jvb-notifications'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-	wp_register_script(
-		'quill-js',
-		'https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js',
-		[],
-		null,
-		true
-	);
-
-	wp_register_script(
-		'sortable-js',
-		'https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js',
-		array(),
-		null,
-		true
-	);
-
-	// Load MultiDrag plugin
-	wp_register_script(
-		'sortable-multidrag',
-		'https://cdn.jsdelivr.net/npm/sortablejs@latest/plugins/MultiDrag.min.js',
-		array('sortable-js'),
-null,
-		true
-	);
-
-    //Custom Dashboard Navigator
-//    wp_register_script(
-//        'jvb-dashboard-navigator',
-//        JVB_URL.'assets/js/min/DashboardNavigator.min.js',
-//        [
-//            'jvb-a11y',
-//            'jvb-loading',
-//            'jvb-content',
-//            'jvb-crud',
-//			'jvb-tabs'
-//        ],
-//        '1.0.0',
-//        [
-//            'strategy'    => 'defer',
-//            'in_footer'    => true,
-//        ]
-//    );
-
-    //Notifications
-    wp_register_script(
-        'jvb-notifications',
-        JVB_URL.'assets/js/min/notifications.min.js',
-        [
-            'jvb-utility',
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer' => true,
-        ]
-    );
-
-    //Base Form Handler
-    wp_register_script(
-        'jvb-form',
-        JVB_URL.'assets/js/min/form.min.js',
-        [
-            'jvb-utility',
-            'jvb-tabs',
-            'jvb-selector',
-            'jvb-uploader',
-			'sortable-js',
-			'jvb-populate-form',
-			'jvb-quill',
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-	wp_register_script(
-		'jvb-populate-form',
-		JVB_URL.'assets/js/min/populate.min.js',
-		[],
-		'1.0.1',
-		[
-			'strategy'	=> 'defer',
-			'in_footer'	=> true,
-		]
-	);
-	wp_register_script(
-		'jvb-quill',
-		JVB_URL.'assets/js/min/quill.min.js',
-		[
-			'quill-js'
-		],
-		'1.0.1',
-		[
-			'strategy'	=> 'defer',
-			'in_footer'	=> true,
-		]
-	);
-    //CRUD Base Manager
-    wp_register_script(
-        'jvb-crud',
-        JVB_URL.'assets/js/min/crud.min.js',
-        [
-			'jvb-selector',
-			'jvb-settings',
-            'jvb-a11y',
-            'jvb-error',
-            'jvb-data-store',
-			'jvb-populate-form',
-            'jvb-queue',
-            'jvb-utility',
-			'jvb-quill',
-            'jvb-form',
-			'jvb-view',
-			'jvb-modal'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-	wp_register_script(
-		'jvb-view',
-		JVB_URL.'assets/js/min/view.min.js',
-		[
-			'jvb-settings',
-			'jvb-a11y',
-			'jvb-utility',
-			'jvb-data-store',
-			'jvb-error',
-			'jvb-populate-form'
-		],
-		'1.0.1',
-		[
-			'strategy'	=> 'defer',
-			'in_footer'	=> true,
-		]
-	);
-
-    //Bio Manager TODO: Replace with Form Handler
-    wp_register_script(
-        'jvb-bio',
-        JVB_URL.'assets/js/min/bioManager.min.js',
-        [
-            'jvb-tabs',
-            'jvb-form',
-            'jvb-queue'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Shop Manager TODO: Replace with Form Handler
-    wp_register_script(
-        'jvb-shop',
-        JVB_URL.'assets/js/min/shopManager.min.js',
-        [
-            'jvb-tabs',
-            'jvb-form',
-            'jvb-queue'
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Content Manager TODO: Replace with CRUD.js
-    wp_register_script(
-        'jvb-content',
-        JVB_URL.'assets/js/min/ContentManager.min.js',
-        [
-            'jvb-queue',
-            'jvb-cache',
-            'jvb-error',
-            'jvb-uploader',
-            'jvb-utility',
-            'jvb-modal',
-            'jvb-selector',
-            'jvb-post-selector',
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Favourites Manager TODO: Replace with CRUD.js
-    wp_register_script(
-        'jvb-favourites',
-        JVB_URL.'assets/js/min/favouritesManager.min.js',
-        [
-            'jvb-a11y',
-            'jvb-queue',
-            'jvb-cache',
-            'jvb-error',
-            'jvb-utility',
-            'jvb-tabs',
-            'jvb-selector',
-            'jvb-notifications',
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //News Manager TODO: Replace with CRUD.js
-    wp_register_script(
-        'jvb-news',
-        JVB_URL.'assets/js/min/news.min.js',
-        [
-            'jvb-a11y',
-            'jvb-queue',
-            'jvb-cache',
-            'jvb-error',
-            'jvb-utility',
-            'jvb-modal',
-            'jvb-selector',
-            'jvb-tabs',
-        ],
-        '1.0.1',
-        [
-            'strategy'    => 'defer',
-            'in_footer'    => true,
-        ]
-    );
-
-    //Notification Manager TODO: Replace with CRUD? Not quite...
-    wp_register_script(
-        'jvb-notification-manager',
-        JVB_URL.'assets/js/min/notificationManager.min.js',
-        [
-            'jvb-a11y',
-            'jvb-tabs',
-        ]
-    );
-
-	wp_register_script(
-		'jvb-navigation',
-		JVB_URL.'assets/js/min/navigation.min.js',
-	);
-
     add_action('wp_head', 'jvbInlineNavStyles');
 
 	if (Features::forSite()->has('dashboard')) {
 		wp_enqueue_script('jvb-queue');
 	}
 
+	wp_enqueue_script('jvb-auth');
 	wp_enqueue_script('jvb-settings');
     wp_enqueue_script('jvb-navigation');
 //    wp_enqueue_script('jvb-ui');
-    wp_enqueue_script('jvb-media');
+//    wp_enqueue_script('jvb-media');
 	wp_enqueue_script('jvb-gallery');
     wp_enqueue_script('jvb-cache');
 
 
+	$interactions = [];
+	if (Features::forSite()->has('favourites')) {
+		$interactions[] = 'favourites';
+	}
+	if (Features::anyContentHas('karma') ||
+		Features::anyTaxonomyHas('karma') ||
+		Features::anyUserHas('karma')) {
+		$interactions[] = 'karma';
+	}
+	if (Features::forSite()->has('notifications')) {
+		$interactions[] = 'notifications';
+	}
 
-    $userID = get_current_user_id();
-    $queue = (is_user_logged_in()) ?
-        [
-            'api'             => rest_url('jvb/v1/'),
-            'currentUser'     => $userID,
-            'nonce'            => wp_create_nonce('wp_rest'),
-            'dash'            => wp_create_nonce('dash-'.$userID),
-            'favourites'    => wp_create_nonce('favourites-'.$userID),
-            'notifications'    => wp_create_nonce('notifications-'.$userID),
-			'labels'			=> jvbGetLabels(),
-        ] :
-        [
-            'api'         => rest_url('jvb/v1/'),
-            'nonce'       => wp_create_nonce('wp_rest'),
-            'currentUser' => false,
-            'redirect'    => wp_login_url(home_url(add_query_arg(null, null))), // Current URL as redirect
-			'labels'			=> jvbGetLabels(),
-        ];
+	if (!empty($interactions)) {
+		wp_enqueue_script('jvb-interactions');
+		foreach($interactions as $interaction) {
+			wp_enqueue_script('jvb-'.$interaction);
+		}
+	}
 
-    wp_localize_script('jvb-utility', 'jvbSettings', $queue);
 
+	$queue = [
+		'api' => rest_url('jvb/v1/'),
+		'redirect' => wp_login_url(home_url(add_query_arg(null, null))),
+		'labels' => jvbGetLabels(),
+	];
+
+    wp_localize_script('jvb-auth', 'jvbSettings', $queue);
 
 
 	$initUserSettings = 'async function initUserItems() {
@@ -1143,3 +547,20 @@
 //	}
 //	return $result;
 //}, 10, 3);
+
+add_filter('rest_authentication_errors', function($result) {
+
+	// Don't override existing authentication
+	if (is_wp_error($result) || $result === true) {
+		return $result;
+	}
+
+	// Try to authenticate from cookie
+	$cookie_user = wp_validate_auth_cookie('', 'logged_in');
+
+	if ($cookie_user) {
+		wp_set_current_user($cookie_user);
+		return true;
+	}
+	return $result;
+}, 99);
diff --git a/src/faq/style.scss b/src/faq/style.scss
index 14b79d2..13564fa 100644
--- a/src/faq/style.scss
+++ b/src/faq/style.scss
@@ -1,8 +1,8 @@
 nav#faq {
-	--height: fit-content;
+	height: max-content;
 	display: block;
 	background-color: var(--base-100);
-	border-radius: var(--outerRadius);
+	border-radius: var(--radius-outer);
 	padding: 1.5rem;
 	touch-action: auto;
 	ol {
@@ -12,17 +12,18 @@
 		counter-reset: faq;
 		li {
 			counter-increment: faq;
+			width: max-content;
 			&::before {
 				content: counter(faq);
 				display: block;
 				font-family: var(--heading);
-				font-weight: var(--hBold);
+				font-weight: var(--fw-h-bold);
 			}
 		}
 	}
 	h2 {
 		left: 0;
-		font-size: var(--large);
+		font-size: var(--txt-large);
 		margin: .5rem 0 .5rem;
 	}
 	a {
@@ -35,7 +36,7 @@
 	max-width: none;
 	width: 100%;
 	> * {
-		max-width: var(--alignWide);
+		max-width: var(--wide);
 		margin: 1rem auto;
 	}
 	h2 {
@@ -52,11 +53,11 @@
 		h2 {
 			background-color: var(--base);
 			padding: 1rem 1.5rem;
-			border-radius: var(--outerRadius);
+			border-radius: var(--radius-outer);
 		}
 	}
 	details {
-		max-width: var(--maxWidth);
+		max-width: var(--content);
 		margin: 1rem auto;
 		padding: .75rem;
 	}
@@ -65,7 +66,7 @@
 	}
 	details .button {
 		height: fit-content;
-		display: block;
+		display: flex;
 		margin-left: auto;
 	}
 }
diff --git a/src/feed/style.scss b/src/feed/style.scss
index b40d457..74fbf73 100644
--- a/src/feed/style.scss
+++ b/src/feed/style.scss
@@ -35,7 +35,7 @@
 //	position: sticky;
 //	top: 3rem;
 //	z-index: 15;
-//	background: var(--overlay-heavy);
+//	background: rgba(var(--base-rgb),var(--op-6));
 //	padding: .25rem 3rem;
 //	details[open] summary {
 //		background-color: var(--overlay);
@@ -72,7 +72,7 @@
 //
 //	details[open],
 //	summary:hover {
-//		background-color: var(--overlay-heavy);
+//		background-color: rgba(var(--base-rgb),var(--op-6));
 //	}
 //
 //	&:has(#favourites) {
@@ -156,7 +156,7 @@
 //	text-align: center;
 //	padding: 2rem;
 //	background: var(--base-100);
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //	margin: 0 auto;
 //	max-width: 600px;
 //}
@@ -190,7 +190,7 @@
 //	background: var(--base-50);
 //	box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 //	opacity: 0;
-//	transition: opacity var(--transition-base) var(--delay);
+//	transition: opacity var(--trans-base) var(--delay);
 //	height: fit-content;
 //	padding: 0;
 //
@@ -242,9 +242,9 @@
 //			bottom: 0;
 //			left: 0;
 //			right: 0;
-//			background-color: var(--overlay-light);
+//			background-color: rgba(var(--base-rgb),var(--op-3));
 //			backdrop-filter: blur(5px);
-//			border-radius: var(--innerRadius);
+//			border-radius: var(--radius);
 //			z-index: 1;
 //			padding: .25rem .25rem .25rem 1.1rem;
 //		}
@@ -281,12 +281,12 @@
 //	background: var(--base);
 //	color: var(--contrast);
 //	border-radius: 4px;
-//	font-size: var(--medium);
-//	transition: all var(--transition-base);
+//	font-size: var(--txt-medium);
+//	transition: all var(--trans-base);
 //	border: 2px solid transparent;
 //	&[hidden] {
 //		opacity: 0;
-//		transition: all var(--transition-base);
+//		transition: all var(--trans-base);
 //	}
 //	&:hover {
 //		background: var(--pink-50);
@@ -302,9 +302,9 @@
 //	top: .5rem;
 //	right: .5rem;
 //	z-index: 10;
-//	background: var(--overlay-medium);
+//	background: rgba(var(--base-rgb),var(--op-4));
 //	border-radius: 50%;
-//	box-shadow: var(--subtle);
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw-subtle);
 //	border: none;
 //	width: 2rem;
 //	height: 2rem;
@@ -312,7 +312,7 @@
 //	justify-content: center;
 //	align-items: center;
 //	backdrop-filter: blur(5px);
-//	transition: all var(--transition-base);
+//	transition: all var(--trans-base);
 //
 //	&:hover {
 //		transform: scale(1.1);
@@ -358,7 +358,7 @@
 //		width: 100%;
 //		height: 100%;
 //		object-fit: cover;
-//		transition: transform var(--timing) var(--function);
+//		transition: transform var(--trans-t) var(--trans-fn);
 //	}
 //	a:hover img {
 //		transform: scale(1.05);
@@ -402,7 +402,7 @@
 //		margin: 0 0 .5em 0!important;
 //		font-size: 1.1rem;
 //		font-family: var(--body);
-//		font-weight: var(--bWeight);
+//		font-weight: var(--fw-b);
 //	}
 //	span {
 //		text-transform: uppercase;
@@ -461,7 +461,7 @@
 //	left: 0;
 //	right: 0;
 //	bottom: 0;
-//	background-color: var(--overlay-medium);
+//	background-color: rgba(var(--base-rgb),var(--op-4));
 //	display: flex;
 //	align-items: center;
 //	justify-content: center;
@@ -487,9 +487,9 @@
 //	}
 //
 //	.wrapper {
-//		background-color: var(--overlay-heavy);
+//		background-color: rgba(var(--base-rgb),var(--op-6));
 //		padding: 2rem;
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //		text-align: center;
 //		max-width: 90%;
 //		width: 400px;
@@ -512,7 +512,7 @@
 //			left: calc(50% - var(--h));
 //			opacity: .5;
 //			z-index: 0;
-//			animation: spin 1s var(--timing) infinite;
+//			animation: spin 1s var(--trans-t) infinite;
 //		}
 //		div.icon {
 //			height: 50px;
@@ -542,7 +542,7 @@
 //				margin: 0;
 //				max-width: 275px;
 //				color: var(--contrast-100);
-//				font-size: var(--small);
+//				font-size: var(--txt-x-small);
 //				animation: flicker 2s infinite;
 //			}
 //		}
@@ -642,8 +642,11 @@
 
 
 .feed-block {
+	grid-column: full;
 	.feed-filters {
 		padding: 1rem 0;
+		max-width:var(--wide);
+		margin: 0 auto;
 	}
 	.filter-group {
 		position: relative;
@@ -655,6 +658,10 @@
 		> .label {
 			top: 0;
 		}
+		[type=radio] {
+			position:absolute;
+			left: var(--offScreen);
+		}
 		button, label {
 			position: relative;
 			padding: .5rem;
@@ -678,7 +685,7 @@
 			bottom: -2rem;
 			width: max-content;
 			white-space: nowrap;
-			font-weight: var(--bWeight);
+			font-weight: var(--fw-b);
 		}
 
 
@@ -701,7 +708,10 @@
 	}
 }
 
-
+.item-grid {
+	padding: 0 var(--chip);
+	max-width: none;
+}
 /** FEED ITEM **/
 .feed.item {
 	position: relative;
@@ -715,7 +725,7 @@
 	img {
 		opacity: .7;
 		filter: grayscale(.5) sepia(.3) blur(7px);
-		transition: opacity var(--transition-base), filter var(--transition-base);
+		transition: opacity var(--trans-base), filter var(--trans-base);
 		&[data-loaded=true] {
 			opacity: 1;
 			filter: none;
@@ -792,9 +802,9 @@
 			bottom: 0;
 			left: 0;
 			right: 0;
-			background-color: var(--overlay-light);
+			background-color: rgba(var(--base-rgb),var(--op-3));
 			backdrop-filter: blur(5px);
-			border-radius: var(--innerRadius);
+			border-radius: var(--radius);
 			z-index: 1;
 			padding: .25rem .25rem .25rem 1.1rem;
 		}
diff --git a/src/feed/styleOld.scss b/src/feed/styleOld.scss
deleted file mode 100644
index 069dd16..0000000
--- a/src/feed/styleOld.scss
+++ /dev/null
@@ -1,1414 +0,0 @@
-/* Base Feed Container */
-.feed-block {
-    max-width: var(--full);
-    margin: 0 auto;
-}
-
-.feed-block > *:not(.feed-grid, h2) {
-    max-width: var(--alignWide);
-    margin: 1rem var(--mr) 1rem var(--ml);
-}
-.feed-block > h2 {
-    max-width: var(--maxWidth);
-}
-
-.feed-block[data-loading="true"] {
-    opacity: 0.7;
-}
-
-.feed-block:empty::before {
-    content: "Looks like there's nothing here yet.";
-    display: block;
-    text-align: center;
-    padding: 2rem;
-}
-
-/* Feed Grid Layout */
-.feed-grid {
-    display: grid;
-    grid-template-columns: repeat(auto-fit, minmax(250px,1fr));
-    gap: .5rem;
-    margin-bottom: 2rem;
-	padding: 0 4rem;
-}
-
-.feed-empty-state {
-	grid-column: 1/-1;
-}
-
-
-/* Feed Items */
-.placeholder {
-	aspect-ratio: 1;
-	background: var(--base);
-	border: 1rem solid var(--base-50);
-	border-radius: 1rem;
-	display: flex;
-	justify-content: center;
-	align-items: center;
-}
-	.placeholder .icon {
-		--w: 50%;
-		color: var(--base-200);
-	}
-		.placeholder .icon svg {
-			animation: dance 2.5s ease-in-out infinite;
-		}
-.feed-item {
-    position: relative;
-    border-radius: 0.5rem;
-    overflow: hidden;
-    background: var(--base-50);
-    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-    opacity: 0;
-    transition: opacity var(--transition-base) var(--delay);
-    height: fit-content;
-    padding: 0;
-}
-.feed-item details a {
-	font-size: clamp(1rem, 0.9306rem + 0.2222vw, 1.125rem);
-}
-	.feed-item details a::before,
-	.feed-item details a::after {
-		display: none;
-	}
-
-.feed-item[data-loaded] {
-    opacity: 1;
-}
-
-.feed-item[data-loaded] + .feed-item[data-loaded] {
-    --delay: var(--delay) + var(--increase);
-}
-
-.feed-item.highlighted {
-    animation: highlight 2s ease-out;
-}
-
-/* Feed Item Images */
-.feed-image {
-    display: block;
-    aspect-ratio: 1;
-    overflow: hidden;
-    width: 100%;
-    height: 100%;
-}
-
-.feed-images.multi {
-	width: 100%;
-	height: 100%;
-	display: grid;
-	grid-template-columns: repeat(3, 1fr);
-	grid-auto-rows: 1fr;
-	gap: 4px;
-}
-	.multi > a {
-		width: 100%;
-		height: 100%;
-		aspect-ratio: 1;
-	}
-	.feed-images > a::before,
-	.feed-images > a::after {
-		display: none;
-	}
-	.multi .feed-image {
-		grid-row: span 2;
-		grid-column: span 2;
-	}
-		.feed-item:nth-of-type(4n + 2) .multi .feed-image {
-			grid-column: 2 / span 2;
-			grid-row: 1 / span 2;
-		}
-		.feed-item:nth-of-type(4n + 3) .multi .feed-image {
-			grid-row: 2 / span 2;
-			grid-column: 1 / span 2;
-		}
-		.feed-item:nth-of-type(4n + 4) .multi .feed-image {
-			grid-column: 2 / span 2;
-			grid-row: 2 / span 2;
-		}
-.feed-images img {
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-    transition: transform var(--timing) var(--function);
-}
-
-.feed-images a:hover img {
-    transform: scale(1.05);
-}
-
-/* Item Information */
-.item-info {
-    padding: .25rem;
-	border-left: 1px solid var(--base-200);
-}
-.item-info a::before,
-.item-info a::after {
-	display: none;
-}
-
-.item-info > div + div {
-	margin-top: .5em;
-	position: relative;
-}
-	.item-info > div + div::before {
-		content: '';
-		display: block;
-		position: absolute;
-		top: -.3em;
-		left: -.25rem;
-		width: 66.6%;
-		border-bottom: 1px solid var(--base-200);
-	}
-.item-list ul {
-	margin: 0;
-	padding: 0.5em 0;
-	display: flex;
-	flex-wrap: wrap;
-	gap: .5rem;
-}
-	.item-list ul li {
-		list-style: none;
-	}
-	.item-list a {
-		background-color: var(--pink-0);
-		border: 1px solid transparent;
-		border-radius: 4px;
-		color: var(--light-0);
-		padding: .25em;
-		line-height: .8;
-	}
-
-		.item-list a:visited {
-			background-color: var(--pink-100);
-			color: var(--white);
-		}
-		.item-list a:visited:hover,
-		.item-list a:visited:focus,
-		.item-list a:hover,
-		.item-list a:focus {
-			background-color: transparent;
-			border-color: var(--contrast);
-			color: var(--contrast);
-		}
-
-.item-info h3 {
-    margin: 0 0 .5em 0!important;
-    font-size: 1.1rem;
-    font-family: var(--body);
-    font-weight: var(--bWeight);
-}
-.item-info span {
-    text-transform: uppercase;
-    display: flex;
-    align-items: center;
-}
-.item-info .icon {
-	--w: 1.1em;
-    margin-right: .5em;
-	display: inline-block;
-	vertical-align: middle;
-}
-
-.label {
-    display: flex;
-    align-items: center;
-    gap: 0.25rem;
-    font-size: 0.9rem;
-}
-
-.label a {
-    color: inherit;
-    text-decoration: none;
-}
-
-.label a:hover {
-    color: var(--pink-0);
-}
-
-/* Favourite Button */
-button.favourite {
-    position: absolute;
-    top: .5rem;
-    right: .5rem;
-    z-index: 10;
-    background: var(--overlay-medium);
-    border-radius: 50%;
-    box-shadow: var(--subtle);
-    border: none;
-    cursor: pointer;
-    width: 2rem;
-    height: 2rem;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    backdrop-filter: blur(5px);
-    transition: all var(--transition-base);
-}
-
-button.favourite:hover {
-    transform: scale(1.1);
-    color: var(--pink-0);
-    background: var(--base);
-    box-shadow: 0 4px 8px rgba(0,0,0,0.15);
-}
-
-button.favourite.favourited {
-    animation: favourite-pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
-}
-
-/* Filters */
-.feed-filters {
-    margin: 2rem 0!important;
-	max-width: 100%!important;
-	position: sticky;
-	top: 3rem;
-	z-index: 15;
-	background: var(--overlay-heavy);
-	padding: .25rem 3rem;
-}
-.feed-filters .feed-controls {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    gap: 2rem;
-    width: 100%;
-}
-
-.feed-filters details summary {
-    justify-content: flex-start;
-    //padding: 2rem .5rem .5rem;
-    /*    display: flex;*/
-    /*    border: 0;*/
-    /*    flex-wrap: nowrap;*/
-    /*    padding: 2rem .5rem .5rem;*/
-    /*    gap: .125rem;*/
-    /*    position: relative;*/
-    /*    justify-content: flex-start;*/
-    /*    gap: .5rem;*/
-    /*    border-radius: var(--innerRadius);*/
-}
-.feed-filters details[open],
-.feed-filters summary:hover,
-.feed-filters details[open] summary {
-    background-color: var(--overlay-heavy);
-}
-.radio-group-label > label,
-.feed-filters .filter-toggle,
-.feed-filters .type-filter > label {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    padding: .35rem;
-    white-space: nowrap;
-    width: fit-content;
-    height: fit-content;
-    cursor: pointer;
-    border: 1px solid var(--base-200);
-    border-radius: 4px;
-    font-size: .875rem;
-    transition: border-color var(--transition-base);
-	margin-bottom: 0;
-}
-
-.filter-toggle .icon {
-    margin-right: .5rem;
-}
-.type-filter:hover {
-    color: var(--pink-0);
-    border-color: var(--pink-0);
-    transition: var(--transition-color);
-}
-.feed-filters .type-filter > label {
-    flex-direction: column;
-}
-.type-filter.favourites-toggle {
-    margin-left: auto;
-}
-.type-filter.favourites-toggle label {
-    position: relative;
-}
-.type-filter.favourites-toggle label .label {
-    top: 100%;
-    right: 0;
-}
-summary > * {
-	order: 3;
-}
-ul.filter-label {
-	display: inline-block;
-	vertical-align: middle;
-	height: 1.3em;
-	margin: 0;
-	padding: 0 .5rem;
-	overflow: hidden;
-	order: 2;
-}
-summary .type-filter.label {
-	order: 1;
-}
-ul.filter-label li {
-	list-style: none;
-	height: 0;
-	overflow:hidden;
-}
-ul.filter-label .active {
-	height: 100%;
-}
-
-input[hidden] + label {
-    display: none;
-}
-.feed-filters svg {
-    width: 25px;
-    height: 25px;
-}
-.order-options {
-    position: relative;
-    display: flex;
-    justify-content: space-between;
-}
-.order-options .order-by {
-    display: flex;
-}
-.order-options .order-direction,
-.order-options .order-by .radio-group-label{
-    display: flex;
-    padding-top: 1.5rem;
-    position: relative;
-}
-.order-options .order-by > .label {
-    margin-right: 2rem;
-}
-.radio-group-label {
-    display: flex;
-    gap: .5rem;
-}
-.feed-filters .radio-group-label label .label {
-    top: .5rem;
-    right: .5rem;
-}
-.feed-filters .order-options label svg {
-    width: 20px;
-    height: 20px;
-}
-.radio-group-label input:checked + label,
-.feed-filters label:hover,
-.feed-filters input:checked + label {
-    background-color: var(--white);
-    border-color: var(--pink);
-    color: var(--pink);
-}
-.feed-filters label .label {
-    visibility: hidden;
-    opacity: 0;
-    transition: transform var(--timing) var(--function);
-    transition-property: max-width, transform;
-}
-.feed-filters input:checked + label .label {
-    visibility: visible;
-    opacity: 1;
-}
-.feed-filters .filters {
-    padding: 1rem;
-    margin-top: 1rem;
-    background-color: transparent;
-}
-.has-filters.filters {
-    background-color: var(--base-50);
-}
-.filter-group {
-    display: flex;
-    gap: .5rem;
-    flex-wrap: wrap;
-    margin-bottom: .25rem;
-    position: relative;
-}
-
-/* Loading States */
-.feed-overlay {
-    display: none;
-    opacity: 0;
-    visibility: hidden;
-}
-.loading .feed-overlay {
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    margin: 0!important;
-    max-width: none!important;
-    width: 100%;
-    height: 100%;
-    background: var(--overlay-medium);
-    backdrop-filter: blur(5px);
-    -webkit-backdrop-filter: blur(5px);
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    z-index: 999999;
-    opacity: 1;
-    visibility: visible;
-    transition: opacity 0.3s ease, visibility 0.3s ease;
-}
-.feed-overlay-content {
-    background: var(--base);
-    padding: 2rem;
-    border-radius: 1rem;
-    box-shadow: var(--shadow);
-    text-align: center;
-    width: min(400px, 60vw);
-}
-.loading .loading-icon-container {
-    position: relative;
-    margin-bottom: 1.5rem;
-    animation: dance 1s ease-in-out infinite;
-    transition: opacity 0.2s ease;
-    will-change: transform, opacity;
-}
-.loading .loading-message .icon {
-    width: 3em;
-    height: 3em;
-}
-.loading .loading-message .icon svg {
-    width: 100%;
-    height: 100%;
-    margin-right: 1rem;
-    animation: dance 2s ease-in-out infinite;
-    transition: color 0.3s ease;
-}
-
-/* Message Styling */
-.loading .loading-message {
-    will-change: opacity;
-
-    font-size: 1rem;
-    color: #666;
-    text-align: center;
-    min-height: 24px;
-    transition: opacity 0.2s ease;
-    margin-bottom: 1rem;
-}
-
-.loading .loading-dots {
-    color: var(--pink-0);
-    width: 4px;
-    aspect-ratio: 1;
-    border-radius: 50%;
-    box-shadow: 19px 0 0 7px, 38px 0 0 3px, 57px 0 0 0;
-    transform: translateX(-38px) scale(.666);
-    animation: bubble .5s infinite alternate linear;
-}
-
-/* Empty States */
-.feed-empty-state {
-    grid-column-start: 1;
-    grid-column-end: 2;
-    text-align: center;
-    padding: 2rem;
-    background: var(--base);
-    border-radius: 1rem;
-    margin: 0 auto;
-    max-width: 600px;
-}
-
-.feed-empty-state h3 {
-    text-align: center;
-    font-family: var(--heading);
-    font-size: clamp(1.5rem, 3vw, 2.5rem);
-    margin: 0 0 2rem 0;
-    color: var(--pink-0);
-}
-
-.feed-empty-state p {
-    font-family: var(--body);
-    margin: 1rem 0;
-    font-size: clamp(1rem, 2vw, 1.2rem);
-    line-height: 1.4;
-}
-
-.feed-empty-state p:last-child {
-    color: var(--pink-0);
-    margin-top: 2rem;
-}
-
-/* Animations */
-@keyframes highlight {
-    0%, 100% {
-        box-shadow: none;
-    }
-    50% {
-        box-shadow: 0 0 0 4px var(--pink-0);
-    }
-}
-
-@keyframes favourite-pop {
-    0% { transform: scale(1); }
-    50% { transform: scale(1.3); }
-    75% { transform: scale(0.9); }
-    100% { transform: scale(1); }
-}
-
-@keyframes bubble {
-    50%  { box-shadow: 19px 0 0 3px, 38px 0 0 7px, 57px 0 0 3px }
-    100% { box-shadow: 19px 0 0 0, 38px 0 0 3px, 57px 0 0 7px }
-}
-
-
-/* Artist Tattoos Grid */
-.artist-tattoos {
-    display: grid;
-    grid-template-columns: repeat(3, 1fr);
-    gap: .25em;
-}
-
-.artist-tattoos a:has(img) {
-    overflow: hidden;
-    aspect-ratio: 1;
-    background-color: var(--base-100);
-}
-.artist-tattoos a img{
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-}
-.artist-tattoos a::before,
-.artist-tattoos a::after {
-    display: none;
-}
-
-.artist-tattoos .feed-image {
-    grid-row: span 2;
-    grid-column: span 2;
-}
-
-/* Details & Summary */
-.feed-item summary {
-	width: calc(100% - 1rem);
-	height: 100%;
-	aspect-ratio: 1;
-}
-.feed-item summary .handle {
-    position: absolute;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    background-color: var(--overlay-light);
-    backdrop-filter: blur(5px);
-    border-radius: var(--innerRadius);
-    z-index: 1;
-    padding: .25rem .25rem .25rem 1.1rem;
-}
-
-.feed-item:hover summary .handle,
-.feed-item[open] summary .handle {
-    background-color: var(--overlay-pink-medium);
-    backdrop-filter: blur(5px);
-}
-.feed-item summary:after {
-    z-index: 11;
-    position: absolute;
-    bottom: .35rem;
-    right: .7rem;
-    width: 1.5rem;
-    height: 1.5rem;
-    cursor: pointer;
-}
-
-.feed-item label {
-    display: flex;
-    font-weight: normal;
-    text-transform: none;
-}
-.feed-item label .icon {
-    --w: 1.5em;
-}
-
-/* Loading Message Transitions */
-.loading .loading-message {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    overflow: hidden;
-}
-
-
-.loading .dots-wrapper {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-}
-
-.loading .loading-message p {
-    opacity: 1;
-    transform: scaleY(1);
-    transform-origin: bottom;
-    transition: opacity var(--transition-base),
-    transform var(--transition-base);
-}
-
-.loading .changing .loading-message p {
-    opacity: 0;
-    transform: scaleY(0);
-    transform-origin: top;
-}
-
-/* Media Queries */
-@media (max-width: 768px) {
-    .feed-filters .feed-controls {
-        flex-direction: column;
-        gap: 1rem;
-    }
-
-    .feed-empty-state {
-        grid-column-end: none;
-        padding: 2rem 1rem;
-        margin: 1rem;
-    }
-
-    .feed-filters details summary {
-        gap: .5rem;
-        justify-content: flex-start;
-    }
-}
-.feed-filters details summary::after {
-	order: 4;
-}
-
-*[hidden],
-*[hidden] + label{
-    display: none;
-}
-
-.feed-loader {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    gap: 1rem;
-    margin: 2rem auto 0!important;
-}
-.load-more {
-    opacity: 1;
-    display: flex;
-    align-items: center;
-    gap: 0.5rem;
-    padding: 0.75rem 1.5rem;
-    background: var(--base-200);
-    color: var(--contrast-200);
-    border: none;
-    border-radius: 4px;
-    font-size: var(--medium);
-    cursor: pointer;
-    transition: all var(--transition-base);
-}
-.load-more[hidden]{
-    opacity: 0;
-    transition: all var(--transition-base);
-}
-
-.load-more:hover {
-    background: var(--pink-0);
-    transform: translateY(-2px);
-}
-
-.load-more:focus-visible {
-    outline: 2px solid var(--pink-0);
-    outline-offset: 2px;
-}
-
-.feed-filters:not(:has(details)){
-    display: flex;
-    flex-direction: column;
-    position: relative;
-}
-.feed-filters:not(:has(details)) .favourites-toggle {
-    position: absolute;
-    top: 1.5rem;
-    left: -3.5rem;
-    z-index: 10;
-}
-@media (min-width: 768px){
-    .feed-filters:not(:has(details)) .favourites-toggle {
-        right: 0;
-        left: auto;
-    }
-}
-
-.icon.colour {
-    background: rgb(255,0,128);
-    background: linear-gradient(180deg, rgba(255,0,128,1) 0%, rgba(250,71,101,1) 14%, rgba(251,121,35,1) 28%, rgba(176,190,19,1) 42%, rgba(14,204,0,1) 56%, rgba(14,225,166,1) 70%, rgba(63,152,253,1) 84%, rgba(166,90,196,1) 100%);
-    mask-image: var(--colour);
-    -webkit-mask-image: var(--colour);
-    -webkit-mask-repeat: no-repeat;
-    -webkit-mask-size: contain;
-    mask-repeat: no-repeat;
-    mask-size: contain;
-    width: 1.25rem;
-    height: 1.25rem;
-}
-
-/* Accessibility-focused CSS */
-
-/* Focus styles - make keyboard focus visible and consistent */
-.feed-item:focus,
-.feed-item:focus-visible,
-button:focus,
-button:focus-visible,
-[role="button"]:focus,
-[role="button"]:focus-visible,
-.label-button + label:focus,
-.label-button + label:focus-visible,
-a:focus,
-a:focus-visible,
-input:focus,
-input:focus-visible,
-select:focus,
-select:focus-visible,
-textarea:focus,
-textarea:focus-visible {
-    outline: 2px solid #FF0080 !important;
-    outline-offset: 2px !important;
-    box-shadow: 0 0 0 4px rgba(255, 0, 128, 0.2) !important;
-}
-
-/* Remove focus outline for mouse users but keep it for keyboard users */
-:focus:not(:focus-visible) {
-    outline: none !important;
-    box-shadow: none !important;
-}
-
-/* Skip link for keyboard navigation */
-.skip-to-content {
-    background: #FF0080;
-    color: white;
-    height: auto;
-    left: 50%;
-    padding: 8px;
-    position: absolute;
-    transform: translateY(-100%) translateX(-50%);
-    transition: transform 0.3s;
-    width: auto;
-    z-index: 100;
-}
-
-.skip-to-content:focus {
-    transform: translateY(0%) translateX(-50%);
-}
-
-/* Loading states - ensure they're accessible */
-[aria-busy="true"] {
-    cursor: progress;
-}
-
-/* Disabled states */
-[aria-disabled="true"],
-[disabled] {
-    cursor: not-allowed;
-    opacity: 0.7;
-}
-
-/* Live region styles */
-//.live-region:not(:empty) {
-//    border: 1px solid #FF0080;
-//    padding: 10px;
-//    margin-top: 10px;
-//    background-color: rgba(255, 0, 128, 0.1);
-//}
-
-/* High contrast mode support */
-@media (forced-colors: active) {
-    .feed-item {
-        border: 1px solid CanvasText;
-    }
-
-    button,
-    [role="button"] {
-        border: 1px solid ButtonText;
-    }
-
-    button.favourite.favourited {
-        background-color: Highlight;
-        color: HighlightText;
-    }
-}
-
-/* Reduce animations for users who prefer reduced motion */
-@media (prefers-reduced-motion: reduce) {
-    *,
-    *::before,
-    *::after {
-        animation-duration: 0.01ms !important;
-        animation-iteration-count: 1 !important;
-        transition-duration: 0.01ms !important;
-        scroll-behavior: auto !important;
-    }
-
-    .feed-overlay-content,
-    .loading-dots,
-    .gallery-modal {
-        animation: none !important;
-        transition: none !important;
-    }
-
-    .feed-item {
-        transition: none !important;
-    }
-}
-
-/* Keyboard navigable feed items */
-.feed-item[tabindex="0"] {
-    position: relative;
-}
-
-.feed-item[tabindex="0"]::after {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    pointer-events: none;
-    border: 2px solid transparent;
-    transition: border-color 0.2s ease;
-}
-
-.feed-item[tabindex="0"]:focus::after {
-    border-color: #FF0080;
-}
-
-/* Highlighted item */
-.feed-item.highlighted {
-    box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1);
-    animation: highlight-pulse 2s ease-in-out;
-}
-
-@keyframes highlight-pulse {
-    0%, 100% { box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1); }
-    50% { box-shadow: 0 0 0 8px #FF0080, 0 12px 24px rgba(0, 0, 0, 0.15); }
-}
-
-/* Error states */
-.error-state {
-    padding: 2rem;
-    border: 1px solid #FF0080;
-    border-radius: 0.5rem;
-    margin: 2rem 0;
-    text-align: center;
-}
-
-.error-state h3 {
-    color: #FF0080;
-    margin-top: 0;
-}
-
-.error-state button {
-    margin-top: 1rem;
-}
-
-/* Error feedback modal */
-.error-feedback-modal {
-    padding: 2rem;
-    border: 2px solid #FF0080;
-    border-radius: 0.5rem;
-    max-width: 500px;
-    width: 100%;
-}
-
-.error-feedback-modal h2 {
-    margin-top: 0;
-    color: #FF0080;
-}
-
-.error-feedback-modal textarea {
-    width: 100%;
-    min-height: 100px;
-    margin: 1rem 0;
-    padding: 0.5rem;
-    border: 1px solid #ccc;
-    border-radius: 0.25rem;
-}
-
-.error-feedback-modal .actions {
-    display: flex;
-    justify-content: flex-end;
-    gap: 1rem;
-}
-
-.error-feedback-modal button {
-    padding: 0.5rem 1rem;
-    border: 1px solid #ccc;
-    border-radius: 0.25rem;
-    background: #f5f5f5;
-    cursor: pointer;
-}
-
-.error-feedback-modal button.primary {
-    background: #FF0080;
-    color: white;
-    border-color: #FF0080;
-}
-
-/* Dialog accessibility improvements */
-dialog::backdrop {
-    background-color: rgba(0, 0, 0, 0.5);
-}
-
-dialog.filter-dropdown {
-    max-height: 80vh;
-    overflow: auto;
-}
-
-dialog.filter-dropdown .cancel {
-    position: sticky;
-    top: 0;
-    z-index: 1;
-}
-
-/**
-Term Breadcrumbs
- */
-
-.term-divider {
-    position: relative;
-    text-align: center;
-    margin: 1rem 0;
-    border-bottom: 1px solid var(--base-200);
-}
-
-.term-divider span {
-    background: var(--base);
-    padding: 0 1rem;
-    color: var(--contrast);
-    font-size: 0.9rem;
-    position: relative;
-    top: 0.5em;
-}
-
-.common-term {
-    background: var(--base-50);
-    border-radius: var(--innerRadius);
-}
-
-.loading-indicator {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 0.5rem;
-    padding: 1rem;
-    color: var(--contrast-100);
-    font-size: 0.9rem;
-}
-
-.loading-indicator svg {
-    animation: spin 1s linear infinite;
-}
-
-.pagination-info {
-    text-align: center;
-    padding: 0.5rem;
-    font-size: 0.9rem;
-    color: var(--contrast-100);
-    border-top: 1px solid var(--base-100);
-}
-
-@keyframes spin {
-    from { transform: rotate(0deg); }
-    to { transform: rotate(360deg); }
-}
-
-
-.term-breadcrumb {
-    margin-bottom: 1rem;
-    padding: 0.5rem;
-    background: var(--base-50);
-    border-radius: 4px;
-}
-
-.back-to-parent {
-    display: flex;
-    align-items: center;
-    gap: 0.5rem;
-    border: none;
-    background: none;
-    color: var(--contrast);
-    cursor: pointer;
-    padding: 0.5rem;
-    border-radius: 4px;
-    font-size: var(--small);
-}
-
-.back-to-parent:hover {
-    background: var(--base-100);
-}
-
-.term-row {
-    display: flex;
-    align-items: center;
-    gap: 0.5rem;
-    width: 100%;
-    padding: 0.25rem 0;
-}
-
-.toggle-children {
-    border: none;
-    background: none;
-    padding: 0.25rem;
-    cursor: pointer;
-    color: var(--contrast);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    margin-left: auto;
-    border-radius: 4px;
-}
-
-.toggle-children:hover {
-    background: var(--base-50);
-}
-
-.loading-indicator {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 24px;
-    height: 24px;
-}
-
-.loading-indicator .loading {
-    width: 16px;
-    height: 16px;
-    border: 2px solid var(--base-100);
-    border-top-color: var(--contrast);
-    border-radius: 50%;
-    animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-    to { transform: rotate(360deg); }
-}
-
-.term-breadcrumb {
-    display: flex;
-    align-items: center;
-    gap: 0.5rem;
-    margin-bottom: 1rem;
-    padding: 0.5rem;
-    background: var(--base-50);
-    border-radius: 4px;
-}
-
-.term-breadcrumb .path {
-    display: flex;
-    align-items: center;
-    gap: 0.25rem;
-    flex-wrap: wrap;
-}
-
-.term-breadcrumb button {
-    border: none;
-    background: none;
-    padding: 0.25rem 0.5rem;
-    border-radius: 4px;
-    cursor: pointer;
-    color: var(--contrast);
-    font-size: var(--small);
-}
-
-.term-breadcrumb button:hover {
-    background: var(--base-100);
-}
-
-.path-separator {
-    color: var(--contrast-50);
-}
-
-.path-level {
-    white-space: nowrap;
-}
-
-.create-term-section {
-    margin-top: 2rem;
-    padding-top: 1rem;
-    border-top: 1px solid var(--base-100);
-}
-
-.suggestion-prompt {
-    font-size: var(--small);
-    color: var(--contrast-50);
-    margin-bottom: 1rem;
-}
-
-.create-term-form {
-    display: flex;
-    flex-direction: column;
-    gap: 0.5rem;
-}
-
-.form-row {
-    display: flex;
-    align-items: center;
-    gap: 0.5rem;
-}
-
-.name-row {
-    position: relative;
-}
-
-.name-row input {
-    width: 100%;
-    padding: 0.5rem;
-    border: 2px solid var(--base-100);
-    border-radius: 4px;
-    background: var(--base);
-    color: var(--contrast);
-}
-
-.name-row input:focus {
-    border-color: var(--pink-0);
-    outline: none;
-}
-
-.parent-row {
-    font-size: var(--small);
-}
-
-.parent-row label {
-    display: flex;
-    align-items: center;
-    gap: 0.5rem;
-    cursor: pointer;
-}
-
-dialog[open].gallery-modal {
-    width: calc(100vw - var(--padding) * 2);
-    height: 99vh;
-    background: var(--overlay-heavy);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-}
-
-.gallery-content {
-    position: relative;
-    max-width: 100%;
-    max-height: 100%;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    padding: 2rem;
-}
-.gallery-favourite button.favourite {
-    top: unset;
-    bottom: 1rem;
-    right: 1rem;
-}
-
-.gallery-image {
-    max-width: 100%;
-    max-height: calc(100vh - 4rem);
-    object-fit: contain;
-}
-
-.gallery-close {
-    position: absolute;
-    top: 1rem;
-    right: 1rem;
-    background: none;
-    border: none;
-    color: white;
-    cursor: pointer;
-    padding: 0.5rem;
-    z-index: 10;
-    transition: color 0.3s ease;
-}
-
-.gallery-close:hover {
-    color: #FF0080;
-}
-
-.gallery-nav {
-    position: absolute;
-    top: 50%;
-    height: 50%;
-    z-index: 5;
-    transform: translateY(-50%);
-    border: none;
-    color: var(--contrast);
-    cursor: pointer;
-    padding: 1rem;
-    transition: color 0.3s ease;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-}
-.gallery-nav:hover {
-    background-color: var(--overlay-heavy);
-}
-
-.gallery-nav:hover {
-    color: #FF0080;
-}
-
-.gallery-prev {
-    left: 1rem;
-}
-
-.gallery-next {
-    right: 1rem;
-}
-
-.gallery-counter {
-    position: absolute;
-    top: 1rem;
-    left: 1rem;
-    color: white;
-    font-size: 0.875rem;
-}
-
-.gallery-content details {
-    position:absolute;
-    bottom: 1rem;
-    left: 2rem;
-    width: calc(100% - 4rem);
-    background-color: var(--overlay-light);
-    padding: 0;
-}
-.gallery-content details:hover,
-.gallery-content details[open] {
-    background-color: var(--overlay-heavy);
-    backdrop-filter: blur(5px);
-}
-.gallery-content details[open] summary {
-    background-color: transparent;
-}
-
-
-/**
-Loading
- */
-.loading {
-	opacity: 0.7;
-}
-.loading-overlay {
-	position: fixed;
-	top: 0;
-	left: 0;
-	right: 0;
-	bottom: 0;
-	margin: 0!important;
-	max-width:100%!important;
-	background-color: var(--overlay-medium);
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	opacity: 0;
-	visibility: hidden;
-	transition: opacity 0.3s ease, visibility 0.3s ease;
-	z-index: 9999;
-}
-
-.loading-overlay.active {
-	opacity: 1;
-	visibility: visible;
-}
-
-/* Shimmer Effect */
-.loading .loading-overlay::after {
-	content: '';
-	position: absolute;
-	z-index: -1;
-	inset: 0;
-	background: linear-gradient(
-			90deg,
-			var(--shimmer)
-	);
-	animation: shimmer 3s ease-in-out infinite;
-}
-
-@keyframes shimmer {
-	0% { transform: translateX(-100%); }
-	50%, 100% { transform: translateX(100%); }
-}
-
-.loading-overlay .wrapper {
-	background-color: var(--overlay-heavy);
-	padding: 2rem;
-	border-radius: 8px;
-	text-align: center;
-	max-width: 90%;
-	width: 400px;
-	height: 300px;
-	z-index: 5;
-	display: flex;
-	justify-content:center;
-	align-items: center;
-	position: relative;
-}
-
-.upload-spinner {
-	--h: 150px;
-	--w: calc(var(--h) * 2);
-	border-top: 5px solid var(--pink-0);
-	border-radius: 50%;
-	position: absolute;
-	width: var(--w);
-	height: var(--w);
-	top: calc(50% - var(--h));
-	left: calc(50% - var(--h));
-	opacity: .25;
-	z-index: 0;
-	animation: spin 1s var(--timing) infinite;
-}
-.loading-icon {
-	height: 50px;
-	width: 50px;
-}
-	.loading-icon .icon {
-		--w: 100%;
-	}
-	.loading-icon .icon svg {
-		animation: dance 2s ease-in-out infinite;
-//		transition: color 0.3s ease;
-//	}
-///* Dancing Animation */
-@keyframes dance {
-	0%, 100% { transform: rotate(-5deg) scale(1);}
-	50% { transform: rotate(5deg) scale(1.1); }
-}
-.upload-status {
-	height: 200px;
-	width: 100%;
-	z-index: 5;
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-}
-.upload-status h3 {
-	margin: 1.5rem 0 .25rem!important;
-	color: var(--contrast-200);
-}
-
-.upload-message {
-	margin: 0;
-	max-width: 275px;
-	color: var(--contrast-100);
-	font-size: var(--small);
-}
-
-@keyframes spin {
-	0% { transform: rotate(0deg); }
-	100% { transform: rotate(360deg); }
-}
-
-/* Optional: Add a pulsing effect to the text */
-.upload-message {
-	animation: flicker 2s infinite;
-}
-
-@keyframes flicker {
-	0% { opacity: 0.6; }
-	50% { opacity: 1; }
-	100% { opacity: 0.6; }
-}
diff --git a/src/feed/view.js b/src/feed/view.js
index 6c64b6e..5e3b453 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -377,8 +377,8 @@
 			this.a11y.announceItems(0, this.store.filters['page'] >1, false);
 		}
 
-		this.ui.filters.match.hidden = window.isEmptyObject(this.taxonomyFilters);
-		this.ui.clearFilter.hidden = window.isEmptyObject(this.taxonomyFilters);
+		this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+		this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
 	}
 
 	/**
@@ -562,13 +562,21 @@
 		}
 
 		if ('ResizeObserver' in window) {
-			this.resizeObserver = new ResizeObserver(window.debounce(() => {
-				this.updateImageSizes();
-			}, 250));
+			this.resizeObserver = new ResizeObserver(() => {
+				window.debouncer.schedule(
+					'feed-update-images',
+					() => this.updateImageSizes(),
+					250
+				);
+			});
 		} else {
-			window.addEventListener('resize', window.debounce(()=> {
-				this.updateImageSizes();
-			}, 250));
+			window.addEventListener('resize', () => {
+				window.debouncer.schedule(
+					'feed-update-images',
+					() => this.updateImageSizes(),
+					250
+				);
+			});
 		}
 
 		window.addEventListener('popstate', this.popStateHandler);
diff --git a/src/forms/edit.js b/src/forms/edit.js
index 6869956..3a8387b 100644
--- a/src/forms/edit.js
+++ b/src/forms/edit.js
@@ -1,4 +1,5 @@
 /**
+ * edit.js
  * WordPress dependencies
  */
 import { __ } from '@wordpress/i18n';
diff --git a/src/forms/index.js b/src/forms/index.js
index c477d23..fc49c90 100644
--- a/src/forms/index.js
+++ b/src/forms/index.js
@@ -1,3 +1,4 @@
+//index.js
 /**
  * Registers a new block provided a unique name and an object defining its behavior.
  *
diff --git a/src/forms/save.js b/src/forms/save.js
index 83b144f..933c127 100644
--- a/src/forms/save.js
+++ b/src/forms/save.js
@@ -1,3 +1,4 @@
+//save.js
 /**
  * React hook that is used to mark the block wrapper element.
  * It provides all the necessary props like the class name.
diff --git a/src/forms/style.scss b/src/forms/style.scss
index a272c0e..d540000 100644
--- a/src/forms/style.scss
+++ b/src/forms/style.scss
@@ -21,10 +21,10 @@
 //	right: 0;
 //	width: 100%;
 //	margin: 4rem 0 0 0!important;
-//	height: var(--height);
+//	height: var(--btn);
 //	padding: 0;
 //	background-color: var(--base);
-//	box-shadow: var(--shadow);
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
 //}
 //main>* {
 //	max-width: min(768px, 90vw)!important;
@@ -32,10 +32,10 @@
 //}
 //main h1 {
 //	margin: 0!important;
-//	font-size: var(--large);
+//	font-size: var(--txt-large);
 //}
 //main h1 + p + h2 {
-//	font-size: var(--medium);
+//	font-size: var(--txt-medium);
 //	text-transform: none;
 //	margin: 0!important;
 //}
@@ -66,20 +66,20 @@
 //}
 //
 //.dashboard-nav {
-//	height: var(--height);
+//	height: var(--btn);
 //	max-width:100vw;
 //	padding: 0 .5rem;
 //}
 //.dashboard-nav ul {
-//	height: var(--height);
+//	height: var(--btn);
 //	overflow-x: auto;
 //}
 //.dashboard-nav li + li:before {
 //	display: none!important;
 //}
 //.dashboard-nav a {
-//	height: var(--height);
-//	min-width: var(--height);
+//	height: var(--btn);
+//	min-width: var(--btn);
 //	padding: 0 .75rem;
 //	color: var(--contrast)!important;
 //}
@@ -127,7 +127,7 @@
 //	left: 0;
 //	right: 0;
 //	bottom: 0;
-//	background-color: var(--overlay-medium);
+//	background-color: rgba(var(--base-rgb),var(--op-4));
 //	display: flex;
 //	align-items: center;
 //	justify-content: center;
@@ -169,7 +169,7 @@
 //.upload-message {
 //	margin: 0;
 //	color: var(--contrast-100);
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //}
 //
 //
@@ -206,7 +206,7 @@
 //	left: 0;
 //	right: 0;
 //	background-color: var(--base-100);
-//	box-shadow: var(--shadow);
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
 //	z-index: 10;
 //}
 //.form-sections ul {
@@ -256,14 +256,14 @@
 //	position: absolute;
 //	z-index: -1;
 //	top: calc(50% - (1.875rem / 2));
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //	background-color: var(--action-0);
 //	color: var(--action-contrast);
 //	padding: .25rem .5rem;
 //	border-radius: 4px;
 //	white-space: nowrap;
 //	visibility: hidden;
-//	transition: all var(--transition-base);
+//	transition: all var(--trans-base);
 //	opacity: 0;
 //}
 //.submit-container .icon {
@@ -322,7 +322,7 @@
 //	border-bottom-color: var(--action-50);
 //	border-radius: 50%;
 //	color: var(--contrast-200);
-//	transition: color .25s var(--timing) var(--function);
+//	transition: color .25s var(--trans-t) var(--trans-fn);
 //	transition-property: color, background-color, border;
 //	animation: spin 1s linear infinite;
 //}
@@ -359,7 +359,7 @@
 //	background-color: var(--base);
 //	border-radius: 4px;
 //	padding: .25rem .5rem;
-//	box-shadow: var(--subtle);
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw-subtle);
 //}
 //.field {
 //	margin: 3rem .5rem;
@@ -431,7 +431,7 @@
 //	margin: 1rem 0;
 //}
 //.tab-content h2 {
-//	font-size: var(--large);
+//	font-size: var(--txt-large);
 //	margin: 0!important;
 //}
 //.tab-content .tab-navigation,
@@ -511,7 +511,7 @@
 //	flex-direction: column;
 //}
 //.item.news h3 {
-//	font-size: var(--medium);
+//	font-size: var(--txt-medium);
 //	margin: 0!important;
 //}
 //.item.news summary .image {
@@ -562,14 +562,14 @@
 //
 //details.uploader .file-upload-container {
 //	margin: 1rem var(--mr) 1rem var(--ml);
-//	max-width: var(--maxWidth);
+//	max-width: var(--content);
 //}
 //details .no-items {
 //	text-align: center;
 //	font-style: italic;
 //	background-color: var(--base-50);
-//	padding: var(--outerPadding);
-//	border-radius: var(--innerRadius);
+//	padding: var(--p-outer);
+//	border-radius: var(--radius);
 //}
 //
 //.controls {
@@ -642,7 +642,7 @@
 //	border: 1px solid var(--base-200);
 //	border-radius: 4px;
 //	font-size: .875rem;
-//	transition: border-color var(--transition-base);
+//	transition: border-color var(--trans-base);
 //	margin-bottom: .5rem;
 //}
 //.filter-toggle .icon {
@@ -664,7 +664,7 @@
 //.create-item {
 //	left: auto!important;
 //	right: 1rem;
-//	bottom: calc(var(--height) + 1rem)!important;
+//	bottom: calc(var(--btn) + 1rem)!important;
 //}
 //body:has(.group-display:not([hidden])) button.create-item{
 //	display: none;
@@ -673,7 +673,7 @@
 //.item-grid {
 //	--padding: 0;
 //	padding: var(--padding);
-//	transition: padding var(--transition-base);
+//	transition: padding var(--trans-base);
 //}
 //.uploader .groups,
 //.item-grid:not(.list-view) {
@@ -687,7 +687,7 @@
 //}
 //.item-grid.empty div {
 //	text-align: center;
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //	background-color: var(--base-100);
 //}
 //.item-grid.empty h3 .icon {
@@ -725,13 +725,13 @@
 //	align-items: center;
 //	top: .125rem;
 //	padding: 0!important;
-//	border-radius: var(--innerRadius);
-//	background-color: var(--overlay-light);
+//	border-radius: var(--radius);
+//	background-color: rgba(var(--base-rgb),var(--op-3));
 //	color: var(--base-200);
 //}
 //.item-grid:not(.list-view) button.favourite:hover,
 //.item-grid:not(.list-view) .item-select label:hover {
-//	background-color: var(--overlay-heavy);
+//	background-color: rgba(var(--base-rgb),var(--op-6));
 //	color: var(--contrast);
 //}
 //.item-grid:not(.list-view) .item-select label::before {
@@ -812,7 +812,7 @@
 //.item-grid .item-info h3 {
 //	margin: 0!important;
 //	text-align: right;
-//	font-size: var(--medium);
+//	font-size: var(--txt-medium);
 //	text-transform: none;
 //}
 //.item-grid .item-info a {
@@ -870,7 +870,7 @@
 //	left: 100%;
 //	border: 1px solid transparent;
 //	background-color: var(--action-50);
-//	box-shadow:var(--shadow);
+//	box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);
 //	z-index: 5;
 //}
 //.selection-container #save-changes:hover {
@@ -882,7 +882,7 @@
 //.group {
 //	padding: 1rem .66rem;
 //	background-color: var(--base-50);
-//	border-radius: var(--outerRadius);
+//	border-radius: var(--radius-outer);
 //}
 //.group.empty {
 //	aspect-ratio: 1;
@@ -908,7 +908,7 @@
 //.group .items {
 //	margin-top: 1rem;
 //	padding: 1rem;
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //	background-color: var(--base);
 //}
 //.group .item-actions {
@@ -1020,12 +1020,12 @@
 //	grid-template-columns: repeat(3, 1fr);
 //	padding: .5rem;
 //	background-color: var(--base-100);
-//	border-radius: var(--outerRadius);
+//	border-radius: var(--radius-outer);
 //}
 //.gallery-preview .preview-item {
 //	padding: .5rem;
 //	background-color: var(--base);
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //}
 //
 //.gallery .preview-item:hover .move-image {
@@ -1106,7 +1106,7 @@
 //.list-card {
 //	background-color: var(--base-50);
 //	padding: 1rem;
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //}
 //.list-header {
 //	display: flex;
@@ -1121,7 +1121,7 @@
 //.list-card h3,
 //.list-header h2 {
 //	margin: 0!important;
-//	font-size: var(--large);
+//	font-size: var(--txt-large);
 //}
 //.list-actions {
 //	display: flex;
@@ -1132,7 +1132,7 @@
 //	justify-content: flex-end;
 //}
 //.create-list-btn {
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //}
 //.meta-stats {
 //	display: flex;
@@ -1152,13 +1152,13 @@
 //	max-height: 0;
 //	overflow: hidden;
 //	transform: scaleY(0);
-//	transition: max-height var(--timing) var(--function);
+//	transition: max-height var(--trans-t) var(--trans-fn);
 //	transition-property: max-height, transform;
 //}
 //.image-display.has-image {
 //	max-height: 100%;
 //	transform: scaleY(1);
-//	transition: max-height var(--timing) var(--function);
+//	transition: max-height var(--trans-t) var(--trans-fn);
 //	transition-property: max-height, transform;
 //}
 //.file-upload-container {
@@ -1177,7 +1177,7 @@
 //}
 //.file-upload-wrapper h2 {
 //	margin: 0!important;
-//	font-size: var(--large);
+//	font-size: var(--txt-large);
 //}
 //
 //.file-upload-wrapper:hover,
@@ -1219,7 +1219,7 @@
 //	max-height: 0;
 //	overflow: hidden;
 //	transform: scaleY(0);
-//	transition: max-height var(--timing) var(--function), transform var(--timing) var(--function);
+//	transition: max-height var(--trans-t) var(--trans-fn), transform var(--trans-t) var(--trans-fn);
 //	transition-property: max-height, transform;
 //}
 //
@@ -1265,7 +1265,7 @@
 //	height: fit-content;
 //	padding: var(--padding);
 //	max-width: calc(100% - (var(--padding) * 2));
-//	transition: padding var(--transition-base);
+//	transition: padding var(--trans-base);
 //}
 //.selecting .item {
 //	opacity: .666;
@@ -1322,7 +1322,7 @@
 //	max-height: 0;
 //	overflow: hidden;
 //	transform: scaleY(0);
-//	transition: transform var(--timing) var(--function);
+//	transition: transform var(--trans-t) var(--trans-fn);
 //	transition-property: transform, max-height;
 //	transform-origin: top;
 //	display: flex!important;
@@ -1335,7 +1335,7 @@
 //	max-width: 100%;
 //	max-height: 100%;
 //	transform: scaleY(1);
-//	transition: transform var(--timing) var(--function);
+//	transition: transform var(--trans-t) var(--trans-fn);
 //	transition-property: transform, max-height;
 //	overflow:visible;
 //	transform-origin: top;
@@ -1347,7 +1347,7 @@
 //}
 //
 //.selected-count {
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //	font-style: italic;
 //	font-weight: normal;
 //	margin-left: 1rem;
@@ -1378,7 +1378,7 @@
 //	gap: .5rem;
 //	background-color: var(--base);
 //	padding: .5rem;
-//	border-radius: var(--outerRadius);
+//	border-radius: var(--radius-outer);
 //}
 //.bulk-edit-modal .selected input[type=checkbox] {
 //	position: absolute;
@@ -1405,7 +1405,7 @@
 //	max-width: 100%;
 //	object-fit: cover;
 //	aspect-ratio: 1;
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //}
 //
 //dialog .term-list {
@@ -1413,7 +1413,7 @@
 //}
 //.pagination-info {
 //	position: sticky;
-//	background-color: var(--overlay-heavy);
+//	background-color: rgba(var(--base-rgb),var(--op-6));
 //	top: 0;
 //}
 //.pagination-info:empty {
@@ -1431,8 +1431,8 @@
 //	padding: .25rem;
 //	gap: 1rem;
 //	background-color: var(--base);
-//	border-top-left-radius: var(--innerRadius);
-//	border-top-right-radius: var(--innerRadius);
+//	border-top-left-radius: var(--radius);
+//	border-top-right-radius: var(--radius);
 //	border-bottom: 4px solid var(--base-50);
 //}
 //.ql-toolbar .ql-formats {
@@ -1442,8 +1442,8 @@
 //.editor-container .ql-container {
 //	--padding: 1rem;
 //	background-color: var(--base);
-//	border-bottom-left-radius: var(--innerRadius);
-//	border-bottom-right-radius: var(--innerRadius);
+//	border-bottom-left-radius: var(--radius);
+//	border-bottom-right-radius: var(--radius);
 //	height: fit-content;
 //	padding: 2px;
 //}
@@ -1472,7 +1472,7 @@
 //	transform: translateY(10px);
 //	background-color: var(--base-100);
 //	border: 1px solid var(--base);
-//	box-shadow: 0px 0px 5px var(--overlay-heavy);
+//	box-shadow: 0px 0px 5px rgba(var(--base-rgb),var(--op-6));
 //	color: var(--contrast);
 //	padding: 5px 12px;
 //	white-space: nowrap;
@@ -1486,7 +1486,7 @@
 //.all-filters {
 //	position: relative;
 //	background-color: var(--base);
-//	border-radius: var(--outerRadius);
+//	border-radius: var(--radius-outer);
 //	padding: .5rem;
 //	display: flex;
 //	flex-direction: column;
@@ -1592,15 +1592,15 @@
 //.item-grid .item-actions button {
 //	width: 2em;
 //	height: 2em;
-//	border-radius: var(--innerRadius);
-//	background-color: var(--overlay-light);
+//	border-radius: var(--radius);
+//	background-color: rgba(var(--base-rgb),var(--op-3));
 //	display: flex;
 //	justify-content: center;
 //	align-items: center;
 //}
 //.item-grid .item-actions button:focus,
 //.item-grid .item-actions button:hover {
-//	background-color: var(--overlay-heavy);
+//	background-color: rgba(var(--base-rgb),var(--op-6));
 //	color: var(--action-0);
 //}
 //
@@ -1614,7 +1614,7 @@
 //		flex-wrap: wrap;
 //	}
 //	.list-card h3 {
-//		font-size: var(--medium);
+//		font-size: var(--txt-medium);
 //	}
 //	.item-grid.list-view .item {
 //		align-items: center;
@@ -1651,7 +1651,7 @@
 //
 //.common-term {
 //	background: var(--base-50);
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //}
 //
 //.loading-indicator {
@@ -1699,7 +1699,7 @@
 //	cursor: pointer;
 //	padding: .5rem;
 //	border-radius: 4px;
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //}
 //
 //.back-to-parent:hover {
@@ -1776,7 +1776,7 @@
 //	border-radius: 4px;
 //	cursor: pointer;
 //	color: var(--contrast);
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //}
 //
 //.term-breadcrumb button:hover {
@@ -1801,7 +1801,7 @@
 //}
 //
 //.suggestion-prompt {
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //	color: var(--contrast-50);
 //	margin-bottom: 1rem;
 //}
@@ -1837,7 +1837,7 @@
 //}
 //
 //.parent-row {
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //}
 //
 //.parent-row label {
@@ -1857,7 +1857,7 @@
 //	background: var(--action-0);
 //	color: var(--base);
 //	cursor: pointer;
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //	transition: all .2s ease;
 //}
 //
@@ -1919,11 +1919,11 @@
 //
 //	/* Animation */
 //	transform-origin: top;
-//	transition: all .2s var(--function);
+//	transition: all .2s var(--trans-fn);
 //}
 //
 //.create-term-form:not([hidden]) {
-//	animation: slideDown .2s var(--function);
+//	animation: slideDown .2s var(--trans-fn);
 //}
 //
 //.create-term-form[hidden] {
@@ -2000,7 +2000,7 @@
 //}
 //
 //.dashboard .queue-status-panel {
-//	bottom: calc(var(--height) + 1rem);
+//	bottom: calc(var(--btn) + 1rem);
 //}
 //.dashboard .queue-status-toggle {
 //	bottom: 0;
@@ -2028,7 +2028,7 @@
 //
 //p.hint {
 //	margin: 0 0 .5rem 0;
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //	font-style: italic;
 //}
 //.item-grid + .hint {
@@ -2051,7 +2051,7 @@
 //	transition: all .3s ease;
 //}
 //.upload-item:hover {
-//	box-shadow: var(--shadow);
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
 //	transform: translateY(-2px);
 //}
 //.upload-item[data-status=processing] {
@@ -2074,8 +2074,8 @@
 //	position: absolute;
 //	bottom: .25rem;
 //	right: .25rem;
-//	background-color: var(--overlay-light);
-//	box-shadow: var(--shadow);
+//	background-color: rgba(var(--base-rgb),var(--op-3));
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
 //	border-radius: 50%;
 //}
 //.upload-item img {
@@ -2094,7 +2094,7 @@
 //	left: 0;
 //	right: 0;
 //	bottom: 0;
-//	background: var(--overlay-heavy);
+//	background: rgba(var(--base-rgb),var(--op-6));
 //	display: flex;
 //	flex-direction: column;
 //	justify-content: space-between;
@@ -2114,11 +2114,11 @@
 //
 //.submit-uploads {
 //	position: fixed;
-//	bottom: calc(var(--height) + 1rem);
+//	bottom: calc(var(--btn) + 1rem);
 //	right: 1rem;
 //	background-color: var(--base);
-//	height: var(--height);
-//	box-shadow: var(--shadow);
+//	height: var(--btn);
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
 //}
 ///*** UPLOADER GROUPS ***/
 //.group-display {
@@ -2129,7 +2129,7 @@
 //.preview-actions {
 //	position: sticky;
 //	padding: .5rem;
-//	top: calc(var(--height) + .25rem);
+//	top: calc(var(--btn) + .25rem);
 //	left: 0;
 //	background-color: var(--base-50);
 //	z-index: 5;
@@ -2215,7 +2215,7 @@
 //.item-grid .upload-item summary {
 //	display: flex;
 //	align-items: center;
-//	font-size: var(--small);
+//	font-size: var(--txt-x-small);
 //	gap: .5rem;
 //	text-transform: uppercase;
 //}
@@ -2267,7 +2267,7 @@
 //	justify-content: center;
 //	align-items: center;
 //	border: 2px dashed var(--action-200);
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //	margin: 10px 0;
 //	cursor: pointer;
 //	transition: all .2s ease;
@@ -2283,7 +2283,7 @@
 //	grid-template-columns: repeat(3, 1fr);
 //	gap: .5rem;
 //	border: 2px dashed var(--action-200);
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //	margin: 10px 0;
 //	cursor: pointer;
 //	transition: all .2s ease;
@@ -2301,7 +2301,7 @@
 //
 //.upload-group {
 //	background-color: var(--base-100);
-//	border-radius: var(--innerRadius);
+//	border-radius: var(--radius);
 //	border: 1px solid var(--contrast-200);
 //}
 //.group-actions {
@@ -2319,8 +2319,8 @@
 //
 ///** RESTORE FROM CACHE **/
 //.restore-notification {
-//	border-radius: var(--innerRadius);
-//	box-shadow: var(--shadow);
+//	border-radius: var(--radius);
+//	box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
 //	padding: 1rem;
 //	background: var(--base-200);
 //	border: 1px solid var(--contrast-200);
@@ -2355,7 +2355,7 @@
 //	background-color: transparent;
 //	filter: grayscale(50);
 //	opacity: .8;
-//	transition: padding var(--transition-base);
+//	transition: padding var(--trans-base);
 //	transition-property: padding, background-color;
 //	cursor: pointer;
 //}
@@ -2369,8 +2369,8 @@
 //.upload-item .featured + label {
 //	width: 2em;
 //	height: 2em;
-//	border-radius: var(--innerRadius);
-//	background-color: var(--overlay-light);
+//	border-radius: var(--radius);
+//	background-color: rgba(var(--base-rgb),var(--op-3));
 //	display: flex;
 //	justify-content: center;
 //	align-items: center;
@@ -2445,9 +2445,9 @@
 ///*.file-upload-container {*/
 ///*    position: relative;*/
 ///*    padding: .25rem;*/
-///*    transition: border-color var(--transition-base),*/
-///*    background-color var(--transition-base),*/
-///*    padding var(--transition-base);*/
+///*    transition: border-color var(--trans-base),*/
+///*    background-color var(--trans-base),*/
+///*    padding var(--trans-base);*/
 ///*}*/
 //
 ///*.file-upload-container.dragover {*/
@@ -3276,7 +3276,7 @@
 ///*    gap: 1rem;*/
 ///*    padding: .5rem 1rem;*/
 ///*    background-color: var(--action-50);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    color: var(--contrast);*/
 ///*    font-size: .9rem;*/
 ///*}*/
@@ -3286,7 +3286,7 @@
 ///*    border: 1px solid rgba(255, 255, 255, .3);*/
 ///*    color: inherit;*/
 ///*    padding: .25rem .5rem;*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    display: flex;*/
 ///*    align-items: center;*/
 ///*    gap: .25rem;*/
@@ -3307,7 +3307,7 @@
 ///*    align-items: center;*/
 ///*    padding: 1rem;*/
 ///*    background-color: var(--base-100);*/
-///*    border-radius: var(--outerRadius);*/
+///*    border-radius: var(--radius-outer);*/
 ///*    margin-bottom: 1rem;*/
 ///*}*/
 //
@@ -3315,7 +3315,7 @@
 ///*.upload-item {*/
 ///*    position: relative;*/
 ///*    background: var(--base);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    overflow: hidden;*/
 ///*    cursor: pointer;*/
 ///*    transition: transform .2s ease, box-shadow .2s ease;*/
@@ -3323,7 +3323,7 @@
 //
 ///*.upload-item:hover {*/
 ///*    transform: translateY(-2px);*/
-///*    box-shadow: var(--shadow);*/
+///*    box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);*/
 ///*}*/
 //
 ///*.upload-item[draggable="true"] {*/
@@ -3413,7 +3413,7 @@
 ///*!* Group Enhancements *!*/
 ///*.upload-group {*/
 ///*    background: var(--base-50);*/
-///*    border-radius: var(--outerRadius);*/
+///*    border-radius: var(--radius-outer);*/
 ///*    padding: 1rem;*/
 ///*    margin-bottom: 1rem;*/
 ///*    border: 2px solid transparent;*/
@@ -3468,7 +3468,7 @@
 ///*.group-actions button {*/
 ///*    background: var(--base);*/
 ///*    border: 1px solid var(--base-200);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: .5rem;*/
 ///*    cursor: pointer;*/
 ///*    transition: all .2s ease;*/
@@ -3495,7 +3495,7 @@
 //
 ///*.group-drop-zone {*/
 ///*    border: 2px dashed var(--base-300);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: 2rem;*/
 ///*    text-align: center;*/
 ///*    color: var(--text-muted);*/
@@ -3520,7 +3520,7 @@
 ///*.group-item {*/
 ///*    position: relative;*/
 ///*    aspect-ratio: 1;*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    overflow: hidden;*/
 ///*    background: var(--base);*/
 ///*    transition: transform .2s ease;*/
@@ -3585,7 +3585,7 @@
 ///*!* Empty Group State *!*/
 ///*.empty-group {*/
 ///*    border: 4px dashed var(--base-200);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: 2rem;*/
 ///*    text-align: center;*/
 ///*    color: var(--text-muted);*/
@@ -3607,7 +3607,7 @@
 ///*!* Sidebar *!*/
 ///*.sidebar {*/
 ///*    background: var(--base-50);*/
-///*    border-radius: var(--outerRadius);*/
+///*    border-radius: var(--radius-outer);*/
 ///*    padding: 1.5rem;*/
 ///*    min-height: 400px;*/
 ///*}*/
@@ -3632,7 +3632,7 @@
 ///*    background: var(--action-50);*/
 ///*    color: var(--contrast);*/
 ///*    border: none;*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: .75rem;*/
 ///*    margin-bottom: 1rem;*/
 ///*    cursor: pointer;*/
@@ -3666,7 +3666,7 @@
 ///*    gap: 1rem;*/
 ///*    padding: 1rem;*/
 ///*    background: var(--base-100);*/
-///*    border-radius: var(--outerRadius);*/
+///*    border-radius: var(--radius-outer);*/
 ///*    min-height: 200px;*/
 ///*}*/
 //
@@ -3677,7 +3677,7 @@
 ///*    color: var(--text-muted);*/
 ///*    padding: 2rem;*/
 ///*    border: 2px dashed var(--base-300);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*}*/
 //
 ///*!* File Upload Container *!*/
@@ -3751,7 +3751,7 @@
 ///*    background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);*/
 ///*    border: 1px solid #ffc107;*/
 ///*    border-left: 4px solid #ff6b35;*/
-///*    border-radius: var(--outerRadius);*/
+///*    border-radius: var(--radius-outer);*/
 ///*    padding: 1.5rem;*/
 ///*    margin-bottom: 1.5rem;*/
 ///*    box-shadow: 0 4px 12px rgba(255, 107, 53, .15);*/
@@ -3814,7 +3814,7 @@
 ///*.restore-message .warning {*/
 ///*    background: rgba(220, 53, 69, .1);*/
 ///*    border: 1px solid rgba(220, 53, 69, .2);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: .5rem .75rem;*/
 ///*    margin-top: .75rem;*/
 ///*    font-size: .9rem;*/
@@ -3835,7 +3835,7 @@
 ///*    background: #dc3545;*/
 ///*    color: white;*/
 ///*    border: none;*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: .5rem 1rem;*/
 ///*    font-size: .9rem;*/
 ///*    font-weight: 500;*/
@@ -3858,7 +3858,7 @@
 ///*    background: transparent;*/
 ///*    border: 1px solid #6c5419;*/
 ///*    color: #6c5419;*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: .5rem;*/
 ///*    cursor: pointer;*/
 ///*    transition: all .2s ease;*/
@@ -3877,7 +3877,7 @@
 ///*!* Start Over Confirmation Dialog *!*/
 ///*.start-over-confirmation {*/
 ///*    border: none;*/
-///*    border-radius: var(--outerRadius);*/
+///*    border-radius: var(--radius-outer);*/
 ///*    padding: 0;*/
 ///*    box-shadow: 0 10px 30px rgba(0, 0, 0, .3);*/
 ///*    max-width: 500px;*/
@@ -3892,7 +3892,7 @@
 ///*.confirmation-content {*/
 ///*    padding: 2rem;*/
 ///*    background: var(--base);*/
-///*    border-radius: var(--outerRadius);*/
+///*    border-radius: var(--radius-outer);*/
 ///*}*/
 //
 ///*.confirmation-content h3 {*/
@@ -3937,7 +3937,7 @@
 ///*    background: #dc3545;*/
 ///*    color: white;*/
 ///*    border: none;*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: .75rem 1.5rem;*/
 ///*    font-weight: 500;*/
 ///*    cursor: pointer;*/
@@ -3952,7 +3952,7 @@
 ///*    background: var(--base-100);*/
 ///*    color: var(--text);*/
 ///*    border: 1px solid var(--base-300);*/
-///*    border-radius: var(--innerRadius);*/
+///*    border-radius: var(--radius);*/
 ///*    padding: .75rem 1.5rem;*/
 ///*    cursor: pointer;*/
 ///*    transition: all .2s ease;*/
@@ -4861,10 +4861,10 @@
 //		display: flex;
 //		flex-direction: column;
 //		gap: 0;
-//		height: calc(100vh - var(--height) - var(--height));
+//		height: calc(100vh - var(--btn) - var(--btn));
 //		position: fixed;
-//		top: var(--height);
-//		bottom: var(--height);
+//		top: var(--btn);
+//		bottom: var(--btn);
 //		left: 0;
 //		right: 0;
 //		z-index:999;
@@ -4961,14 +4961,14 @@
 //	/* Upload button - fixed at bottom */
 //	.submit-uploads {
 //		position: fixed !important;
-//		bottom: calc(var(--height) + .5rem);
+//		bottom: calc(var(--btn) + .5rem);
 //		right: .5rem;
 //		z-index: 20;
 //		height: 3rem;
 //		font-size: 1.1rem;
 //		font-weight: 600;
-//		box-shadow: var(--shadow);
-//		border-radius: var(--outerRadius);
+//		box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+//		border-radius: var(--radius-outer);
 //	}
 //
 //	.submit-uploads:hover {
@@ -4978,7 +4978,7 @@
 //
 //	/* Enhanced upload items for mobile */
 //	.upload-item {
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //		overflow: hidden;
 //		background: var(--base);
 //		border: 1px solid var(--base-200);
@@ -4998,7 +4998,7 @@
 //		min-width: 44px;
 //		min-height: 44px;
 //		padding: .75rem;
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //	}
 //
 //	/* Better checkbox targets */
@@ -5008,13 +5008,13 @@
 //		display: flex;
 //		align-items: center;
 //		justify-content: center;
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //	}
 //
 //	/* Enhanced group styling for mobile */
 //	.upload-group {
 //		background: var(--base-100);
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //		border: 1px solid var(--base-200);
 //		padding: 1rem;
 //		margin-bottom: 1rem;
@@ -5043,7 +5043,7 @@
 //		min-height: 44px;
 //		padding: .5rem .75rem;
 //		font-size: .9rem;
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //	}
 //
 //	.upload-group .item-grid.group {
@@ -5087,7 +5087,7 @@
 //		flex: 1;
 //		padding: .25rem;
 //		font-size: .9rem;
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //	}
 //
 //	/* Enhanced dragging states for mobile */
@@ -5160,7 +5160,7 @@
 //	.upload-item details summary {
 //		padding: .75rem;
 //		background: var(--base-100);
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //		cursor: pointer;
 //		display: flex;
 //		align-items: center;
@@ -5171,7 +5171,7 @@
 //	}
 //
 //	.upload-item details[open] summary {
-//		border-radius: var(--innerRadius) var(--innerRadius) 0 0;
+//		border-radius: var(--radius) var(--radius) 0 0;
 //		border-bottom: 1px solid var(--base-200);
 //	}
 //
@@ -5180,7 +5180,7 @@
 //	.upload-meta textarea {
 //		padding: .75rem;
 //		font-size: 16px; /* Prevents zoom on iOS */
-//		border-radius: var(--innerRadius);
+//		border-radius: var(--radius);
 //		border: 2px solid var(--base-200);
 //		transition: border-color .2s ease;
 //	}
@@ -5546,7 +5546,7 @@
 //
 //form .tabs {
 //	position: sticky;
-//	top: calc(var(--height) + 2rem);
+//	top: calc(var(--btn) + 2rem);
 //	left: 0;
 //	right: 0;
 //	z-index: 50;
diff --git a/src/forms/view.js b/src/forms/view.js
index d8f6948..e4a88ea 100644
--- a/src/forms/view.js
+++ b/src/forms/view.js
@@ -1,4 +1,5 @@
 /**
+ * view.js
  * Frontend JavaScript for the Form Block
  * Handles form validation and submission
  */
@@ -7,7 +8,7 @@
 		this.controller = new window.jvbForm();
 
 		document.querySelectorAll('.jvb-form-block form').forEach(form => {
-			this.controller.registerForm(form);
+			this.controller.registerForm(form, {autosave: true});
 		});
 
 		this.controller.subscribe((event, data) => {
@@ -18,15 +19,9 @@
 	}
 
 	async handleFormSubmission(data) {
-		let [
-			formId,
-			formConfig,
-			formData
-		] = [
-			data.formId,
-			data.config,
-			data.data
-		];
+		let formId = data.formId;
+		let formConfig = data.config;
+		let formData = data.fullData;
 		let form = formConfig.element;
 
 		let headers = {
@@ -34,51 +29,30 @@
 			'Content-Type': 'application/json'
 		};
 
-		data['form_type'] = formId;
-		data['form_id'] = form.id;
 		let block = form.closest('.jvb-form-block');
 		this.controller.showFormStatus(formId, 'uploading');
+
 		try {
-			const response = await fetch (`${jvbSettings.api}forms`,
-				{
-					method: 'POST',
-					headers,
-					body: JSON.stringify(formData)
-				});
+			const response = await fetch(`${jvbSettings.api}forms`, {
+				method: 'POST',
+				headers,
+				body: JSON.stringify(formData)
+			});
+
 			if (!response.ok) {
 				this.controller.showFormStatus(formId, 'error');
 				const errorData = await response.json().catch(() => ({}));
-				throw new Error(errorData.message|| `Request failed with status ${response.status}`);
+				throw new Error(errorData.message || `Request failed with status ${response.status}`);
 			}
+
 			this.controller.showFormStatus(formId, 'submitted');
-
 			this.controller.showSummary(formId, '.jvb-form-block');
-			this.controller.store.delete(formId);
-
 		} catch (error) {
 			throw error;
+		} finally {
+			this.controller.store.delete(formId);
 		}
 	}
-
-	updateUI(response, block) {
-
-		let summary = window.getTemplate('formSummary');
-		summary.querySelector('h2').textContent = 'Success!';
-		console.log('Form Response: ', response);
-		console.log(summary);
-		for (let [key, value] of Object.entries(response)) {
-			let item = summary.querySelector(`#${key}`);
-			if (item) {
-				let title = item.querySelector('h4');
-				if (title.innerText.includes('%s')) {
-					title.innerHTML = title.replace('%s', '<b>'+value+'</b>');
-				} else {
-					item.querySelector('div').innerHTML = value;
-				}
-			}
-		}
-		block.append(summary);
-	}
 }
 
 document.addEventListener('DOMContentLoaded', function() {
diff --git a/src/glossary/style.scss b/src/glossary/style.scss
index 0b9e16c..5b0d119 100644
--- a/src/glossary/style.scss
+++ b/src/glossary/style.scss
@@ -16,7 +16,9 @@
 	> ul {
 		--dir: column;
 		--align: flex-start;
+		--justify: flex-start;
 		touch-action: pan-y;
+		max-height: 100%;
 		height: 100%;
 		width: 100%;
 		overflow: hidden auto;
@@ -27,15 +29,14 @@
 	}
 	a {
 		--justify: center;
-		background-color: var(--overlay-heavy);
+		background-color: rgba(var(--base-rgb),var(--op-6));
 		word-wrap: anywhere;
 		white-space: wrap;
-		transition: background-color 0.2s ease;
 	}
 	a:hover,
 	a:focus,
 	a.active {
-		background-color: rgba(var(--action-rgb), var(--rgb-heavy));
+		background-color: rgba(var(--action-rgb), var(--op-6));
 		color: var(--action-contrast);
 	}
 }
@@ -47,10 +48,9 @@
 .glossary dt {
 	position: relative;
 	left: 0;
-	transition: margin var(--transition-base),
-	left var(--transition-base),
-	color var(--transition-base),
-	width var(--transition-base);
+	transition: margin var(--trans-base),
+	left var(--trans-base),
+	width var(--trans-base);
 }
 .glossary dt:target,
 .glossary dt.active {
@@ -66,25 +66,23 @@
 
 main header,
 dl.glossary {
-	margin-right:0;
-	margin-left:0;
+	grid-column: full;
 	padding: 0 var(--navWidth) 0 2rem;
-	max-width: 100vw;
 	@media (min-width:768px) {
 		margin-left: auto;
-		max-width: var(--maxWidth);
+		max-width: var(--content);
 		margin-right: var(--navWidth);
-		padding-right: var(--height);
+		padding-right: var(--btn);
 	}
 }
 
 @media (max-width: 768px) {
 	.glossary {
 		h2 {
-			font-size: var(--medium);
+			font-size: var(--txt-medium);
 		}
 		p {
-			font-size: var(--small);
+			font-size: var(--txt-x-small);
 		}
 	}
 	.glossary-index {
@@ -92,13 +90,13 @@
 			height: fit-content;
 		}
 		a {
-			font-size: var(--small);
+			font-size: var(--txt-x-small);
 			padding: .25rem;
 			min-height: 2em;
 		}
 	}
 
 	body:has(.glossary) h1 {
-		font-size: var(--xxlarge);
+		font-size: var(--txt-xx-large);
 	}
 }
diff --git a/src/gmbreviews/style.scss b/src/gmbreviews/style.scss
index b7804c9..bca3db1 100644
--- a/src/gmbreviews/style.scss
+++ b/src/gmbreviews/style.scss
@@ -1,7 +1,7 @@
 .gmb-reviews {
 	max-width: none;
 	> .row.btw {
-		max-width:var(--alignWide);
+		max-width:var(--wide);
 		.button {
 			width: 100%;
 			height: max-content;
@@ -85,7 +85,7 @@
 	}
 	article {
 		padding: 1rem;
-		border-radius: var(--outerRadius);
+		border-radius: var(--radius-outer);
 		background-color: var(--base);
 		header {
 			--align: center;
diff --git a/src/list/style.scss b/src/list/style.scss
index d90019e..f7db6e2 100644
--- a/src/list/style.scss
+++ b/src/list/style.scss
@@ -22,7 +22,7 @@
             padding: .5rem 1rem;
             display: flex;
             justify-content: space-between;
-            border-radius: var(--innerRadius);
+            border-radius: var(--radius);
         }
         > ul {
             display: flex;
diff --git a/src/summary/style.scss b/src/summary/style.scss
index e4a7284..b182fe9 100644
--- a/src/summary/style.scss
+++ b/src/summary/style.scss
@@ -11,8 +11,8 @@
 }
 
 header + details {
-	margin: 1.5rem var(--mr) 3rem var(--ml)!important;
-	max-width: var(--alignMed);
+	margin: 1.5rem auto 3rem!important;
+	max-width: var(--wide);
 }
 
 main {
diff --git a/src/timeline/style.scss b/src/timeline/style.scss
index 2f6ac8d..8545b52 100644
--- a/src/timeline/style.scss
+++ b/src/timeline/style.scss
@@ -6,7 +6,8 @@
 }
 
 #at-a-glance {
-	max-width: var(--alignWide);
+	max-width: var(--wide);
+	margin: 0 auto;
 	--gap: 0;
 	img {
 		width: 100%;
@@ -14,7 +15,7 @@
 		border: 2px solid var(--action-0);
 	}
 	h3 {
-		font-size: var(--small);
+		font-size: var(--txt-x-small);
 	}
 	.before {
 		img {
@@ -41,7 +42,7 @@
 	max-width: 100vw;
 	position: relative;
 	overflow: hidden;
-	.open-gallery {
+	img {
 		width: 40%;
 		border-radius: 4px;
 		position: sticky;
@@ -53,7 +54,7 @@
 		position: relative;
 		h2 {
 			margin: 0 0 .5rem;
-			font-size: var(--medium);
+			font-size: var(--txt-medium);
 			position: relative;
 			.icon {
 				--w: 2.5rem;
@@ -91,12 +92,12 @@
 @media (min-width:768px) {
 	#at-a-glance {
 		h3  {
-			font-size: var(--xlarge);
+			font-size: var(--txt-x-large);
 		}
 	}
 	.timeline-point.timeline-point {
 		--gap: 4rem;
-		.open-gallery {
+		img {
 			width: 50%;
 		}
 		.info {
@@ -117,7 +118,7 @@
 
 			time {
 				text-transform: uppercase;
-				font-size: var(--small);
+				font-size: var(--txt-x-small);
 			}
 		}
 		&::before,
diff --git a/src/video/block.json b/src/video/block.json
index 18bbd6d..36469df 100644
--- a/src/video/block.json
+++ b/src/video/block.json
@@ -91,6 +91,7 @@
 	},
 	"textdomain": "jvb",
 	"editorScript": "file:./index.js",
+	"viewScript": "file:./view.js",
 	"editorStyle": "file:./index.css",
 	"style": "file:./style-index.css"
 }
diff --git a/src/video/style.scss b/src/video/style.scss
index 43841af..9782fdb 100644
--- a/src/video/style.scss
+++ b/src/video/style.scss
@@ -78,18 +78,18 @@
 				.media-text {
 					--align: flex-start;
 					gap: 3rem;
-					max-width: var(--maxWidth);
+					max-width: var(--content);
 				}
 			}
 			.media-text > div {
 				width: fit-content;
 			}
 		.buttons a {
-			font-weight: 500;
+			font-weight: var(--fw-h-bold);
 			color: var(--action-contrast);
 			border-color: var(--action-contrast);
 			&:visited {
-				color: var(--action-0);
+				color: var(--action-contrast);
 				&:hover {
 					color: var(--action-contrast);
 				}
@@ -101,10 +101,13 @@
 		}
 
 		.outline a {
-			background-color: rgba(var(--base-rgb), var(--overlay-light));
+			background-color: rgba(var(--base-rgb), rgba(var(--base-rgb),var(--op-3)));
 		}
 		.buttons {
 			margin: 3rem 0;
+			li {
+				background-color: rgba(var(--action-rgb), var(--op-4));
+			}
 		}
 		/* Button styles */
 		.wp-block-button__link {
diff --git a/src/video/view.js b/src/video/view.js
index 8974786..38cce0a 100644
--- a/src/video/view.js
+++ b/src/video/view.js
@@ -1,12 +1,46 @@
-const observer = new IntersectionObserver((entries) => {
-	entries.forEach(entry => {
-		if (entry.isIntersecting) {
-			loadVideo(entry.target);
-			observer.unobserve(entry.target);
-		}
-	});
-});
+document.addEventListener("DOMContentLoaded", function () {
+	const lazyVideos = [].slice.call(
+		document.querySelectorAll(".video-container video")
+	);
 
-document.querySelectorAll('.video-container .placeholder').forEach(el => {
-	observer.observe(el);
+	// Build a helper to actually set sources + load
+	function loadVideo(video) {
+		const sources = video.querySelectorAll("source[data-src]");
+		sources.forEach(source => {
+			source.src = source.dataset.src;
+		});
+		video.load();
+	}
+
+	// --- 1. IntersectionObserver (best case) ---
+	if ("IntersectionObserver" in window) {
+		const lazyVideoObserver = new IntersectionObserver(
+			function (entries, observer) {
+				entries.forEach(entry => {
+					if (entry.isIntersecting) {
+						loadVideo(entry.target);
+						observer.unobserve(entry.target);
+					}
+				});
+			},
+			{
+				rootMargin: "200px 0px",
+				threshold: 0.1,
+			}
+		);
+
+		lazyVideos.forEach(video => lazyVideoObserver.observe(video));
+		return;
+	}
+
+	// --- 2. Fallback: requestIdleCallback ---
+	if ("requestIdleCallback" in window) {
+		requestIdleCallback(() => {
+			lazyVideos.forEach(video => loadVideo(video));
+		});
+		return;
+	}
+
+	// --- 3. Final fallback: load immediately ---
+	lazyVideos.forEach(video => loadVideo(video));
 });
diff --git a/webpack.jvb.js b/webpack.jvb.js
index e2efb38..da6ceae 100644
--- a/webpack.jvb.js
+++ b/webpack.jvb.js
@@ -4,48 +4,49 @@
 module.exports = {
     mode: 'production',
     entry: {
-		'a11y':                './assets/js/dash/A11yHelper.js',
+		'a11y':                './assets/js/concise/A11yHelper.js',
+		'auth':                './assets/js/concise/AuthManager.js',
 		// 'admin':               './assets/js/dash/Admin.js',
-		'bioManager':          './assets/js/dash/BioManager.js',
-		'ContentManager':      './assets/js/dash/ContentManager.js',
-		'hours':               './assets/js/dash/CopyHours.js',
-		'crud':                './assets/js/dash/CRUD.js',
+		'bioManager':          './assets/js/concise/BioManager.js',
+		'ContentManager':      './assets/js/concise/ContentManager.js',
+		'hours':               './assets/js/concise/CopyHours.js',
+		'crud':                './assets/js/concise/CRUD.js',
 		'dataStore':           './assets/js/concise/DataStore.js',
 		'dragHandler':         './assets/js/concise/DragHandler.js',
-		'error':               './assets/js/dash/ErrorHandler.js',
-		'favouritesManager':   './assets/js/dash/FavouritesManager.js',
+		'error':               './assets/js/concise/ErrorHandler.js',
+		'favouritesManager':   './assets/js/concise/FavouritesManager.js',
 		'form':                './assets/js/concise/FormController.js',
-		'favourites':          './assets/js/concise/FrontendFavourites.js',
-		'votes':               './assets/js/concise/FrontendVotes.js',
+		// 'favourites':          './assets/js/concise/FrontendFavourites.js',
+		// 'votes':               './assets/js/concise/FrontendVotes.js',
+		'interactions':        './assets/js/concise/UserInteractions.js',
 		'gallery':             './assets/js/concise/Gallery.js',
-		'swiper':             './assets/js/concise/Swiper.js',
-		'maps':                './assets/js/dash/GoogleMaps.js',
+		// 'swiper':             './assets/js/concise/Swiper.js',
+		'maps':                './assets/js/concise/GoogleMaps.js',
 		'handleSelection':     './assets/js/concise/HandleSelection.js',
-		'integrations':        './assets/js/dash/Integrations.js',
-		'loading':             './assets/js/dash/LoadingManager.js',
-		'media':               './assets/js/concise/Media.js',
-		'modal':               './assets/js/dash/Modal.js',
+		'integrations':        './assets/js/concise/Integrations.js',
+		'modal':               './assets/js/concise/Modal.js',
 		'navigation':          './assets/js/concise/navigation.js',
-		'news':                './assets/js/dash/NewsManager.js',
-		'notificationManager': './assets/js/dash/NotificationManager.js',
-		'notifications':       './assets/js/Notifications.js',
-		'page-nav':            './assets/js/on-this-page.js',
+		'news':                './assets/js/concise/NewsManager.js',
+		'notificationManager': './assets/js/concise/NotificationManager.js',
+		'notifications':       './assets/js/concise/Notifications.js',
+		'page-nav':            './assets/js/concise/on-this-page.js',
 		'populate':            './assets/js/concise/PopulateForm.js',
 		'popup':               './assets/js/concise/Popup.js',
-		'postSelector':        './assets/js/dash/PostSelector.js',
+		'postSelector':        './assets/js/concise/PostSelector.js',
 		'quill':               './assets/js/concise/quill.js',
 		'queue':               './assets/js/concise/Queue.js',
 		'referral':            './assets/js/concise/Referral.js',
-		'shopManager':         './assets/js/dash/ShopManager.js',
+		'shopManager':         './assets/js/concise/ShopManager.js',
 		'cache':               './assets/js/concise/SimpleCache.js',
-		'square':              './assets/js/dash/SquareCheckout.js',
-		'tabs':                './assets/js/dash/Tabs.js',
-		'creator':             './assets/js/dash/TaxonomyCreator.js',
+		'schema':              './assets/js/concise/SchemaManager.js',
+		'square':              './assets/js/concise/SquareCheckout.js',
+		'tabs':                './assets/js/concise/Tabs.js',
+		'creator':             './assets/js/concise/TaxonomyCreator.js',
 		'selector':            './assets/js/concise/TaxonomySelector.js',
-		'ui':                  './assets/js/ui-handler.js',
+		// 'ui':                  './assets/js/ui-handler.js',
 		'uploader':            './assets/js/concise/UploadManager.js',
 		'settings':            './assets/js/concise/UserSettings.js',
-		'utility':             './assets/js/dash/UtilityFunctions.js',
+		'utility':             './assets/js/concise/UtilityFunctions.js',
 		'view':                './assets/js/concise/View.js',
     },
     output: {

--
Gitblit v1.10.0