From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve

---
 src/faq/style.scss                      |    1 
 assets/js/min/crud.min.js               |    2 
 activate.php                            |   11 
 assets/js/dash/CRUD.js                  |   22 
 inc/helpers/renderFields.php            |    4 
 build/faq/style-index.css               |    2 
 assets/js/dash/UtilityFunctions.js      |    1 
 src/gmbreviews/style.scss               |    6 
 assets/css/dash.min.css                 |    2 
 jvb.php                                 |   14 
 inc/rest/routes/ReferralRoutes.php      |  546 ++++++++
 inc/managers/ReferralManager.php        |  997 +++++++++++++--
 assets/js/min/form.min.js               |    2 
 inc/managers/DashboardManager.php       |   34 
 inc/rest/routes/ContentRoutes.php       |   12 
 assets/js/min/referral.min.js           |    2 
 inc/rest/routes/UploadRoutes.php        |    5 
 assets/js/min/queue.min.js              |    2 
 inc/blocks/CustomBlocks.php             |  202 +-
 inc/rest/routes/FeedRoutes.php          |    5 
 inc/integrations/GoogleMyBusiness.php   |    2 
 build/gmbreviews/render.php             |    2 
 build/faq/style-index-rtl.css           |    2 
 inc/managers/AdminPages.php             |   71 +
 assets/css/nav.min.css                  |    2 
 build/gmbreviews/style-index.css        |    2 
 build/gmbreviews/style-index-rtl.css    |    2 
 assets/js/concise/TaxonomySelector.js   |    3 
 assets/js/min/uploader.min.js           |    2 
 assets/js/concise/Queue.js              |   25 
 inc/managers/EmailManager.php           |   10 
 inc/integrations/Integrations.php       |   41 
 build/feed/view.asset.php               |    2 
 inc/importers/JaneAppClientImporter.php |  214 ++
 inc/importers/JaneAppSalesImporter.php  |   24 
 assets/js/concise/DataStore.js          |  269 +++-
 inc/integrations/Cloudflare.php         |    4 
 assets/js/concise/Referral.js           |  499 ++++--
 assets/js/min/dataStore.min.js          |    2 
 build/feed/view.js                      |    2 
 assets/js/min/selector.min.js           |    2 
 src/gmbreviews/render.php               |    2 
 assets/js/concise/FormController.js     |    3 
 assets/js/concise/UploadManager.js      |  623 ++++++---
 inc/blocks/FeedBlock.php                |   17 
 src/feed/view.js                        |   53 
 46 files changed, 2,859 insertions(+), 893 deletions(-)

diff --git a/activate.php b/activate.php
index 39b04a1..72e59e5 100644
--- a/activate.php
+++ b/activate.php
@@ -1,6 +1,8 @@
 <?php
 
 use JVBase\integrations\Umami;
+use JVBase\managers\ReferralManager;
+use JVBase\utility\Features;
 
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
@@ -40,6 +42,7 @@
     remove_role('editor');
 	error_log('Roles removed!');
 	error_log('New Roles done!');
+	jvbRegisterAdminPages();
 	if (array_key_exists('integrations', JVB_SITE) && array_key_exists('umami', JVB_SITE['integrations']) && JVB_SITE['integrations']['umami']=== true) {
 		error_log('Adding Umami tables');
 		Umami::createTables();
@@ -268,3 +271,11 @@
 	do_action('jvbDailyReset');
 	error_log('Daily options reset completed at ' . current_time('Y-m-d H:i:s'));
 }
+
+
+function jvbRegisterAdminPages():void
+{
+	if (Features::forSite()->has('referrals')){
+		ReferralManager::addSubpage();
+	}
+}
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index e3d33d7..ee26f5f 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}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--height)}.dashboard h1:first-of-type{margin-top:4rem!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:fit-content;padding:.5rem!important;min-width:0;min-height:0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}.dashboard.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--outerRadius);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.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
diff --git a/assets/css/nav.min.css b/assets/css/nav.min.css
index 7dd5b31..c3ce512 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}
\ No newline at end of file
+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
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 47f72e8..ffe1891 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -14,10 +14,11 @@
 		DataStore.instance = this;
 
 		// Shared resources
-		this.databases = new Map();      // Shared IndexedDB connections
-		this.stores = new Map();         // Registered store namespaces
-		this.subscribers = new Map();    // Per-store event subscribers
-		this.pendingInits = new Map();   // Track initialization promises
+		this.dbConfig = new Map();		// Definitions for the databases
+		this.databases = new Map();     // Shared IndexedDB connections
+		this.stores = new Map();        // Registered store namespaces
+		this.subscribers = new Map();   // Per-store event subscribers
+		this.pendingInits = new Map();  // Track initialization promises
 		this.fetchQueue = [];
 
 		// Global state
@@ -27,7 +28,7 @@
 
 		this.init();
 
-		window.addEventListener('beforeunload', () => this.destroy());
+		// window.addEventListener('beforeunload', () => this.destroy());
 	}
 
 	async init() {
@@ -41,71 +42,97 @@
 
 	/**
 	 * Register a new store namespace
+	 * @param {string} name Database Name
+	 * @param {object|array} configs An object defining the store, or an array of objects defining the stores
+	 * @param {number} version the database version
 	 */
-	register(name, config = {}) {
-		if (this.stores.has(name)) {
-			console.warn(`Store "${name}" already registered`);
-			return this.getStoreAPI(name);
+	register(name, configs = [], version = 1.1) {
+		if (!Array.isArray(configs)) configs = [configs];
+		if (configs.length === 0) return;
+
+		if (!this.dbConfig.has(name)) {
+			this.dbConfig.set(name, {
+				dbName: `jvb_${name}`,
+				version: version,
+				stores: {},
+				_initialized: false
+			});
 		}
 
-		if (!config.keyPath) {
-			throw new Error(`Store "${name}" requires a keyPath`);
-		}
+		let dbEntry = this.dbConfig.get(name);
 
-		const store = {
-			name,
-			config: {
-				// Storage
-				dbName: `jvb_${name}_db`,
-				version: 1,
-				storeName: 'items',
-				keyPath: 'id',
-				indexes: [],
+		configs.forEach(config => {
+			if (!config.storeName) {
+				throw new Error(`Store config for "${name}" missing storeName`);
+			}
+			if (!config.keyPath) {
+				throw new Error(`Store "${config.storeName}" requires keyPath`);
+			}
 
-				// API
-				endpoint: null,
-				apiBase: jvbSettings.api,
-				filters: {},
-				required: null,
 
-				// Cache
-				TTL: 3600000, // 1 hour
-				useHttpCaching: true,
+			const storeKey = `${name}_${config.storeName}`;
 
-				// Behavior
-				showLoading: false,
-				delayFetch: true,
-				validateData: true, // Validate data is serializable
+			const store = {
+				config: {
+					// Storage
+					dbName: dbEntry.dbName,
+					storeName: 'items',
+					keyPath: 'id',
+					indexes: [],
 
-				...config
-			},
+					// API
+					endpoint: null,
+					apiBase: jvbSettings.api,
+					filters: {},
+					required: null,
 
-			// State
-			db: null,
-			data: new Map(),
-			cache: new Map(),
-			httpHeaders: new Map(),
-			filters: { ...config.filters },
-			isFetching: false,
-			currentRequest: null,
-			lastResponse: null,
-			_initialized: false
-		};
+					// Cache
+					TTL: 3600000, // 1 hour
+					useHttpCaching: true,
 
-		store.config.headers = {
-			'X-WP-Nonce': jvbSettings?.nonce,
-			...store.config.headers
-		};
+					// Behavior
+					showLoading: false,
+					delayFetch: true,
+					validateData: true, // Validate data is serializable
+					...config
+				},
+				dbKey: name,
+				storeKey: storeKey,
+				data: new Map(),
+				cache: new Map(),
+				httpHeaders: new Map(),
+				subscribers: new Map(),
+				filters: {...(config.filters || {}) },
+				isFetching: false,
+				currentRequest: null,
+				lastResponse: null,
+				_initialized: false
+			};
 
-		this.stores.set(name, store);
-		this.subscribers.set(name, new Set());
+			store.config.headers = {
+				'X-WP-Nonce': jvbSettings?.nonce,
+				...store.config.headers
+			};
+
+			dbEntry.stores[config.storeName] = storeKey;
+
+			this.stores.set(storeKey, store);
+			if (!this.subscribers.has(storeKey)) {
+				this.subscribers.set(storeKey, new Set());
+			}
+		});
+
 
 		// Initialize database asynchronously
-		this.initStoreDB(name).catch(error => {
+		this.initDB(name).catch(error => {
 			console.error(`Failed to initialize store "${name}":`, error);
 		});
 
-		return this.getStoreAPI(name);
+		const apis = {};
+		for (const [storeName, storeKey] of Object.entries(dbEntry.stores)) {
+			apis[storeName] = this.getStoreAPI(storeKey);
+		}
+		return apis;
 	}
 
 	/**
@@ -171,6 +198,16 @@
 			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));
@@ -189,6 +226,66 @@
 	}
 
 	/**
+	 * Convert FormData to plain object for storage
+	 */
+	formDataToObject(formData) {
+		const obj = {
+			_isFormData: true,
+			entries: {}
+		};
+
+		for (const [key, value] of formData.entries()) {
+			// Skip File/Blob objects - they're stored separately in UploadManager
+			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();
+
+		// Restore text entries
+		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);
+			}
+		}
+
+		if (window.jvbUploads && obj.entries.upload_ids) {
+			const uploadIds = JSON.parse(obj.entries.upload_ids);
+
+			for (const uploadId of uploadIds) {
+				const file = await window.jvbUploads.getBlobData(uploadId);
+				if (file) {
+					formData.append('files[]', file);
+				}
+			}
+		}
+
+		return formData;
+	}
+
+	/**
 	 * Strip DOM references from object
 	 */
 	stripDOMReferences(obj, visited = new WeakSet()) {
@@ -212,6 +309,12 @@
 			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;
@@ -242,40 +345,52 @@
 	/**
 	 * Initialize database for a specific store
 	 */
-	async initStoreDB(name) {
-		const store = this.stores.get(name);
-		if (!store || store._initialized) return;
+	async initDB(name) {
+		const db = this.dbConfig.get(name);
+		if (!db || db._initialized) return;
 
 		if (this.pendingInits.has(name)) {
 			return this.pendingInits.get(name);
 		}
 
-		const initPromise = this._performStoreInit(name);
+		const initPromise = this._performDBInit(name);
 		this.pendingInits.set(name, initPromise);
 
 		try {
 			await initPromise;
-			store._initialized = true;
+			db._initialized = true;
 		} finally {
 			this.pendingInits.delete(name);
 		}
 	}
 
-	async _performStoreInit(name) {
-		const store = this.stores.get(name);
-		const { dbName, version } = store.config;
+	async _performDBInit(name) {
+		const database = this.dbConfig.get(name);
+		const { dbName, version } = database;
+		const stores = Object.values(database.stores);
 
 		try {
 			if (!this.databases.has(dbName)) {
 				const db = await this.openDatabase(dbName, version, (db) => {
-					this.setupStores(db, store.config);
+					stores.forEach(store => {
+						let storeObj = this.stores.get(store);
+						if (storeObj) {
+							this.setupStores(db, storeObj.config);
+						}
+					});
 				});
 				this.databases.set(dbName, db);
 			}
 
-			store.db = this.databases.get(dbName);
-			this.loadStoreDataInBackground(name);
-			this.notify(name, 'db-init');
+			stores.forEach(storeName => {
+				let store = this.stores.get(storeName);
+				if (store) {
+					store.db = this.databases.get(dbName);
+					store._initialized = true;
+					this.loadStoreDataInBackground(storeName);
+					this.notify(storeName, 'db-init');
+				}
+			})
 
 		} catch (error) {
 			console.error(`Failed to initialize database for store "${name}":`, error);
@@ -464,7 +579,7 @@
 		}
 
 		if (!store._initialized) {
-			await this.initStoreDB(name);
+			await this.initDB(store.dbKey);
 		}
 	}
 
@@ -627,6 +742,14 @@
 
 		// Auto-normalize Sets/Maps
 		let processed = this.normalizeForStorage(item);
+
+		if (processed.data instanceof FormData) {
+			processed = {
+				...processed,
+				data: this.formDataToObject(processed.data)
+			};
+		}
+
 		processed = this.stripDOMReferences(processed);
 
 		// Validate data is serializable
@@ -640,10 +763,10 @@
 
 		const key = this.getItemKey(processed, store.config.keyPath);
 
-		// Store in memory
+		// Store the original in memory (with original data intact)
 		store.data.set(key, item);
 
-		// Store in IndexedDB
+		// Store processed in IndexedDB
 		if (store.db) {
 			const tx = store.db.transaction([store.config.storeName], 'readwrite');
 			const objectStore = tx.objectStore(store.config.storeName);
@@ -682,6 +805,10 @@
 			return { valid: true };
 		}
 
+		if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+			return { valid: true };
+		}
+
 		// Reject DOM elements
 		if (obj instanceof HTMLElement ||
 			obj instanceof NodeList ||
@@ -810,7 +937,6 @@
 		} else {
 			store.filters[key] = value;
 		}
-
 		this.notify(name, 'filters-changed', {
 			filters: store.filters,
 			changed: { key, oldValue, newValue: value }
@@ -912,6 +1038,9 @@
 	}
 
 	subscribe(name, callback) {
+		if (!this.subscribers.has(name)) {
+			this.subscribers.set(name, new Set());
+		}
 		const subscribers = this.subscribers.get(name);
 		subscribers.add(callback);
 		return () => subscribers.delete(callback);
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 80be7b3..0e53476 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -8,7 +8,7 @@
 			collectFormData: false,
 			... config
 		}
-		this.store = window.jvbStore.register(
+		const store = window.jvbStore.register(
 			'forms',
 			{
 				storeName: 'forms',
@@ -23,6 +23,7 @@
 				validateData: true,
 				delayFetch: true
 			});
+		this.store = store.forms;
 
 		this.debouncer = window.debouncer;
 
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 56dcb97..297885d 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -26,7 +26,8 @@
 		this.errors = window.jvbError;
 
 		// Initialize DataStore for queue persistence
-		this.store = window.jvbStore.register('queue', {
+		const store = window.jvbStore.register('queue', {
+			storeName: 'queue',
 			keyPath: 'id',
 			endpoint: this.config.endpoint,
 			TTL: Infinity,
@@ -37,6 +38,7 @@
 			showLoading: false,
 			delayFetch: false, // Queue should fetch immediately
 		});
+		this.store = store.queue;
 
 		this.classes = [
 			'offline',
@@ -306,41 +308,28 @@
 
 	async processOperation(operation) {
 		try {
-			//update to uploading
 			this.updateOperationStatus(operation.id, 'uploading');
 
-			// Get fresh copy from store to restore FormData
-			operation = this.getQueue(operation.id);
+			if (operation.data?._isFormData) {
+				operation.data = await this.store.objectToFormData(operation.data);
+			}
 
-			//build request
 			const url = `${this.config.apiBase}${operation.endpoint}`;
 			let requestBody;
-			console.log(operation.data);
+
 			if (operation.data instanceof FormData) {
 				operation.data.append('id', operation.id);
 				operation.data.append('user', this.user);
 				requestBody = operation.data;
-				// console.log('Sending formData: ');
-				// for (const pair of requestBody.entries()) {
-				// 	console.log(pair[0], pair[1]);
-				// }
-
 			} else {
 				requestBody = JSON.stringify({
 					...operation.data,
 					id: operation.id,
 					user: this.user
 				});
-				// console.log('Sending data: ', {
-				// 	...operation.data,
-				// 	id: operation.id,
-				// 	user: this.user
-				// });
 				operation.headers['Content-Type'] = 'application/json';
 			}
 
-
-
 			const response = await fetch(url, {
 				method: operation.method,
 				headers: operation.headers,
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index 9fe722d..9ec7702 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -1,7 +1,6 @@
 /**
  * Referral Widget Manager
  * Handles both logged-in share widget and public code validation widget
- *
  */
 
 class Referral {
@@ -12,19 +11,23 @@
 		}
 
 		this.a11y = window.jvbA11y;
-
 		this.toggle = document.querySelector('button[data-action="toggle-referral"]');
 
 		this.initElements();
 		this.initListeners();
 		this.checkForReferral();
+
+		// Load additional data for logged-in users
+		if (this.isLoggedIn()) {
+			this.loadStats();
+			this.loadRecentReferrals();
+		}
 	}
 
-	initElements()
-	{
+	initElements() {
 		this.selectors = {
-			copy: 'button.copy',
-			login: '.login',
+			copyBtn: '.copy-btn',
+			checkCode: '.check-code-btn',
 			submit: '[type=submit]',
 		};
 
@@ -34,18 +37,10 @@
 			popup: this.container,
 			name: 'Referral Box',
 			onOpen: () => {
-				this.forms.forEach(form => {
-					form.addEventListener('submit', this.submitHandler);
-				});
-				this.container.addEventListener('click', this.clickHandler);
-				this.container.addEventListener('input', this.inputHandler);
+				this.bindEventListeners(true);
 			},
 			onClose: () => {
-				this.forms.forEach(form => {
-					form.removeEventListener('submit', this.submitHandler);
-				});
-				this.container.removeEventListener('click', this.clickHandler);
-				this.container.removeEventListener('input', this.inputHandler);
+				this.bindEventListeners(false);
 			}
 		});
 
@@ -61,43 +56,170 @@
 		this.clickHandler = this.handleClick.bind(this);
 		this.inputHandler = this.handleInput.bind(this);
 		this.submitHandler = this.handleFormSubmit.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
+	}
 
+	bindEventListeners(bind) {
+		const method = bind ? 'addEventListener' : 'removeEventListener';
+
+		this.forms.forEach(form => {
+			form[method]('submit', this.submitHandler);
+		});
+
+		this.container[method]('click', this.clickHandler);
+		this.container[method]('input', this.inputHandler);
+	}
+
+	isLoggedIn() {
+		return Boolean(jvbSettings.currentUser);
 	}
 
 	handleClick(e) {
-		if (e.target.classList.contains('.copy')) {
-			let target = e.target.dataset.target;
-			let value = this.container.querySelector(`#${target}`);
-			value = (value) ? value.textContent : false;
-			if (value) {
-				this.handleCopy(e.target, value);
-			}
+		const target = e.target.closest('.copy-btn, .check-code-btn, .attn');
+		if (!target) return;
+
+		if (target.classList.contains('copy-btn')) {
+			this.handleCopyClick(target);
+		} else if (target.classList.contains('check-code-btn')) {
+			this.handleCheckCode(e);
+		} else if (target.classList.contains('attn')) {
+			target.classList.remove('attn');
 		}
 	}
 
-	handleChange(e) {
-		if (e.target.id === 'referral-code') {
-			window.debouncer.schedule(
-				'check-referral',
-				()=> this.makeRequest('referrals/check-code', {code: e.target.value})),
-				150
+	/**
+	 * Handle copy button click with fallback
+	 */
+	handleCopyClick(button) {
+		const targetId = button.dataset.target;
+		const codeElement = this.container.querySelector(`#${targetId}`);
+
+		if (!codeElement) return;
+
+		const text = codeElement.textContent.trim();
+
+		// Try clipboard API first
+		if (navigator.clipboard && navigator.clipboard.writeText) {
+			navigator.clipboard.writeText(text).then(() => {
+				this.showCopySuccess(button);
+			}).catch(() => {
+				// Fallback to selection
+				this.selectText(codeElement);
+				this.showCopyFallback(button);
+			});
+		} else {
+			// Fallback to selection
+			this.selectText(codeElement);
+			this.showCopyFallback(button);
 		}
 	}
 
+	/**
+	 * Select text in element
+	 */
+	selectText(element) {
+		if (window.getSelection && document.createRange) {
+			const selection = window.getSelection();
+			const range = document.createRange();
+			range.selectNodeContents(element);
+			selection.removeAllRanges();
+			selection.addRange(range);
+		} else if (document.body.createTextRange) {
+			// IE fallback
+			const range = document.body.createTextRange();
+			range.moveToElementText(element);
+			range.select();
+		}
+	}
+
+	/**
+	 * Show copy success feedback
+	 */
+	showCopySuccess(button) {
+		const originalHTML = button.innerHTML;
+		button.innerHTML = window.jvbIcon('check', {size: 16}) + ' Copied!';
+		button.classList.add('success');
+
+		setTimeout(() => {
+			button.innerHTML = originalHTML;
+			button.classList.remove('success');
+		}, 2000);
+	}
+
+	/**
+	 * Show fallback message
+	 */
+	showCopyFallback(button) {
+		const originalHTML = button.innerHTML;
+		button.innerHTML = '✓ Selected - Press Ctrl+C';
+		button.classList.add('selected');
+
+		setTimeout(() => {
+			button.innerHTML = originalHTML;
+			button.classList.remove('selected');
+		}, 3000);
+	}
+
 	handleInput(e) {
-		if (e.target.id === 'referral-code') {
+		if (e.target.id === 'referral_code' || e.target.name === 'referral_code') {
 			e.target.value = e.target.value.toUpperCase();
 		}
 	}
 
-	// ==========================================
-	// SHARE WIDGET (Logged-In Users)
-	// ==========================================
+	/**
+	 * Handle code verification
+	 */
+	async handleCheckCode(e) {
+		e.preventDefault();
 
-	initShareWidget() {
-		this.initCopyButton();
-		this.loadStats();
+		const form = e.target.closest('form');
+		const codeInput = form.querySelector('[name="referral_code"]');
+		const statusDiv = form.querySelector('.code-status');
+
+		if (!codeInput || !statusDiv) return;
+
+		const code = codeInput.value.trim();
+
+		if (!code) {
+			this.showCodeStatus(statusDiv, 'Please enter a code', 'error');
+			return;
+		}
+
+		// Show loading
+		statusDiv.hidden = false;
+		statusDiv.className = 'code-status loading';
+		statusDiv.innerHTML = '<span class="spinner"></span> Checking...';
+
+		try {
+			const result = await this.validateCodeOnly(code);
+
+			if (result.success) {
+				this.showCodeStatus(
+					statusDiv,
+					`✓ Valid! Referred by ${result.referrer_name}`,
+					'success'
+				);
+			} else {
+				this.showCodeStatus(statusDiv, result.message || 'Invalid code', 'error');
+			}
+		} catch (error) {
+			console.error('Error checking code:', error);
+			this.showCodeStatus(statusDiv, 'Error checking code', 'error');
+		}
+	}
+
+	/**
+	 * Show code verification status
+	 */
+	showCodeStatus(statusDiv, message, type) {
+		statusDiv.hidden = false;
+		statusDiv.className = `code-status ${type}`;
+		statusDiv.textContent = message;
+
+		if (type === 'error') {
+			setTimeout(() => {
+				statusDiv.hidden = true;
+			}, 5000);
+		}
 	}
 
 	/**
@@ -106,15 +228,17 @@
 	async checkForReferral() {
 		const isLoggedIn = this.getUrlParameter('seeReferral');
 		const refCode = this.getUrlParameter('ref');
+
 		if (!isLoggedIn && !refCode) {
 			return;
 		}
+
 		if (!refCode) {
 			this.popup.openPopup();
 			return;
 		}
 
-		const codeInput = this.container.querySelector('#referral-code-input');
+		const codeInput = this.container.querySelector('[name="referral_code"]');
 		if (!codeInput) return;
 
 		// Convert to uppercase
@@ -122,25 +246,30 @@
 
 		// Pre-fill the code input
 		codeInput.value = code;
-		codeInput.readOnly = true; // Make it read-only since it came from link
+		codeInput.readOnly = true;
 
 		this.popup.togglePopup();
 
-		// Validate the code immediately to show referrer info
+		// Validate the code immediately
 		try {
 			const referrer = await this.validateCodeOnly(code);
 
 			if (referrer.success) {
-				// Show referrer info banner
-				this.showReferrerBanner(referrer.referrer_name, code);
+				const statusDiv = codeInput.closest('form').querySelector('.code-status');
+				if (statusDiv) {
+					this.showCodeStatus(
+						statusDiv,
+						`✓ ${referrer.referrer_name} invited you!`,
+						'success'
+					);
+				}
 
-				// Focus on name input (first empty field)
-				const nameInput = this.container.querySelector('#referral-name');
+				// Focus on name input
+				const nameInput = this.container.querySelector('[name="referral_name"]');
 				if (nameInput) {
 					nameInput.focus();
 				}
 			} else {
-				// Invalid code - make input editable and show error
 				codeInput.readOnly = false;
 				this.showMessage('This referral link is invalid. Please enter a valid code.', 'error');
 			}
@@ -149,21 +278,15 @@
 			codeInput.readOnly = false;
 		}
 
-		// Clean up URL (remove ?ref parameter)
+		// Clean up URL
 		this.removeUrlParameter('ref');
 	}
 
-	/**
-	 * Get URL parameter value
-	 */
 	getUrlParameter(name) {
 		const urlParams = new URLSearchParams(window.location.search);
 		return urlParams.get(name);
 	}
 
-	/**
-	 * Remove URL parameter (clean URL)
-	 */
 	removeUrlParameter(name) {
 		const url = new URL(window.location);
 		url.searchParams.delete(name);
@@ -171,13 +294,14 @@
 	}
 
 	/**
-	 * Validate code without registering (just check if valid)
+	 * Validate code without registering
 	 */
 	async validateCodeOnly(code) {
-		const response = await fetch(`${jvbSettings.api}/referrals/check-code`, {
+		const response = await fetch(`${jvbSettings.api}referrals/check-code`, {
 			method: 'POST',
 			headers: {
-				'Content-Type': 'application/json'
+				'Content-Type': 'application/json',
+				'X-WP-Nonce': jvbSettings.nonce
 			},
 			body: JSON.stringify({ code: code })
 		});
@@ -186,65 +310,14 @@
 	}
 
 	/**
-	 * Show banner with referrer info
+	 * Load user stats
 	 */
-	showReferrerBanner(referrerName, code) {
-		const header = this.container.querySelector('.referral-header');
-		if (!header) return;
-
-		// Create banner
-		const banner = document.createElement('div');
-		banner.className = 'referrer-banner';
-		banner.innerHTML = `
-            <div class="banner-icon">🎉</div>
-            <div class="banner-content">
-                <strong>${window.escapeHtml(referrerName)}</strong> referred you!
-                <div class="banner-code">Code: <code>${window.escapeHtml(code)}</code></div>
-            </div>
-        `;
-
-		// Insert after header
-		header.parentNode.insertBefore(banner, header.nextSibling);
-
-		// Update header text
-		const headerTitle = header.querySelector('h3');
-		if (headerTitle) {
-			headerTitle.textContent = 'Complete Your Registration';
-		}
-
-		const headerDesc = header.querySelector('p');
-		if (headerDesc) {
-			headerDesc.textContent = 'Enter your details below to claim your welcome reward!';
-		}
-	}
-
-	/**
-	 * Copy referral link to clipboard
-	 */
-	handleCopy(button, text = '') {
-		if (text === '' || typeof text !== 'string') {
-			return;
-		}
-		let originalText = button.textContent;
-		if (navigator.clipboard || navigator.clipboard.writeText) {
-			navigator.clipboard.writeText(text).then(() => {
-				button.textContent = 'Copied!';
-				button.style.background = '#00a32a';
-
-				setTimeout(() => {
-					button.textContent = originalText;
-					button.style.background = '';
-				}, 2000);
-			})
-		}
-	}
-
 	async loadStats() {
-		const statsContainer = this.container.querySelector('.referral-stats');
+		const statsContainer = this.container.querySelector('.stats-summary');
 		if (!statsContainer) return;
 
 		try {
-			const response = await fetch(`${jvbSettings.api}/referrals/stats`, {
+			const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${jvbSettings.currentUser}`, {
 				headers: { 'X-WP-Nonce': jvbSettings.nonce }
 			});
 
@@ -277,76 +350,129 @@
 	}
 
 	/**
+	 * 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) {
+		if (!referrals || referrals.length === 0) {
+			container.innerHTML = '<p class="no-referrals">Share your code to get started!</p>';
+			return;
+		}
+
+		const html = 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>
+				</div>
+				<div class="referral-date">${this.formatDate(ref.referred_at)}</div>
+			</div>
+		`).join('');
+
+		container.innerHTML = html;
+	}
+
+	/**
+	 * Format date nicely
+	 */
+	formatDate(dateString) {
+		const date = new Date(dateString);
+		const now = new Date();
+		const diffTime = Math.abs(now - date);
+		const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
+
+		if (diffDays === 0) return 'Today';
+		if (diffDays === 1) return 'Yesterday';
+		if (diffDays < 7) return `${diffDays} days ago`;
+
+		return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+	}
+
+	/**
 	 * Handle form submission
 	 */
 	async handleFormSubmit(event) {
-		window.debouncer.cancel('check-referral');
 		event.preventDefault();
 
 		const form = event.target;
-
-		// Get form data
 		const formData = new FormData(form);
 
-		let data = {};
-
 		// Disable form
 		this.setFormLoading(true, form);
 
 		try {
+			let result = { success: false, message: '' };
 
-			let result = {success: false, message: ''};
 			if (form.id === 'referral-code-form') {
-				if (!formData.get('name')) {
-					result.message += 'We need your name to know who you are.';
-				}
-				if (!formData.get('email')) {
-					result.message += 'We need your email to confirm you have access to it.';
-				}
-				if (!formData.get('referral_code')) {
-					result.message += 'We need the referral code to know who sent you.';
-				}
-				if (formData.get('name') && formData.get('email') && formData.get('referral_code')) {
-					data.name = formData.get('name');
-					data.email = formData.get('email');
-					data.code = formData.get('referral_code');
+				const data = {
+					name: formData.get('referral_name'),
+					email: formData.get('referral_email'),
+					code: formData.get('referral_code')
+				};
+
+				if (!data.name || !data.email || !data.code) {
+					result.message = 'Please fill in all fields';
+				} else {
 					result = await this.makeRequest('referrals/register', data);
 				}
-			} else if (form.id === 'login-form' && formData.get('login-email')) {
-				data.type = 'login';
-				data.email = formData.get('login-email');
-				data.context = {};
-				data.context['redirect_to'] = window.location.href+'?seeReferral=1';
-				console.log('Making Request with: ', data);
+			} else if (form.id === 'login-form') {
+				const data = {
+					type: 'login',
+					email: formData.get('login_email'),
+					context: {
+						redirect_to: window.location.href + '?seeReferral=1'
+					}
+				};
 				result = await this.makeRequest('magic', data);
 			}
 
-
 			if (result.success) {
-				this.handleSuccess(result);
+				this.handleSuccess(form, result);
 			} else {
-				this.showMessage(result.message || 'Something went wrong. Please try again.', 'error');
-				this.setFormLoading(false, form);
+				this.showFormMessage(form, result.message || 'Something went wrong. Please try again.', 'error');
 			}
 		} catch (error) {
-			console.error('Error registering:', error);
-			this.showMessage('Something went wrong. Please try again.', 'error');
-			this.setFormLoading(false, form);
+			console.error('Error submitting form:', error);
+			this.showFormMessage(form, 'Something went wrong. Please try again.', 'error');
 		} finally {
 			this.setFormLoading(false, form);
 		}
 	}
 
 	async makeRequest(endpoint, data) {
-		if (![
+		const validEndpoints = [
 			'magic',
 			'referrals/register',
 			'referrals/check-code'
-		].includes(endpoint)) {
-			return {success:false, message: 'Something went wrong (Invalid endpoint).'}
+		];
+
+		if (!validEndpoints.includes(endpoint)) {
+			return { success: false, message: 'Invalid endpoint' };
 		}
-		console.log('Endpoint: ', endpoint);
-		console.log('Data: ', data);
+
 		const response = await fetch(`${jvbSettings.api}${endpoint}`, {
 			method: 'POST',
 			headers: {
@@ -362,23 +488,21 @@
 	/**
 	 * Show success state
 	 */
-	handleSuccess(result) {
-		//Hide forms
-		this.container.querySelectorAll('form').forEach(form => {
-			window.fade(form, false);
-		});
-
-		const successState = this.container.querySelector('.success-message');
-		if (!successState) return;
+	handleSuccess(form, result) {
+		// Hide form
+		form.style.display = 'none';
 
 		// Show success message
-		successState.hidden = false;
+		const successDiv = form.nextElementSibling;
+		if (successDiv && successDiv.classList.contains('success-content')) {
+			successDiv.hidden = false;
 
-		// Scroll to message
-		successState.scrollIntoView({
-			behavior: 'smooth',
-			block: 'center'
-		});
+			// Scroll to message
+			successDiv.scrollIntoView({
+				behavior: 'smooth',
+				block: 'center'
+			});
+		}
 
 		// Fire custom event
 		this.dispatchEvent('emailSent', {
@@ -387,38 +511,45 @@
 	}
 
 	/**
-	 * Set form loading state
+	 * Show message in form status area
 	 */
-	setFormLoading(loading, form) {
-		const inputs = form.querySelectorAll('input');
+	showFormMessage(form, text, type = 'error') {
+		const status = form.querySelector('.status');
+		if (!status) return;
 
-		inputs.forEach(input => input.disabled = loading);
-		let status = form.querySelector('.status');
-		let message = status.querySelector('.message');
-		status.hidden = loading;
-		status.classList.toggle('loading', loading);
-		if (loading) {
-			message.textContent = 'Checking with server...';
-		} else {
-			message.textContent = '';
+		const message = status.querySelector('.message');
+		if (message) {
+			message.textContent = text;
+		}
+
+		status.hidden = false;
+		status.className = `status ${type}`;
+
+		if (type === 'error') {
+			setTimeout(() => {
+				status.hidden = true;
+			}, 5000);
 		}
 	}
 
 	/**
-	 * Show message
+	 * Set form loading state
 	 */
-	showMessage(text, type = 'success') {
-		const messageDiv = this.container.querySelector('#referral-message');
-		if (!messageDiv) return;
+	setFormLoading(loading, form) {
+		const inputs = form.querySelectorAll('input, button');
+		inputs.forEach(input => input.disabled = loading);
 
-		messageDiv.textContent = text;
-		messageDiv.className = 'message ' + type;
-		messageDiv.style.display = 'block';
+		const status = form.querySelector('.status');
+		if (status) {
+			status.classList.toggle('loading', loading);
 
-		if (type === 'error') {
-			setTimeout(() => {
-				messageDiv.style.display = 'none';
-			}, 5000);
+			if (loading) {
+				status.hidden = false;
+				const message = status.querySelector('.message');
+				if (message) {
+					message.textContent = 'Sending...';
+				}
+			}
 		}
 	}
 
@@ -434,8 +565,6 @@
 	}
 }
 
-
 document.addEventListener('DOMContentLoaded', () => {
 	window.jvbReferral = new Referral();
 });
-
diff --git a/assets/js/concise/TaxonomySelector.js b/assets/js/concise/TaxonomySelector.js
index 7f27f13..bf3af8f 100644
--- a/assets/js/concise/TaxonomySelector.js
+++ b/assets/js/concise/TaxonomySelector.js
@@ -16,7 +16,7 @@
 
 		this.subscribers = new Set();
 
-		this.store = window.jvbStore.register(
+		const store = window.jvbStore.register(
 			'taxonomies',
 			{
 				storeName: `terms`,
@@ -39,6 +39,7 @@
 				required: 'taxonomy',
 				delayFetch: true,
 			});
+		this.store = store.terms;
 
 		// Central field management
 		this.fields = new Map();
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index 59a0d5f..56fc593 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -12,40 +12,41 @@
 		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
-
-		// RECOVERY STORES - Cleared after successful upload
-		this.fieldStore = window.jvbStore.register(
-			'upload_fields',
-			{
-				storeName: 'fieldStates',
-				keyPath: 'id',
-				version: 2,
-				indexes: [
-					{ name: 'fieldId', keyPath: 'fieldId' },
-					{ name: 'timestamp', keyPath: 'timestamp' },
-					{ name: 'content', keyPath: 'content' },
-					{ name: 'itemId', keyPath: 'itemId' },
-					{ name: 'status', keyPath: 'status' }
-				],
-				TTL: 86400000 * 7, // 1 week
-				delayFetch: true
-			});
-
-		this.uploadStore = window.jvbStore.register(
+		this.fieldStoreReady = false;
+		this.uploadStoreReady = false;
+		this.hasCheckedForUploads = false;
+		const {fields, uploads} = window.jvbStore.register(
 			'uploads',
-			{
-				name: 'uploads',
-				storeName: 'uploads',
-				keyPath: 'id',
-				storeBlobs: true,
-				indexes: [
-					{ name: 'fieldId', keyPath: 'fieldId' },
-					{ name: 'status', keyPath: 'status' },
-					{ name: 'groupId', keyPath: 'groupId' },
-					{ name: 'attachmentId', keyPath: 'attachmentId' }
-				],
-				delayFetch: true
-			});
+			[
+				{
+					storeName: 'fields',
+					keyPath: 'id',
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'timestamp', keyPath: 'timestamp' },
+						{ name: 'content', keyPath: 'content' },
+						{ name: 'itemId', keyPath: 'itemId' },
+						{ name: 'status', keyPath: 'status' }
+					],
+					TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
+					delayFetch: true
+				},
+				{
+					storeName: 'uploads',
+					keyPath: 'id',
+					storeBlobs: true,
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'status', keyPath: 'status' },
+						{ name: 'groupId', keyPath: 'groupId' },
+						{ name: 'attachmentId', keyPath: 'attachmentId' }
+					],
+					delayFetch: true
+				}
+			]
+		);
+		this.fieldStore = fields;
+		this.uploadStore = uploads;
 
 		window.jvbUploadBlobs = this.uploadStore;
 
@@ -70,9 +71,6 @@
 		// Notification subscribers
 		this.subscribers = new Set();
 
-		// Controllers
-		this.dragController = null;
-
 		// Selectors
 		this.selectors = {
 			field: {
@@ -110,17 +108,6 @@
 			'failed_permanent': 'Upload failed permanently'
 		};
 
-		// Sortable configuration
-		this.sortableConfig = {
-			animation: 150,
-			draggable: '.item',
-			handle: '.select-item, img',
-			ghostClass: 'sortable-ghost',
-			chosenClass: 'sortable-chosen',
-			dragClass: 'sortable-drag',
-			onEnd: (evt) => this.handleReorder(evt)
-		};
-
 		this.init();
 	}
 
@@ -178,7 +165,6 @@
 	/*******************************************************************************
 	 * FIELD MANAGEMENT
 	 *******************************************************************************/
-
 	initializeFields() {
 		const fields = document.querySelectorAll(this.selectors.field.field);
 		fields.forEach(uploader => this.registerUploader(uploader));
@@ -213,10 +199,6 @@
 		uploader.dataset.uploader = fieldId;
 		this.addFieldSelectionHandler(fieldId);
 
-		if (config.destination === 'post_group' && !this.dragController) {
-			this.initGroupFeatures();
-		}
-
 		if (config.type !== 'single') {
 			this.initSortable(fieldId);
 		}
@@ -269,47 +251,219 @@
 	/*******************************************************************************
 	 * SORTABLE INITIALIZATION
 	 *******************************************************************************/
-
 	initSortable(fieldId) {
 		if (!window.Sortable) return;
 
+		// Mount MultiDrag plugin once
+		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+			Sortable.mount(new Sortable.MultiDrag());
+			Sortable._multiDragMounted = true;
+		}
+
 		const fieldEl = this.fieldElements.get(fieldId);
 		if (!fieldEl) return;
 
-		// Find all sortable grids (preview + group grids, but not restore grids)
+		// Initialize sortable on all existing grids
 		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => {
+			const groupId = grid.classList.contains('group')
+				? grid.closest('.upload-group')?.dataset.groupId
+				: null;
+			this.createSortableForGrid(grid, fieldId, groupId);
+		});
 
-		grids.forEach((grid) => {
-			// Skip if already initialized
-			if (grid.sortableInstance) return;
+		// Special handler for empty-group
+		const emptyGroup = fieldEl.element.querySelector('.empty-group');
+		if (emptyGroup && !emptyGroup.sortableInstance) {
+			emptyGroup.sortableInstance = new Sortable(emptyGroup, {
+				animation: 150,
+				draggable: '.item',
+				multiDrag: true,
+				selectedClass: 'selected-for-drag',
+				avoidImplicitDeselect: true,
+				group: { name: fieldId, pull: false, put: true },
+				ghostClass: 'sortable-ghost',
+				chosenClass: 'sortable-chosen',
+				dragClass: 'sortable-drag',
+				onEnd: (evt) => this.handleDrop(evt, fieldId)
+			});
+		}
+	}
 
-			const isGroupGrid = grid.classList.contains('group');
-			const gridId = isGroupGrid
-				? `${fieldId}-group-${grid.closest('.upload-group')?.dataset.groupId}`
-				: `${fieldId}-preview`;
+	syncSortableSelection(fieldId, selectedItems) {
+		// Update Sortable's selection state to match checkboxes
+		this.sortableInstances.forEach((instance, key) => {
+			if (key.startsWith(fieldId)) {
+				const grid = instance.el;
+				const items = grid.querySelectorAll('.item');
 
-			const sortableInstance = new Sortable(grid, {
-				...this.sortableConfig,
-				group: {
-					name: fieldId,
-					pull: true,
-					put: true
-				},
+				items.forEach(item => {
+					const uploadId = item.dataset.uploadId;
+					const shouldBeSelected = selectedItems.has(uploadId);
 
-				onAdd: (evt) => {
-					// Re-enable when items added
-					this.updateSortableState(evt.to);
-				},
-				onRemove: (evt) => {
-					// Disable source if now empty
-					this.updateSortableState(evt.from);
-				}
+					if (shouldBeSelected) {
+						Sortable.utils.select(item);
+					} else {
+						Sortable.utils.deselect(item);
+					}
+				});
+			}
+		});
+	}
+
+	handleDrop(evt, fieldId) {
+		const dropTarget = evt.to;
+		const sourceTarget = evt.from;
+		const items = evt.items?.length > 0 ? evt.items : [evt.item];
+		const uploadIds = items.map(item => item.dataset.uploadId);
+
+		// Determine drop target type
+		const targetType = this.getDropTargetType(dropTarget);
+
+		switch (targetType) {
+			case 'empty-group':
+				this.handleDropToEmptyGroup(items, uploadIds, fieldId);
+				break;
+
+			case 'preview':
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
+
+			case 'group':
+				this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
+				break;
+			default:
+				// Fallback: return to preview
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
+		}
+
+		// Update UI
+		this.updateSortableState(dropTarget);
+		if (sourceTarget !== dropTarget) {
+			this.updateSortableState(sourceTarget);
+		}
+	}
+
+	/**
+	 * Determine what type of drop target this is
+	 */
+	getDropTargetType(target) {
+		if (target.classList.contains('empty-group')) {
+			return 'empty-group';
+		}
+
+		if (target.classList.contains('preview')) {
+			return 'preview';
+		}
+
+		if (target.classList.contains('group')) {
+			return 'group';
+		}
+
+		return 'unknown';
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
+		try {
+			// If same container, it's just a reorder
+			if (dropTarget === sourceTarget) {
+				this.handleReorder({ to: dropTarget, items: items });
+				return;
+			}
+
+			// Moving to different group
+			uploadIds.forEach(uploadId => {
+				this.addToGroup(uploadId, dropTarget, false);
 			});
 
-			// Store reference on element for easy access
-			grid.sortableInstance = sortableInstance;
-			this.sortableInstances.set(gridId, sortableInstance);
-		});
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to group`
+				: 'Moved item to group';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		}  catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop to preview: remove from groups
+	 */
+	handleDropToPreview(items, uploadIds, fieldId) {
+		try {
+			uploadIds.forEach(uploadId => {
+				this.removeFromGroup(uploadId);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to preview`
+				: 'Moved item to preview';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop errors consistently
+	 */
+	handleDropError(items, fieldId, error, message = 'An error occurred') {
+		console.error('Drop error:', error);
+
+		// Return items to preview as fallback
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (fieldEl?.ui?.preview) {
+			items.forEach(item => fieldEl.ui.preview.appendChild(item));
+		}
+
+		this.a11y.announce(`${message}. Items returned to preview.`);
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToEmptyGroup(items, uploadIds, fieldId) {
+		try {
+			const group = this.createGroup(fieldId);
+			if (!group) {
+				this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
+				return;
+			}
+
+			// Move items to new group
+			items.forEach((item, index) => {
+				group.grid.appendChild(item);
+				this.addToGroup(uploadIds[index], group.grid, false);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Created group with ${items.length} items`
+				: 'Created group with item';
+			this.a11y.announce(message);
+
+			// Clear selection after move
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
 	}
 
 	/**
@@ -319,8 +473,8 @@
 		const sortable = grid?.sortableInstance;
 		if (!sortable) return;
 
-		const hasItems = grid.querySelectorAll('.item').length > 0;
-		sortable.option('disabled', !hasItems);
+		// const hasItems = grid.querySelectorAll('.item').length > 0;
+		sortable.option('disabled', false);
 	}
 
 	/**
@@ -339,65 +493,55 @@
 		const fieldWrapper = grid.closest('.field, .upload');
 		if (!fieldWrapper) return;
 
-		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
-		let items = Array.from(fieldWrapper.querySelectorAll('.item')).map(upload => upload.dataset.id);
-		console.log(items);
-		hiddenInput.value = items.join(',');
+		const movedItems = evt.items && evt.items.length > 0 ? evt.items : [evt.item];
 
-		if (window.jvbA11y) {
-			window.jvbA11y.announce('Item reordered');
+		// Get current order from DOM
+		let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
+			.map(upload => upload.dataset.uploadId)
+			.filter(id => id);
+
+		console.log('Reordered items:', items);
+
+		// Update hidden input (for form submission)
+		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+		if (hiddenInput && items.length > 0) {
+			hiddenInput.value = items.join(',');
 		}
 
+		// ✅ Update fieldState with new order
+		const fieldId = this.getFieldIdFromElement(grid);
+		if (fieldId) {
+			const fieldData = this.getFieldData(fieldId);
+
+			// If reordering within a group, update that group's uploads array
+			if (grid.classList.contains('group')) {
+				const groupId = grid.dataset.groupId;
+				const group = fieldData?.groups?.find(g => g.id === groupId);
+				if (group) {
+					group.uploads = items; // Update order
+				}
+			}
+			// If reordering in preview, the order is implicit by DOM position
+			// (we don't store preview order separately)
+
+			this.schedulePersistance(fieldId); // ✅ Persist changes
+		}
+
+		this.a11y.announce('Item reordered');
+
 		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
-			detail: { from: evt.from, to: evt.to, oldIndex: evt.oldIndex, newIndex: evt.newIndex },
+			detail: {
+				from: evt.from,
+				to: evt.to,
+				oldIndex: evt.oldIndex,
+				newIndex: evt.newIndex,
+				items: items
+			},
 			bubbles: true
 		}));
 	}
 
 	/*******************************************************************************
-	 * DRAG & DROP INITIALIZATION
-	 *******************************************************************************/
-
-	initGroupFeatures() {
-		this.dragController = new window.jvbDragHandler({
-			draggableSelector: this.selectors.items.item,
-			dropTargetSelector: `${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,
-			ignoreSelector: 'input:not(.upload-select), button, select, textarea, details, summary, a',
-			previewElement: 'img, video, .icon',
-			getItemId: (element) => element.dataset.uploadId,
-			getSelectedItems: (element) => {
-				const fieldId = this.getFieldIdFromElement(element);
-				const uploadId = element.dataset.uploadId;
-				const selected = this.getCurrentSelection(fieldId);
-				return (selected && selected.includes(uploadId)) ? selected : [uploadId];
-			},
-			validateDrop: (itemIds, targetElement) => {
-				const targetFieldId = this.getFieldIdFromElement(targetElement);
-				const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
-				const itemFieldId = this.getFieldIdFromElement(itemElement);
-				return targetFieldId === itemFieldId;
-			},
-			onDrop: (itemIds, targetElement) => {
-				this.handleItemDrop(itemIds, targetElement);
-				targetElement.scrollIntoView({behavior:'smooth', block:'center'});
-			},
-			onDragEnd: (itemIds, success) => {
-				if (success) {
-					const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
-					const fieldId = this.getFieldIdFromElement(itemElement);
-					const handler = this.selectionHandlers.get(fieldId);
-					handler?.clearSelection();
-				}
-			},
-			previewOptions: {
-				multiOffset: { x: -60, y: -80 },
-				singleOffset: { x: -50, y: -60 },
-				showCount: true
-			}
-		});
-	}
-
-	/*******************************************************************************
 	 * FILE DROP HANDLERS
 	 *******************************************************************************/
 
@@ -462,38 +606,6 @@
 	}
 
 	/*******************************************************************************
-	 * ITEM DROP HANDLER (for rearranging)
-	 *******************************************************************************/
-
-	handleItemDrop(itemIds, targetElement) {
-		const isPreviewDrop = targetElement.classList.contains('preview');
-		let actualTarget = targetElement;
-
-		// Handle drop on empty group placeholder
-		if (targetElement.classList.contains('empty-group')) {
-			const fieldId = this.getFieldIdFromElement(targetElement);
-			const group = this.createGroup(fieldId);
-			if (!group) return;
-			actualTarget = group.grid;
-		}
-
-		// Move each item
-		itemIds.forEach(uploadId => {
-			if (isPreviewDrop) {
-				this.removeFromGroup(uploadId);
-			} else {
-				this.addToGroup(uploadId, actualTarget);
-			}
-		});
-
-		const fieldId = this.getFieldIdFromElement(targetElement);
-		this.schedulePersistance(fieldId);
-
-		const message = itemIds.length > 1 ? `Moved ${itemIds.length} items` : 'Moved item';
-		this.a11y.announce(message);
-	}
-
-	/*******************************************************************************
 	 * CLICK & CHANGE HANDLERS
 	 *******************************************************************************/
 
@@ -563,7 +675,7 @@
 			try {
 				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 
-				// Create upload data (without blob initially)
+				// Create initial upload data
 				const uploadData = {
 					id: uploadId,
 					attachmentId: null,
@@ -577,13 +689,20 @@
 					}
 				};
 
+				// Save initial data
+				await this.uploadStore.save(uploadData);
+
 				// Process file
 				const preview = this.createPreviewUrl(file);
 				const processedFile = file.type.startsWith('image/')
 					? await this.processImage(file, fieldData.config.subtype)
 					: file;
 
-				// Store blob data as ArrayBuffer
+				// Show progress
+				this.showUploadProgress(uploadId, true);
+				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+				// Store blob data (this updates the existing uploadData)
 				await this.saveBlobData(uploadId, processedFile || file);
 
 				// Create DOM element
@@ -595,10 +714,6 @@
 					subtype: subtype
 				}, fieldData.config.destination === 'post_group');
 
-				// Show progress
-				this.showUploadProgress(uploadId, true);
-				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
-
 				// Add to preview grid
 				if (fieldEl.ui.preview) {
 					fieldEl.ui.preview.appendChild(element);
@@ -611,9 +726,12 @@
 					});
 				}
 
-				// Store persistent data
-				uploadData.status = 'processed';
-				await this.uploadStore.save(uploadData);
+				// Update status (gets existing data with blobData intact)
+				const storedUpload = this.uploadStore.get(uploadId);
+				if (storedUpload) {
+					storedUpload.status = 'processed';
+					await this.uploadStore.save(storedUpload);
+				}
 
 				// Add to field
 				fieldData.uploads.add(uploadId);
@@ -650,7 +768,7 @@
 	}
 
 	/*******************************************************************************
-	 * IMAGE PROCESSING (simplified - keeping existing logic)
+	 * IMAGE PROCESSING
 	 *******************************************************************************/
 
 	async processImage(file, uploadId) {
@@ -1297,7 +1415,7 @@
 	 * Handle operation cancellation
 	 */
 	async handleOperationCancelled(fieldId) {
-		const fieldData = this.getFieldData(fieldId);  // ✅
+		const fieldData = this.getFieldData(fieldId);
 		if (!fieldData) return;
 
 		const uploadsArray = fieldData.uploads instanceof Set
@@ -1540,28 +1658,25 @@
 		}
 
 		for (const groupData of groups) {
-			// Create group using existing method which handles all initialization
 			const group = this.createGroup(fieldId, groupData.id);
-
 			if (!group) {
 				console.warn('Failed to create group:', groupData.id);
 				continue;
 			}
 
-			// Find the group in fieldData (createGroup already added it)
 			const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
-
 			if (storedGroup) {
 				// Restore metadata
 				if (groupData.changes) {
 					storedGroup.changes = { ...groupData.changes };
 				}
 
+				// ✅ Preserve upload order
 				if (groupData.uploads) {
 					storedGroup.uploads = [...groupData.uploads];
 				}
 
-				// Restore form field values if they exist
+				// Restore form field values
 				if (groupData.changes) {
 					const titleInput = group.element.querySelector('[name*="post_title"]');
 					const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
@@ -1569,7 +1684,6 @@
 					if (titleInput && groupData.changes.post_title) {
 						titleInput.value = groupData.changes.post_title;
 					}
-
 					if (excerptInput && groupData.changes.post_excerpt) {
 						excerptInput.value = groupData.changes.post_excerpt;
 					}
@@ -1577,7 +1691,6 @@
 			}
 		}
 
-		// Save updated field data
 		await this.saveFieldData(fieldData);
 	}
 
@@ -1801,19 +1914,76 @@
 		const fieldData = this.getFieldData(fieldId);
 		if (!fieldEl?.ui?.dropZone || !fieldData) return;
 
-		if (fieldData.config.destination === 'post_group') return;
-
 		const uploadCount = fieldData.uploads?.size || 0;
-		const maxFiles = fieldData.config?.maxFiles || 999;
+
+		// For groupable uploads, set max to 20
+		const maxFiles = fieldData.config.destination === 'post_group'
+			? 20
+			: (fieldData.config?.maxFiles || 999);
 
 		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
 		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+
+		// Show helpful message for groupable uploads
+		if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
+			this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
+		}
 	}
 
 	/*******************************************************************************
 	 * GROUP MANAGEMENT
 	 *******************************************************************************/
+	/**
+	 * Create sortable instance for a grid
+	 */
+	createSortableForGrid(grid, fieldId, groupId = null) {
+		if (!grid || grid.sortableInstance) return;
 
+		const sortableInstance = new Sortable(grid, {
+			animation: 150,
+			draggable: '.item',
+			multiDrag: true,
+			selectedClass: 'selected-for-drag',
+			avoidImplicitDeselect: true,
+			group: { name: fieldId, pull: true, put: true },
+			ghostClass: 'sortable-ghost',
+			chosenClass: 'sortable-chosen',
+			dragClass: 'sortable-drag',
+
+			// Centralized drop handler
+			onEnd: (evt) => this.handleDrop(evt, fieldId),
+
+			// Selection sync
+			onSelect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && !checkbox.checked) {
+					checkbox.checked = true;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onDeselect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && checkbox.checked) {
+					checkbox.checked = false;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onAdd: (evt) => this.updateSortableState(evt.to),
+			onRemove: (evt) => this.updateSortableState(evt.from)
+		});
+
+		grid.sortableInstance = sortableInstance;
+
+		const gridId = groupId
+			? `${fieldId}-group-${groupId}`
+			: `${fieldId}-preview`;
+
+		this.sortableInstances.set(gridId, sortableInstance);
+
+		return sortableInstance;
+	}
 	createGroup(fieldId, groupId = null) {
 		const fieldData = this.getFieldData(fieldId);
 		const fieldEl = this.fieldElements.get(fieldId);
@@ -1855,32 +2025,25 @@
 
 		// Add to field groups
 		if (!fieldData.groups) fieldData.groups = [];
-		fieldData.groups.push({
-			id: groupId,
-			uploads: [],
-			changes: {}
-		});
-		this.saveFieldData(fieldData);
+		const existingGroup = fieldData.groups.find(g => g.id === groupId);
+		if (!existingGroup) {
+			fieldData.groups.push({
+				id: groupId,
+				uploads: [],
+				changes: {}
+			});
+			this.saveFieldData(fieldData);
+		}
 
-		// Initialize selection handler and sortable
+		// Initialize selection handler
 		this.addGroupSelectionHandler(fieldId, groupId);
 
-		// Initialize sortable for this new group grid
 		if (grid) {
-			const sortableInstance = new Sortable(grid, {
-				...this.sortableConfig,
-				group: { name: fieldId, pull: true, put: true },
-				disabled: true, // Empty initially
-				onAdd: (evt) => this.updateSortableState(evt.to),
-				onRemove: (evt) => this.updateSortableState(evt.from)
-			});
-			grid.sortableInstance = sortableInstance;
-			this.sortableInstances.set(`${fieldId}-group-${groupId}`, sortableInstance);
+			this.createSortableForGrid(grid, fieldId, groupId);
 		}
 
 		return { id: groupId, element: groupElement, grid: grid };
 	}
-
 	createGroupElement(groupId, fieldId) {
 		let groupElement = window.getTemplate('imageGroup');
 		if (!groupElement) return;
@@ -2163,6 +2326,9 @@
 			case 'restore':
 				this.handleRestoreUploads().then(() => {});
 				break;
+			case 'restore-all':
+				this.handleRestoreAll().then(() => {});
+				break;
 			case 'clear-cache':
 				if (!confirm('Save these uploads for later?')) {
 					this.cleanupStoredUploads();
@@ -2260,8 +2426,16 @@
 		handler.subscribe((event, data) => {
 			switch(event) {
 				case 'item-selected':
+					// Sync with Sortable
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
 				case 'item-deselected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
 				case 'range-selected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
 					this.selected.set(fieldId, data.selectedItems);
 					break;
 				case 'select-all':
@@ -2356,6 +2530,11 @@
 	 * 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()
@@ -2588,7 +2767,7 @@
 		window.debouncer.schedule(
 			key,
 			() => this.persistFieldState(fieldId),
-			1000
+			250
 		);
 	}
 
@@ -2637,8 +2816,8 @@
 	handleFieldStoreEvent(event, data) {
 		switch(event) {
 			case 'data-loaded':
-				// Check for pending uploads to restore
-				this.checkForStoredUploads();
+				this.fieldStoreReady = true;
+				this.checkIfBothStoresReady();
 				break;
 		}
 	}
@@ -2646,6 +2825,8 @@
 	handleUploadStoreEvent(event, data) {
 		switch(event) {
 			case 'data-loaded':
+				this.uploadStoreReady = true;
+				this.checkIfBothStoresReady();
 				break;
 			case 'item-saved':
 				this.showSaveIndicator(data.key);
@@ -2653,6 +2834,13 @@
 		}
 	}
 
+	checkIfBothStoresReady() {
+		if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
+			this.hasCheckedForUploads = true;
+			this.checkForStoredUploads();
+		}
+	}
+
 	async checkForStoredUploads() {
 		const allFieldStates = this.fieldStore.getAll();
 
@@ -2660,6 +2848,8 @@
 			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;
 
@@ -2722,8 +2912,8 @@
 			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
 
 			// Process each upload
-			for (const upload of field.uploads) {
-
+			for (let uploadId of field.uploads) {
+				const upload = this.uploadStore.get(uploadId);
 				let uploadItem = window.getTemplate('uploadItem');
 				if (!uploadItem) continue;
 				//
@@ -2875,6 +3065,23 @@
 		this.cleanupRestore();
 	}
 
+	async handleRestoreAll() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
+		// Gets ALL uploads from notification without checking selection
+		const allUploads = [];
+		notification.querySelectorAll('.item.upload').forEach(item => {
+			let uploadId = item.dataset.uploadId;
+			let fieldId = item.dataset.fieldId;
+			allUploads.push({ uploadId, fieldId });
+		});
+
+		await this.restoreSelectedUploads(allUploads);
+		this.cleanupRestore();
+	}
+
 	showSaveIndicator(key) {
 		// Optional: show user that state is being saved
 	}
diff --git a/assets/js/dash/CRUD.js b/assets/js/dash/CRUD.js
index 6780203..0a2144b 100644
--- a/assets/js/dash/CRUD.js
+++ b/assets/js/dash/CRUD.js
@@ -18,9 +18,10 @@
 		this.updateBulkOptions();
 
 		// Initialize components
-		this.store = window.jvbStore.register(
+		const store = window.jvbStore.register(
 			this.content,
 			{
+				storeName: this.content,
 				keyPath: 'id',
 				endpoint: 'content',
 				headers: {
@@ -42,7 +43,9 @@
 					order: 'desc'
 				},
 				TTL: 30 * 60 * 1000,	//30 minutes cache
+				showLoading: true,
 			});
+		this.store = store[this.content];
 
 		this.status = 'all';
 		this.filterTimeout = null;
@@ -411,6 +414,9 @@
 					break;
 			}
 		}
+		if (window.targetCheck(e, 'select[data-filter]')) {
+			this.handleFilterChange(e);
+		}
 	}
 	handleTableChange(e) {
 		const row = e.target.closest('tr[data-id]');
@@ -562,7 +568,7 @@
 		let filter = target.dataset.filter;
 		if (filter === 'taxonomies') {
 			let taxonomy = target.dataset.taxonomy;
-			this.store.setFilter(`tax_${taxonomy}`, filter.value);
+			this.store.setFilter(`tax_${taxonomy}`, target.value);
 		} else {
 			this[target.dataset.filter] = target.value;
 			this.store.setFilter(target.dataset.filter, target.value);
@@ -654,18 +660,6 @@
 	}
 
 	setupFilters() {
-		document.querySelectorAll('[data-filter]').forEach(filter => {
-			this.settings.addSetting(filter)
-			filter.addEventListener('change', (e) => {
-				if (this.filterTimeout) {
-					clearTimeout(this.filterTimeout);
-				}
-				this.filterTimeout = setTimeout(() => {
-					this.filterHandler(e);
-				}, 300);
-			});
-		});
-
 		// Search
 		const searchInput = document.querySelector('input[type="search"]');
 		if (searchInput) {
diff --git a/assets/js/dash/UtilityFunctions.js b/assets/js/dash/UtilityFunctions.js
index 596d6f4..566e1b1 100644
--- a/assets/js/dash/UtilityFunctions.js
+++ b/assets/js/dash/UtilityFunctions.js
@@ -843,4 +843,3 @@
 	}
 }
 window.debouncer = new DebouncedActions();
-
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index 3b0a07f..f067c97 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e){this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.content&&(this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions(),this.store=window.jvbStore.register(this.content,{keyPath:"id",endpoint:"content",headers:{action_nonce:jvbSettings.dash},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let i={};i[t]=e,this.savePosts(i,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"content"===t.endpoint&&(console.log("Queue Subscription in CRUD.js: ",t),"operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init())}handleFormChange(e,t){let i=t.fullData.post_title,s=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=s,void this.savePosts(l,i);let o=[];switch(!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=s,i=`Saving ${i} Changes`,s.post_status&&this.shouldRemoveItem(s.post_status)&&o.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let a=t.config.element.querySelectorAll(".selected input:checked");a.forEach((e=>{l[e.value]=s,s.post_status&&this.shouldRemoveItem(s.post_status)&&o.push(e.value)})),i=`Updating ${a.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=s,i=`Saving ${i} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}window.isEmptyObject(l)||this.savePosts(l,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;for(let t in e)e[t].content||(e[t].content=this.content);let i={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(i)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")})),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,i)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const i=t.dataset.action,s=t.dataset.id;switch(i){case"edit":this.populateEditForm(s),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(s)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemID="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const i=Array.from(this.viewController.selectedItems);i.length>0&&confirm(`Delete ${i.length} items?`)&&(i.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const i=e.target,s=parseInt(t.dataset.id),l=i.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(s);if(!o)return;o.fields[l]=this.getInputValue(i),this.store.save(o);let a={};a[s]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const i=e.target,s=i.closest("[data-field]")?.dataset.field;if(!s)return;const l=parseInt(t.dataset.id),o=i.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(i);if(o){const e=o.dataset.imageId;a.fields.timeline||(a.fields.timeline={}),a.fields.timeline[e]||(a.fields.timeline[e]={}),a.fields.timeline[e][s]=n}else a.fields[s]=n;this.store.save(a);let r={};r[l]=a.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let i={},s=Array.from(this.viewController.selectedItems);s.forEach((s=>{i[s]={content:this.content},i[s][t]=e}));let l=`Adding ${s.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(i,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,i={};for(let t of this.viewController.selectedItems)i[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),window.isEmptyObject(i)||this.savePosts(i,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,i=t.dataset.filter;if("taxonomies"===i){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,i.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions.value=""}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let i=this.store.get(t);const s=window.getTemplate("bulkItem");if(!s)return;const l=s.querySelector("input[type=checkbox]"),o=s.querySelector("img");l&&(l.id=`bulk_${i.id}`,l.value=i.id,l.checked=!0),o&&i.thumbnail&&(o.src=i.thumbnail,o.alt=i.alt||""),e.append(s)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemID=e,this.ui.modals.edit.dataset.content=this.content;let i=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],i.dataset.formId=`edit-${e}`,new window.jvbPopulate(i,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){document.querySelectorAll("[data-filter]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("change",(e=>{this.filterTimeout&&clearTimeout(this.filterTimeout),this.filterTimeout=setTimeout((()=>{this.filterHandler(e)}),300)}))}));const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)}))}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})();
\ No newline at end of file
+(()=>{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
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index a84b6e9..0f078d3 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.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init(),window.addEventListener("beforeunload",(()=>this.destroy()))}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t={}){if(this.stores.has(e))return console.warn(`Store "${e}" already registered`),this.getStoreAPI(e);if(!t.keyPath)throw new Error(`Store "${e}" requires a keyPath`);const s={name:e,config:{dbName:`jvb_${e}_db`,version:1,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},db:null,data:new Map,cache:new Map,httpHeaders:new Map,filters:{...t.filters},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};return s.config.headers={"X-WP-Nonce":jvbSettings?.nonce,...s.config.headers},this.stores.set(e,s),this.subscribers.set(e,new Set),this.initStoreDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)})),this.getStoreAPI(e)}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),clearHttpHeaders:t=>this.clearHttpHeaders(e,t),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}normalizeForStorage(e){if(null==e)return e;if(e instanceof Set)return Array.from(e);if(e instanceof Map)return Object.fromEntries(e);if(Array.isArray(e))return e.map((e=>this.normalizeForStorage(e)));if("object"==typeof e){const t={};for(const[s,r]of Object.entries(e))t[s]=this.normalizeForStorage(r);return t}return e}stripDOMReferences(e,t=new WeakSet){if(null==e)return e;const s=typeof e;if("string"===s||"number"===s||"boolean"===s)return e;if("object"===s&&t.has(e))return"[Circular]";if(e instanceof HTMLElement||e instanceof NodeList||e instanceof HTMLCollection||void 0!==e.nodeType)return null;if(e instanceof Date)return e;if(Array.isArray(e))return t.add(e),e.map((e=>this.stripDOMReferences(e,t))).filter((e=>null!==e));if("object"===s){t.add(e);const s={};for(const[r,i]of Object.entries(e)){const e=this.stripDOMReferences(i,t);null!==e&&(s[r]=e)}return s}return e}async initStoreDB(e){const t=this.stores.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performStoreInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performStoreInit(e){const t=this.stores.get(e),{dbName:s,version:r}=t.config;try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{this.setupStores(e,t.config)}));this.databases.set(s,e)}t.db=this.databases.get(s),this.loadStoreDataInBackground(e),this.notify(e,"db-init")}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}t.useHttpCaching&&!e.objectStoreNames.contains("headers")&&e.createObjectStore("headers",{keyPath:"key"})}loadStoreDataInBackground(e){const t=this.stores.get(e);if(!t?.db)return;const s=[this.loadStoreData(e),this.loadStoreCache(e),this.loadStoreHeaders(e)];Promise.all(s).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async loadStoreData(e){const t=this.stores.get(e);if(t?.db)return new Promise((s=>{const r=t.db.transaction([t.config.storeName],"readonly").objectStore(t.config.storeName).getAll();r.onsuccess=r=>{const i=r.target.result||[];i.forEach((e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.notify(e,"data-loaded",{count:i.length}),s(i)},r.onerror=()=>s([])}))}async loadStoreCache(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("cache"))return new Promise((e=>{const s=t.db.transaction(["cache"],"readonly").objectStore("cache").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)})),e()},s.onerror=()=>e()}))}async loadStoreHeaders(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("headers"))return new Promise((e=>{const s=t.db.transaction(["headers"],"readonly").objectStore("headers").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{t.httpHeaders.set(e.key,e)})),e()},s.onerror=()=>e()}))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initStoreDB(e)}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers},o=t.httpHeaders.get(s);t.config.useHttpCaching&&o&&(o.etag&&(a["If-None-Match"]=o.etag),o.lastModified&&(a["If-Modified-Since"]=o.lastModified));const n=new AbortController;t.currentRequest=n;const c=await fetch(i,{method:"GET",headers:a,signal:n.signal});if(304===c.status&&r)return this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r;if(!c.ok)throw new Error(`HTTP ${c.status}: ${c.statusText}`);const d=await c.json();return t.config.useHttpCaching&&this.storeResponseHeaders(e,s,c),await this.processFetchedData(e,d,s),this.notify(e,"data-loaded",{cached:!1,items:d.items||[]}),d}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s){const r=this.stores.get(e),i=t.items||[];for(const t of i)await this.save(e,t);const a={key:s,items:i.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters}};r.cache.set(s,a),await this.saveToCache(e,s,a),r.lastResponse={has_more:t.has_more||!1,total:t.total||i.length,pages:t.pages||1}}async save(e,t){const s=this.stores.get(e);let r=this.normalizeForStorage(t);if(r=this.stripDOMReferences(r),s.config.validateData){const t=this.validateSerializable(r);if(!t.valid)throw console.error(`Cannot save non-serializable data to store "${e}":`,t.error),new Error(`Non-serializable data: ${t.error}`)}const i=this.getItemKey(r,s.config.keyPath);if(s.data.set(i,t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.put(r)}return this.notify(e,"item-saved",{item:t,key:i}),i}validateSerializable(e,t="root"){if(null==e)return{valid:!0};const s=typeof e;if("string"===s||"number"===s||"boolean"===s)return{valid:!0};if("function"===s)return{valid:!1,error:`Function at ${t}`};if(e instanceof Date)return{valid:!0};if(e instanceof HTMLElement||e instanceof NodeList||e instanceof HTMLCollection||void 0!==e.nodeType)return{valid:!1,error:`DOM element at ${t}`};if(e instanceof FormData)return{valid:!1,error:`FormData at ${t}. Convert to object first.`};if(e instanceof Blob||e instanceof File)return{valid:!1,error:`Blob/File at ${t}. Handle file uploads separately.`};if(Array.isArray(e)){for(let s=0;s<e.length;s++){const r=this.validateSerializable(e[s],`${t}[${s}]`);if(!r.valid)return r}return{valid:!0}}if("object"===s){if(e instanceof Set)return{valid:!1,error:`Set at ${t}. Convert to Array first: Array.from(set)`};if(e instanceof Map)return{valid:!1,error:`Map at ${t}. Convert to Object first: Object.fromEntries(map)`};for(const[s,r]of Object.entries(e)){const e=this.validateSerializable(r,`${t}.${s}`);if(!e.valid)return e}return{valid:!0}}return{valid:!1,error:`Unknown type at ${t}: ${s}`}}async delete(e,t){const s=this.stores.get(e);if(s.data.delete(t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.delete(t)}this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);return r&&r.items?r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);if(t.data.clear(),t.cache.clear(),t.db){const e=t.db.transaction([t.config.storeName],"readwrite").objectStore(t.config.storeName);await e.clear()}this.notify(e,"data-cleared")}setFilter(e,t,s){const r=this.stores.get(e),i=r.filters[t];null==s||""===s?delete r.filters[t]:r.filters[t]=s,this.notify(e,"filters-changed",{filters:r.filters,changed:{key:t,oldValue:i,newValue:s}}),r.config.endpoint&&this.fetch(e)}async setFilters(e,t){const s=this.stores.get(e);Object.keys(t).some((e=>s.filters[e]!==t[e]))&&(s.filters={...s.filters,...t},this.notify(e,"filters-changed",{filters:s.filters,changed:t}),s.config.endpoint&&await this.fetch(e))}removeFilter(e,t){const s=this.stores.get(e),r=s.filters[t];void 0!==r&&(delete s.filters[t],this.notify(e,"filters-changed",{filters:s.filters,removed:{key:t,oldValue:r}}),s.config.endpoint&&this.fetch(e))}clearFilters(e){const t=this.stores.get(e),s={...t.filters};t.filters={...t.config.filters},this.notify(e,"filters-cleared",{oldFilters:s,filters:t.filters}),t.config.endpoint&&this.fetch(e)}clearCache(e){const t=this.stores.get(e);if(t.cache.clear(),t.db&&t.db.objectStoreNames.contains("cache")){t.db.transaction(["cache"],"readwrite").objectStore("cache").clear()}this.notify(e,"cache-cleared")}clearHttpHeaders(e,t=null){const s=this.stores.get(e);if(t){if(s.httpHeaders.delete(t),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").delete(t)}}else if(s.httpHeaders.clear(),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").clear()}}subscribe(e,t){const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}storeResponseHeaders(e,t,s){const r=this.stores.get(e),i={key:t,etag:s.headers.get("ETag"),lastModified:s.headers.get("Last-Modified"),timestamp:Date.now()};if(r.httpHeaders.set(t,i),r.db&&r.db.objectStoreNames.contains("headers")){r.db.transaction(["headers"],"readwrite").objectStore("headers").put(i)}}async saveToCache(e,t,s){const r=this.stores.get(e);if(!r.db||!r.db.objectStoreNames.contains("cache"))return;const i=r.db.transaction(["cache"],"readwrite").objectStore("cache");await i.put(s)}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbStore=new e}))})();
\ No newline at end of file
+(()=>{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
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index d564fba..73ca2d3 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},this.store=window.jvbStore.register("forms",{storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4,validateData:!0,delayFetch:!0}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.isTimeline=!1,window.crudManager&&window.crudManager.isTimeline&&(this.isTimeline=!0),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.processRepeaterField=this.processRepeaterField.bind(this),this.processGroupField=this.processGroupField.bind(this),this.processLocationField=this.processLocationField.bind(this),this.processRegularField=this.processRegularField.bind(this),this.init()}async init(){this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners(),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.type&&this.handleOperationComplete(t)}))}async handleOperationComplete(e){if(e.formId)try{await this.store.delete(e.formId)}catch(e){console.warn("Failed to clear form cache:",e)}const t=this.forms.get(e.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}checkPendingForms(){this.store.getAll().filter((e=>"draft"===e.status)).forEach((e=>{const t=this.forms.get(e.formId);if(t?.element){const s=t.element.querySelector(".restore-form");s&&(s.hidden=!1),new this.populateForm(t.element,e.data)}}))}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e,t){const s=document.querySelector(`[data-form-id="${e}"]`);if(!s)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore-changes" data-form-id="${e}">Restore</button>\n        <button class="discard-changes" data-form-id="${e}">Discard</button>\n    `,s.insertBefore(r,s.firstChild),r.querySelector(".restore-changes").addEventListener("click",(async()=>{await this.restorePendingForm(e,t),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(async()=>{await this.discardPendingForm(e),r.remove()}))}async restorePendingForm(e,t){const s=document.querySelector(`[data-form-id="${e}"]`);s&&(new this.populateForm(s,t),await this.store.save({formId:e,data:t,status:"restored",timestamp:Date.now()}),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){try{await this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}catch(e){console.error("Failed to discard pending form:",e)}}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const s=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=s,e.addEventListener("submit",this.submitHandler);const r={element:e,id:s,status:"",options:{autosave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e,!0)};if(this.initializeFormFields(e,r),this.forms.set(s,r),this.store&&r.options.cache){const e=this.store.get(s);e&&e.formData&&this.showPendingNotification(e)}return r}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),s=t.element,r=t.tabs,a=s.querySelectorAll(".tab-content").length,i=s.querySelector(".form-progress .fill"),n=s.querySelector(".step-text .current"),o=s.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,s)=>{const r=s+1;t.classList.remove("current","completed","pending"),r<e?t.classList.add("completed"):r===e?t.classList.add("current"):t.classList.add("pending")}))};s.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=s.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;r.switchTab(e,!0),l(i+1),s.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=s.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;r.switchTab(e,!0),l(i-1),s.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=r.switchTab.bind(r);r.switchTab=(e,t)=>{c(e,t);const r=s.querySelector(`.tab-content[data-tab="${e}"]`);if(r){const e=parseInt(r.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let s=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(s=!1)}})),s}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const s=e.querySelector(".add-repeater-row"),r=e.querySelector(".repeater-items"),a=e.querySelector("template");s&&a&&r&&(window.Sortable&&new Sortable(r,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),s.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),r.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const s=e.querySelector(".repeater-items"),r=e.querySelector("template"),a=s.children.length,i=e.dataset.field,n=r.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const s=e.nextElementSibling;s&&"LABEL"===s.tagName&&(s.htmlFor=e.id)})),s.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const s=e.closest(".repeater"),r=s.dataset.field;e.remove(),this.updateRepeaterOrder(s,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:r,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const s=e.querySelector(".repeater-items"),r=e.dataset.field;Array.from(s.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const s=e.name.split(":");if(3===s.length){const a=s[2];e.name=`${r}:${t}:${a}`,e.id=`${r}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:r,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((s=>{const r=s.dataset.dependsOn,a=s.dataset.dependsValue,i=s.dataset.dependsOperator||"==";t.dependencies.has(r)||t.dependencies.set(r,[]),t.dependencies.get(r).push({field:s,requiredValue:a,operator:i}),this.checkFieldDependency(e,s,r,a,i)}))}checkFieldDependency(e,t,s,r,a){const i=e.querySelector(`[name="${s}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,r,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,s){const r=String(e||""),a=String(t||"");switch(s){case"==":default:return r==a;case"!=":return r!=a;case">":return parseFloat(r)>parseFloat(a);case"<":return parseFloat(r)<parseFloat(a);case">=":return parseFloat(r)>=parseFloat(a);case"<=":return parseFloat(r)<=parseFloat(a);case"contains":return r.includes(a);case"empty":return""===r;case"not_empty":return""!==r}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),s=e.closest(".field");let r=s?.querySelector(".char-count");!r&&s&&(r=document.createElement("div"),r.className="char-count",r.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,s.appendChild(r));const a=()=>{const s=e.value.length;r&&(r.querySelector(".current").textContent=s,r.classList.toggle("exceeded",s>t)),s>t&&(e.value=e.value.substring(0,t),r&&(r.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const s=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const r=this.collectFormData(t);this.notify("form-submit",{formId:t.dataset.formId,fullData:r,config:s})}else;}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const r=window.getIcon?.("check-circle");r&&(r.classList.add("success-icon"),s.prepend(r))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),this.touchedFields.add(t.field),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const r=window.getIcon?.("close-circle");r&&(r.classList.add("error-icon"),s.prepend(r)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]");switch(t=t.dataset.action,t){case"clear-form":let t=e.target.closest("form");this.store.delete(t.dataset.formId),t?.reset(),e.target.closest(".restore-form").hidden=!0;break;case"dismiss-restore":e.target.closest(".restore-form").hidden=!0}}}handleNumberClick(e,t){let s=0;if(e.target.closest(".increase")?s+=1:e.target.closest(".decrease")&&(s-=1),0!==s){let r=parseFloat(t.step);r=Math.max(r,1),e.ctrlKey&&e.shiftKey?r*=50:e.ctrlKey?r*=5:e.shiftKey&&(r*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+r*s,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,s,r,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>s?(e.value=s,r.disabled=!1):r.disabled?r.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,s=t.form||t.closest("form");if(!s)return;const r=this.forms?.get(s.dataset.formId);if(r&&(r.options.autosave||this.subscribers.size>0)){const e=r.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(s,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(r,a)}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,s=t.form||t.closest("form");if(!s)return;const r=e.target.closest("input, textarea, select");if(r){const e=this.findFieldWrapper(r);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(r)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(r,e)}const a=this.forms?.get(s.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form"))return;const t=e.target.closest("input, textarea, select");if(!t)return;let s=t.closest("form");this.showFormStatus(s.dataset.formId,"pending");const r=this.findFieldWrapper(t);if(!r)return;const a=r.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,((e,t)=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with http:// or https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const r=t.dataset.min,a=t.dataset.max;return void 0!==r&&s<parseFloat(r)?`Value must be at least ${r}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,r=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(r&&e.length>parseInt(r))||`Must be no more than ${r} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const s=this.getFieldValue(e),r=t.dataset.field;if(!this.touchedFields.has(r)&&!e.required)return!0;if(!s&&!e.required)return this.clearValidation(t),!0;if(e.required&&!s)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&s){if(!new RegExp(a).test(s)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(s))return this.showError(t,e.message),!1;if(e.test){const r=e.test(s,t);if(!0!==r)return this.showError(t,r),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}showSuccess(e,t=""){if(!e)return;const s=e.querySelector(".validation-icon.success"),r=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),s&&(s.hidden=!1),r&&(r.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const s=e.querySelector(".validation-icon.success"),r=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),s&&(s.hidden=!0),r&&(r.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),s=e.querySelector(".validation-message"),r=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),r?.classList.remove("error"),t&&(t.hidden=!0),s&&(s.hidden=!0,s.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let s=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const r=e.dataset.field;r&&this.touchedFields.add(r);this.validateField(t,e)||(s=!1,!1===s&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),s}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const s=t.target.closest("input, select");if(!s)return;const r=s.name;if(!r)return;e.querySelectorAll(`[data-show-if*="${r}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const s=e.dataset.field,r=e.querySelector(".validation-message");s&&r&&(t[s]=r.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const s=`autosave_${e.id}`;this.debouncer.schedule(s,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")})).catch((t=>{console.error("Autosave failed:",t),this.showFormStatus(e.id,"error","Failed to save changes")}));const s=this.getChangedFields(e.data,t);if(0!==Object.keys(s).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,r]of Object.entries(t))"object"==typeof r&&(s[e]=r);this.notify("form-autosave",{formId:e.id,changes:s,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const s=this.collectFormData(t.element),r=this.getChangedFields(t.data,s);return Object.keys(r).length>0}showFormStatus(e,t,s=""){let r=this.forms.get(e);if(!r.options.formStatus)return;if(r.status===t)return;r.status=t;const a=r.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove();const n={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);o&&a.prepend(o),""===s&&(s=n[t]||t),i.textContent=s,a.classList.toggle("loading",["uploading","saving"].includes(t)),"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e,t=!1){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);if(e.classList.contains("table")&&"FORM"===e.tagName)return{};const s=new FormData(e);let r={};const a={},i={};for(let[t,n]of s.entries()){if(this.ignore.includes(t)||t.endsWith("_temp"))continue;this.getFieldProcessor(t)(t,n,r,a,i,e)}return window.isEmptyObject(i)?this.mergeRepeaterData(r,a):(r=this.mergeRepeaterData(r,a),this.mergePostData(r,i))}collectTimeline(e){let t={},s={},r=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)\](.+)$/);if(a){const[,t,o]=a;if(s[t]||(s[t]={id:parseInt(t)},r.push(t)),"post_thumbnail"===o)s[t].post_thumbnail=parseInt(e.querySelector(`[name="${i}"]`).closest(".item")?.dataset.id);else{this.getFieldProcessor(o)(o,n,s[t],{},{},e)}}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=r.map((e=>s[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],t}getFieldProcessor(e){return e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((s=>{const r={};Object.keys(t[s]).forEach((e=>{const a=t[s][e];Object.keys(a).length>0&&(r[e]=a)})),e[s]=Object.values(r)})),e}mergePostData(e,t){for(let[s,r]of Object.entries(t))e[s]=r;return e}processRepeaterField(e,t,s,r,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),r[n]||(r[n]={}),r[n][o]||(r[n][o]={}),c||r[n][o][l]?(r[n][o][l]?Array.isArray(r[n][o][l])||(r[n][o][l]=[r[n][o][l]]):r[n][o][l]=[],r[n][o][l].push(t)):r[n][o][l]=t}processGroupField(e,t,s,r,a,i){const n=e.split("::"),o=n[0];s[o]||(s[o]={});let l=s[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,s,r,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(s,n)||(s[n]={},Object.hasOwn(s,"sendAll")?s.sendAll.includes(n)||s.sendAll.push(n):s.sendAll=[n]),s[n][o]=t}processRegularField(e,t,s,r,a,i){s[e=e.replace("[]","")]?(Array.isArray(s[e])||(s[e]=[s[e]]),s[e].push(t)):s[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const s=this.forms.get(e);if(!s)return;const r=s.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary"),[i,n,o]=[a.querySelector("h2"),a.querySelector(".summary"),a.querySelector(".result")],l=["sendAll",...this.ignore];for(const[e,t]of Object.entries(s.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const s=this.getFieldInfo(r,e);if(!s.label)continue;const a=this.createResultElement(o,s,t,r);a&&n.appendChild(a)}o.remove(),(t="form"!==t?r.closest(t)??r:r).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let s=e.querySelector(`label[for="${t}"]`),r=null,a=null;if(r||(r=e.querySelector(`[name="${t}"]`)),r||(r=e.querySelector(`[name="${t}[]"]`)),!r){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(s=a.querySelector("legend"),r=a.querySelector("input, select, textarea"))}if(!s&&r){const e=r.closest(".field, fieldset");e&&(s=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:r&&(i="checkbox"===r.type&&r.name.endsWith("[]")?"checkbox":"checkbox"===r.type?"true_false":"SELECT"===r.tagName&&r.multiple?"select":r.type||"text"),{label:s?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:r}}createResultElement(e,t,s,r){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(s,t.type,r);return this.isHtmlContent(o)?n.innerHTML=o:n.textContent=o,a}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,s){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,s,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,s,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,s)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${s+1}:</strong><ul>`;for(const[s,r]of Object.entries(e))if(!this.isEmptyValue(r)){const e=s.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${r}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[s,r]of Object.entries(e))if(!this.isEmptyValue(r)){const e=s.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof r||Array.isArray(r)?t+=`<li><strong>${e}:</strong> ${r}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(r)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((s=>{e[s]&&t.push(e[s])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,s=null){if(0===e.length)return"<em>None selected</em>";if(t&&s&&s.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,s.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,s){if("select"===s){const s=t.querySelector(`option[value="${e}"]`);return s?.textContent||e}if("radio"===s){const s=t.querySelector(`input[type="radio"][value="${e}"]`),r=s?.nextElementSibling;return r?.textContent||e}if("checkbox"===s){const s=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(s){const e=t.querySelector(`label[for="${s.id}"]`);if(e)return e.textContent.trim();const r=s.nextElementSibling;if("LABEL"===r?.tagName)return r.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
\ No newline at end of file
+(()=>{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
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index ad79130..283d70d 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.user=jvbSettings.currentUser,this.headers={"X-WP-Nonce":jvbSettings.nonce,...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=window.jvbStore.register("queue",{keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1}),this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const e=this.getOperationsByStatus(["completed","failed_permanent"],!1);e.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":if(t.item){const e=this.store.data.get(t.item.id);e&&e.status!==t.item.status&&this.handleOperationStatusChange(t.item,e.status)}this.hasQueuedOperations()&&this.startPolling();break;default:this.updateUI()}})),this.notify("queue-initialized",{operations:e})}handleOperationStatusChange(e,t){if(e&&t!==e.status)switch(e.status){case"completed":this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0===e.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading"),e=this.getQueue(e.id);const t=`${this.config.apiBase}${e.endpoint}`;let s;console.log(e.data),e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),s=e.data):(s=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const i=await fetch(t,{method:e.method,headers:e.headers,body:s}),a=await i.json();if(!i.ok||!1===a.success)throw new Error(a.message||`HTTP ${i.status}`);if(a.id&&e.id!==a.id){const t=this.getQueue(a.id);t?(t.data=window.deepMerge(t.data,e.data),t.status="pending",t.serverData=a,this.updateOperationStatus(t.id,t.status),this.setQueue(t),this.removeOperationFromUI(e.id),e=t):(this.clearQueue(e.id),e.id=a.id,e.status="pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e))}else e.status="pending",e.serverData=a,this.updateOperationStatus(e.id,"pending"),this.setQueue(e);this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(e,t){if(0!==(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length){["cancel","dismiss"].includes(t)&&e.forEach((e=>{this.removeOperationFromUI(e)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:jvbSettings.currentUser})});if(!i.ok){const e=await i.json().catch((()=>{}));throw new Error(e.message||`${t} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return["cancel","dismiss"].includes(t)?e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-operation`,s),this.clearQueue(e)})):(e.forEach((e=>{let s=this.getQueue(e);this.notify(`${t}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),a}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t)));if(i.retried)return i;throw s}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return e.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationsByStatus("completed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"dismiss")}}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationsByStatus("failed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"retry")}}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}handleChange(e){}initUI(){if(this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[e,t]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(e))if("object"==typeof t){this.ui[e]={};for(let[s,i]of Object.entries(t))this.ui[e][s]=this.ui.panel.querySelector(i)}else this.ui[e]=this.ui.panel.querySelector(t)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const e=Array.from(this.store.data.values()),t=this.store.lastResponse?.queue_stats||{queued:0,localProcessing:0,uploading:0,pending:0,processing:0,completed:0,failed:0,failed_permanent:0};if(this.ui.count){const s=e.length-t.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}this.ui.clearButton.disabled=0===this.getOperationsByStatus("completed").length,this.ui.retryButton.disabled=0===this.getOperationsByStatus("failed").length&&0===this.getOperationsByStatus("failed_permanent").length,Object.entries(this.ui.filters).forEach((([s,i])=>{const a="all"===s?e.length:t[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===e.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else e.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.append(t)}))}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),l=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const d=t.querySelector(".actions");d&&this.updateActionButtons(e,d)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let e=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{e--,this.ui.countdown.textContent=e,e<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}showPopup(e,t="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=e),this.ui.popup.className=`popup ${t} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(e,t=!0){return Array.isArray(e)||"string"!=typeof e||(e=[e]),t?Array.from(this.store.data.values()).filter((t=>e.includes(t.status))):Array.from(this.store.data.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new e}))})();
\ No newline at end of file
+(()=>{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
diff --git a/assets/js/min/referral.min.js b/assets/js/min/referral.min.js
index 08fc78b..f29d3f5 100644
--- a/assets/js/min/referral.min.js
+++ b/assets/js/min/referral.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector(".jvb-referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.initElements(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copy:"button.copy",login:".login",submit:"[type=submit]"},this.forms=this.container.querySelectorAll("form"),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.forms.forEach((e=>{e.addEventListener("submit",this.submitHandler)})),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("input",this.inputHandler)},onClose:()=>{this.forms.forEach((e=>{e.removeEventListener("submit",this.submitHandler)})),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("input",this.inputHandler)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this),this.changeHandler=this.handleChange.bind(this)}handleClick(e){if(e.target.classList.contains(".copy")){let t=e.target.dataset.target,r=this.container.querySelector(`#${t}`);r=!!r&&r.textContent,r&&this.handleCopy(e.target,r)}}handleChange(e){"referral-code"===e.target.id&&window.debouncer.schedule("check-referral",(()=>this.makeRequest("referrals/check-code",{code:e.target.value})))}handleInput(e){"referral-code"===e.target.id&&(e.target.value=e.target.value.toUpperCase())}initShareWidget(){this.initCopyButton(),this.loadStats()}async checkForReferral(){const e=this.getUrlParameter("seeReferral"),t=this.getUrlParameter("ref");if(!e&&!t)return;if(!t)return void this.popup.openPopup();const r=this.container.querySelector("#referral-code-input");if(!r)return;const n=t.toUpperCase();r.value=n,r.readOnly=!0,this.popup.togglePopup();try{const e=await this.validateCodeOnly(n);if(e.success){this.showReferrerBanner(e.referrer_name,n);const t=this.container.querySelector("#referral-name");t&&t.focus()}else r.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),r.readOnly=!1}this.removeUrlParameter("ref")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}/referrals/check-code`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});return await t.json()}showReferrerBanner(e,t){const r=this.container.querySelector(".referral-header");if(!r)return;const n=document.createElement("div");n.className="referrer-banner",n.innerHTML=`\n            <div class="banner-icon">🎉</div>\n            <div class="banner-content">\n                <strong>${window.escapeHtml(e)}</strong> referred you!\n                <div class="banner-code">Code: <code>${window.escapeHtml(t)}</code></div>\n            </div>\n        `,r.parentNode.insertBefore(n,r.nextSibling);const a=r.querySelector("h3");a&&(a.textContent="Complete Your Registration");const s=r.querySelector("p");s&&(s.textContent="Enter your details below to claim your welcome reward!")}handleCopy(e,t=""){if(""===t||"string"!=typeof t)return;let r=e.textContent;(navigator.clipboard||navigator.clipboard.writeText)&&navigator.clipboard.writeText(t).then((()=>{e.textContent="Copied!",e.style.background="#00a32a",setTimeout((()=>{e.textContent=r,e.style.background=""}),2e3)}))}async loadStats(){if(this.container.querySelector(".referral-stats"))try{const e=await fetch(`${jvbSettings.api}/referrals/stats`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}async handleFormSubmit(e){window.debouncer.cancel("check-referral"),e.preventDefault();const t=e.target,r=new FormData(t);let n={};this.setFormLoading(!0,t);try{let e={success:!1,message:""};"referral-code-form"===t.id?(r.get("name")||(e.message+="We need your name to know who you are."),r.get("email")||(e.message+="We need your email to confirm you have access to it."),r.get("referral_code")||(e.message+="We need the referral code to know who sent you."),r.get("name")&&r.get("email")&&r.get("referral_code")&&(n.name=r.get("name"),n.email=r.get("email"),n.code=r.get("referral_code"),e=await this.makeRequest("referrals/register",n))):"login-form"===t.id&&r.get("login-email")&&(n.type="login",n.email=r.get("login-email"),n.context={},n.context.redirect_to=window.location.href+"?seeReferral=1",console.log("Making Request with: ",n),e=await this.makeRequest("magic",n)),e.success?this.handleSuccess(e):(this.showMessage(e.message||"Something went wrong. Please try again.","error"),this.setFormLoading(!1,t))}catch(e){console.error("Error registering:",e),this.showMessage("Something went wrong. Please try again.","error"),this.setFormLoading(!1,t)}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic","referrals/register","referrals/check-code"].includes(e))return{success:!1,message:"Something went wrong (Invalid endpoint)."};console.log("Endpoint: ",e),console.log("Data: ",t);const r=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(t)});return await r.json()}handleSuccess(e){this.container.querySelectorAll("form").forEach((e=>{window.fade(e,!1)}));const t=this.container.querySelector(".success-message");t&&(t.hidden=!1,t.scrollIntoView({behavior:"smooth",block:"center"}),this.dispatchEvent("emailSent",{email:e.email}))}setFormLoading(e,t){t.querySelectorAll("input").forEach((t=>t.disabled=e));let r=t.querySelector(".status"),n=r.querySelector(".message");r.hidden=e,r.classList.toggle("loading",e),n.textContent=e?"Checking with server...":""}showMessage(e,t="success"){const r=this.container.querySelector("#referral-message");r&&(r.textContent=e,r.className="message "+t,r.style.display="block","error"===t&&setTimeout((()=>{r.style.display="none"}),5e3))}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbReferral=new e}))})();
\ No newline at end of file
+(()=>{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
diff --git a/assets/js/min/selector.min.js b/assets/js/min/selector.min.js
index b94488e..a54c866 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,this.store=window.jvbStore.register("taxonomies",{storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:12e4,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy",delayFetch:!0}),this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.hasAutocomplete&&window.jvbTaxCreator&&(this.creator=new window.jvbTaxCreator(this)),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":const e=this.store.filters.taxonomy;if(e?.includes(",")&&this.handleBatchDataLoaded(e,t),e){(e.includes(",")?e.split(",").map((e=>e.trim())):[e]).forEach((e=>{this.updateFieldsForTaxonomy(e)}))}if(this.modal?.open&&this.handleTermsLoaded(t),this.isAutocompleteActive&&this.activeField){const e=this.fields.get(this.activeField),i=t.data?.items||[],s=t.filters?.search||"";this.showAutocompleteResults(e,i,s),this.isAutocompleteActive=!1}break;case"filters-changed":this.modal?.open&&this.showLoading();break;case"fetch-error":this.isAutocompleteActive&&this.activeField&&(this.showAutocompleteError(this.activeField),this.isAutocompleteActive=!1),this.handleFetchError(t.error)}}handleTermsLoaded(e){this.hideLoading();const t=this.store.getFiltered(),i=this.store.lastResponse?.page||{},s=e.filters?.search&&e.filters.search.length>0,o=i.page>1;this.notify("terms-loaded",{terms:t,filters:e.filters}),0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,o)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldButtonState(e){const t=this.fields.get(e);if(!t)return;const i=Array.from(this.store.data.values()).some((e=>e.taxonomy===t.taxonomy));t.toggle&&(t.toggle.disabled=!i&&!t.canCreate,t.toggle.title=i?`Select ${this.getPlural(t.taxonomy)}`:`No ${this.getSingular(t.taxonomy)} available`)}updateFieldsForTaxonomy(e){this.getFieldsForTaxonomy(e).forEach((e=>{this.updateFieldButtonState(e.id)}))}getFieldsForTaxonomy(e){return Array.from(this.fields.values()).filter((t=>t.taxonomy===e))}scanExistingFields(e=null){e||(e=document.body);e.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return!1;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=Object.hasOwn(t,"button")?t.button:e.querySelector("button.taxonomy-toggle");Object.hasOwn(t,"buttonSelector")&&this.triggers.add(t.buttonSelector);let r={id:s,input:i,container:e,taxonomy:o.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(o.dataset.max)||0,canSearch:"search"in o.dataset,hasAutocomplete:"autocomplete"in o.dataset,autocompleteDropdown:e.querySelector(".autocomplete-dropdown")??!1,canCreate:"creatable"in o.dataset,isRequired:"required"in o.dataset,selectedTerms:new Set,toggle:o,selectedContainer:Object.hasOwn(t,"selected")?t.selected:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&r.hasAutocomplete&&(this.hasAutocomplete=!0,this.initAutocomplete());const a=i.value.trim();if(""!==a){a.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>r.selectedTerms.add(e)))}return Object.hasOwn(t,"selectedItems")&&t.selectedItems.forEach((e=>{r.selectedTerms.add(e)})),this.fields.set(s,r),this.isInitializing&&this.taxonomiesToFetch.add(r.taxonomy),r.selectedTerms.size>0&&this.initFieldDisplay(s),s}registerFilterButton(e,t={}){const i=this.createFieldId(e);e.dataset.fieldId=i,t.buttonSelector&&this.triggers.add(t.buttonSelector);const s={id:i,input:null,container:t.container||e.closest(".filters")||e.parentElement,taxonomy:e.dataset.taxonomy,name:`filter_${e.dataset.taxonomy}`,maxSelection:parseInt(e.dataset.max)||0,canSearch:"search"in e.dataset,hasAutocomplete:!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(t.selectedItems||[]),toggle:e,selectedContainer:t.selected||null,isFilterMode:!0,...t};return this.fields.set(i,s),this.isInitializing?this.taxonomiesToFetch.add(s.taxonomy):this.store.setFilter("taxonomy",s.taxonomy),i}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;Array.from(t.selectedTerms).forEach((t=>{const i=this.store.get(t);i&&this.addTermToDisplay(e,i.id,i.name,i.path)}))}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),this.hasAutocomplete&&this.initAutocomplete()}initAutocomplete(){this.autocompleteHandler=window.debounce((e=>this.handleAutocomplete(e)),300),document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this)),document.addEventListener("focus",(e=>{if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);i&&this.preloadTaxonomy(i.taxonomy)}),!0)}handleClick(e){const t=window.targetCheck(e,Array.from(this.triggers));if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t,!0)}catch(e){console.error("Error handling toggle click:",e),this.error?.log&&this.error.log(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e,t=!1){this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=this.getSingular(this.currentConfig.taxonomy),this.currentPlural=this.getPlural(this.currentConfig.taxonomy),t&&this.modalInstance.handleOpen(),this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms.forEach((e=>{const t=this.store.get(e);t&&this.selectedTerms.set(e,{id:e,name:t.name,path:t.path})}))}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,hasAutocomplete:!1,autocompleteDropdown:document.querySelector(".autocomplete-dropdown")??!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s,!0),this.modalInstance.handleOpen()}openModal(){this.currentConfig?(!this.creator&&this.currentConfig.canCreate&&"jvbTaxCreator"in window&&(this.creator=new window.jvbTaxCreator(this)),this.updateModalForTaxonomy(),this.updateModalSelections(),this.updateSelectionCount(),window.removeChildren(this.ui.termsList),this.showLoading()):console.error("No active field set")}updateSelectionCount(){if(!this.currentConfig)return;const e=this.selectedTerms.size,t=this.currentConfig.maxSelection,i=this.modal?.querySelector(".selection-count");i&&(i.textContent=t>0?`${e} of ${t} selected`:`${e} selected`)}getSingular(e){return jvbSettings.labels[e]?.single||e}getPlural(e){return jvbSettings.labels[e]?.plural||e}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.notify("selected-terms",{terms:this.selectedTerms,taxonomy:this.currentConfig.taxonomy}),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),!this.hasAutocomplete&&this.creator&&delete this.creator,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const o=i.selectedContainer.querySelector(`[data-id="${s}"]`);o&&o.remove();const r=Array.from(i.selectedTerms);i.input.value=r.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const o=this.fields.get(e);if(!o||o.selectedContainer.querySelector(`[data-id="${t}"]`))return;const r=window.getTemplate("selectedTerm").cloneNode(!0);r.dataset.id=t,r.dataset.path=s,r.dataset.name=i,r.dataset.taxonomy=o.taxonomy,r.querySelector("span").textContent=s,r.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(r)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(e){const t=e.target.value.trim();this.searchHandler&&clearTimeout(this.searchHandler),this.searchHandler=setTimeout((()=>{this.store.setFilters({search:t,page:1,parent:t?0:this.store.filters.parent||0}),window.removeChildren(this.ui.termsList)}),300)}async handleAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);if(!i)return;const s=e.target.value.trim();if(i.currentAutocompleteQuery=s,s.length<2)return i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!0),void(this.isAutocompleteActive=!1);this.activeField=t,this.isAutocompleteActive=!0,i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!1),this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1})}cleanupAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target);this.fields.get(t)&&this.creator&&delete this.creator}showAutocompleteError(e){const t=this.fields.get(e);if(!t)return;t.config.autocompleteDropdown||(t.config.autocompleteDropdown=t.element.querySelector(".autocomplete-dropdown"));const i=t.config.autocompleteDropdown;i&&(window.removeChildren(i),this.showEmptyState("Hmmm... something went wrong",i))}showAutocompleteResults(e,t,i){if(!e||!e.autocompleteDropdown)return;const s=e.autocompleteDropdown;window.removeChildren(s),0===t.length?this.showEmptyState("No items found.",s):t.forEach((t=>{const i=this.createAutocompleteTermElement(e,t);i&&s.appendChild(i)}));const o=e.currentAutocompleteQuery||i;if(e.canCreate&&o&&window.jvbTaxCreator){const e=this.createNewTermOption(o);s.appendChild(e)}s.hidden=!1}createNewTermOption(e){const t=document.createElement("button");return t.type="button",t.className="autocomplete-item create-term",t.dataset.query=e,t.innerHTML=`<strong>Create:</strong> "${e}"`,t}createAutocompleteTermElement(e,t){const i=document.createElement("button");return i.type="button",i.className="autocomplete-item",i.dataset.id=t.id,i.dataset.name=t.name,i.dataset.path=t.path||t.name,i.textContent=t.path||t.name,i.addEventListener("click",(()=>{e.selectedTerms.add(parseInt(t.id)),this.addTermToDisplay(e.id,t.id,t.name,t.path),e.input.value=Array.from(e.selectedTerms).join(","),e.input.dispatchEvent(new Event("change",{bubbles:!0})),e.autocompleteDropdown.hidden=!0;const i=e.container.querySelector("input[data-autocomplete]");i&&(i.value="")})),i}navigateToParent(){this.store.setFilters({parent:0,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.termsList),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.store.setFilters({parent:t,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=0===t}loadMoreTerms(){const e=this.store.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e=null,t=!1,i=!1){if(e||(e=this.store.getFiltered()),t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.store.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s;const o=document.createDocumentFragment();e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&o.appendChild(t)})),this.ui.termsList.appendChild(o)}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),o=t.querySelector("label"),r=t.querySelector("span, .term-name");if(s&&o&&r&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,o.htmlFor=s.id,o.title=e.path||e.name,o.dataset.path=e.path,r.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.store?.filters?.search||"",t=this.store?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found.",t=null){t||(t=this.ui.termsList);const i=window.getTemplate("noResults").cloneNode(!0);e&&i.querySelector("span")&&(i.querySelector("span").textContent=e),t.appendChild(i)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),this.store.setFilters({taxonomy:e.join(","),page:1,search:"",parent:0})}handleBatchDataLoaded(e,t){const i=e.split(",").map((e=>e.trim())),s=this.store.getStore();i.forEach((e=>{const t={taxonomy:e,page:1,search:"",parent:0},i=this.generateCacheKeyForFilters(t),o={key:i,items:Array.from(this.store.data.values()).filter((t=>t.taxonomy===e)).map((e=>e.id)),timestamp:Date.now(),endpoint:s.config.endpoint,filters:t};if(s.cache.set(i,o),s.db?.objectStoreNames.contains("cache")){s.db.transaction(["cache"],"readwrite").objectStore("cache").put(o)}this.updateFieldsForTaxonomy(e)})),this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}generateCacheKeyForFilters(e){const t=Object.keys(e).sort().reduce(((t,i)=>(t[i]=e[i],t)),{});return JSON.stringify(t)}async preloadTaxonomy(e){this.store.setFilters({taxonomy:e,page:1,search:"",parent:0})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.subscribers.clear(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector=new e}))})();
\ No newline at end of file
+(()=>{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
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 51414d3..2ecf58b 100644
--- a/assets/js/min/uploader.min.js
+++ b/assets/js/min/uploader.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStore=window.jvbStore.register("upload_fields",{storeName:"fieldStates",keyPath:"id",version:2,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],TTL:6048e5,delayFetch:!0}),this.uploadStore=window.jvbStore.register("uploads",{name:"uploads",storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}],delayFetch:!0}),window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.uploadElements=new Map,this.fieldElements=new Map,this.groupElements=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.sortableInstances=new Map,this.initWorker(),this.subscribers=new Set,this.dragController=null,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.sortableConfig={animation:150,draggable:".item",handle:".select-item, img",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleReorder(e)},this.init()}async init(){this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":s&&this.handleOperationCancelled(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":this.handleOperationComplete(t,s);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),o=this.buildFieldUI(e),r={id:t,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(r),this.fieldElements.set(t,{element:e,ui:o,config:s}),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"post_group"!==s.destination||this.dragController||this.initGroupFeatures(),"single"!==s.type&&this.initSortable(t),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initSortable(e){if(!window.Sortable)return;const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((t=>{if(t.sortableInstance)return;const s=t.classList.contains("group")?`${e}-group-${t.closest(".upload-group")?.dataset.groupId}`:`${e}-preview`,o=new Sortable(t,{...this.sortableConfig,group:{name:e,pull:!0,put:!0},onAdd:e=>{this.updateSortableState(e.to)},onRemove:e=>{this.updateSortableState(e.from)}});t.sortableInstance=o,this.sortableInstances.set(s,o)}))}updateSortableState(e){const t=e?.sortableInstance;if(!t)return;const s=e.querySelectorAll(".item").length>0;t.option("disabled",!s)}refreshSortable(e){const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((e=>this.updateSortableState(e)))}handleReorder(e){const t=e.to.closest(".field, .upload");if(!t)return;let s=t.querySelector('input[type="hidden"]'),o=Array.from(t.querySelectorAll(".item")).map((e=>e.dataset.id));console.log(o),s.value=o.join(","),window.jvbA11y&&window.jvbA11y.announce("Item reordered"),t.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex},bubbles:!0}))}initGroupFeatures(){this.dragController=new window.jvbDragHandler({draggableSelector:this.selectors.items.item,dropTargetSelector:`${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,ignoreSelector:"input:not(.upload-select), button, select, textarea, details, summary, a",previewElement:"img, video, .icon",getItemId:e=>e.dataset.uploadId,getSelectedItems:e=>{const t=this.getFieldIdFromElement(e),s=e.dataset.uploadId,o=this.getCurrentSelection(t);return o&&o.includes(s)?o:[s]},validateDrop:(e,t)=>{const s=this.getFieldIdFromElement(t),o=document.querySelector(`[data-upload-id="${e[0]}"]`);return s===this.getFieldIdFromElement(o)},onDrop:(e,t)=>{this.handleItemDrop(e,t),t.scrollIntoView({behavior:"smooth",block:"center"})},onDragEnd:(e,t)=>{if(t){const t=document.querySelector(`[data-upload-id="${e[0]}"]`),s=this.getFieldIdFromElement(t),o=this.selectionHandlers.get(s);o?.clearSelection()}},previewOptions:{multiOffset:{x:-60,y:-80},singleOffset:{x:-50,y:-60},showCount:!0}})}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const o=this.getFieldIdFromElement(t);o&&(this.processFiles(o,s),this.a11y.announce(`${s.length} file(s) dropped for upload`))}handleItemDrop(e,t){const s=t.classList.contains("preview");let o=t;if(t.classList.contains("empty-group")){const e=this.getFieldIdFromElement(t),s=this.createGroup(e);if(!s)return;o=s.grid}e.forEach((e=>{s?this.removeFromGroup(e):this.addToGroup(e,o)}));const r=this.getFieldIdFromElement(t);this.schedulePersistance(r);const a=e.length>1?`Moved ${e.length} items`:"Moved item";this.a11y.announce(a)}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}if(t){const s=this.getFieldData(t);"post_group"===s?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return;o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const r=t.length;let a=0;this.updateUploadProgress(e,0,r,"Processing files...");const i=Array.from(t).map((async t=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,n={id:i,attachmentId:null,fieldId:e,status:"local_processing",groupId:null,meta:{originalName:t.name,size:t.size,type:t.type}},l=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,s.config.subtype):t;await this.saveBlobData(i,d||t);const c=this.getSubtypeFromMime(t.type),u=this.createUploadElement({id:i,preview:l,meta:n.meta,subtype:c},"post_group"===s.config.destination);return this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),o.ui.preview&&(o.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:l,location:o.ui.preview})),n.status="processed",await this.uploadStore.save(n),s.uploads.add(i),await this.saveFieldData(s),a++,this.updateUploadProgress(e,a,r,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),a++,this.updateUploadProgress(e,a,r,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((o,r)=>{let a,i=!1;a=setTimeout((()=>{i||(i=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),r(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:a}),this.handleProcess(e,t).then((e=>{i||(i=!0,clearTimeout(a),this.worker.tasks.delete(t),o(e))})).catch((e=>{i||(i=!0,clearTimeout(a),this.worker.tasks.delete(t),r(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((o,r)=>{const a=new Image,i=document.createElement("canvas"),n=i.getContext("2d");let l=null;const d=()=>{a.onload=null,a.onerror=null,l&&(URL.revokeObjectURL(l),l=null),i.width=1,i.height=1,n.clearRect(0,0,1,1)};a.onload=()=>{try{const{width:l,height:c}=this.calculateOptimalDimensions(a,t);i.width=l,i.height=c,n.imageSmoothingEnabled=!0,n.imageSmoothingQuality="high",n.drawImage(a,0,0,l,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);i.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});o(s)}else r(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),r(new Error(`Canvas processing failed: ${e.message}`))}},a.onerror=()=>{d(),r(new Error(`Failed to load image: ${e.name}`))};try{l=this.createPreviewUrl(e),a.src=l}catch(e){d(),r(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,o){return new Promise(((r,a)=>{if(!this.worker.worker)return void a(new Error("Worker not available"));const i=`${t}_${Date.now()}`,n=t=>{if(t.data.messageId===i)if(this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});r(s)}else a(new Error(t.data.error||"Worker processing failed"))},l=e=>{this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),a(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",n),this.worker.worker.addEventListener("error",l),this.worker.worker.postMessage({messageId:i,file:e,maxDimension:s,quality:o,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n\t\t\t\tself.onmessage = async function(e) {\n\t\t\t\t\tconst { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst bitmap = await createImageBitmap(file);\n\t\t\t\t\t\tconst scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n\t\t\t\t\t\tconst width = Math.round(bitmap.width * scale);\n\t\t\t\t\t\tconst height = Math.round(bitmap.height * scale);\n\t\t\t\t\t\tconst canvas = new OffscreenCanvas(width, height);\n\t\t\t\t\t\tconst ctx = canvas.getContext('2d');\n\t\t\t\t\t\tctx.imageSmoothingEnabled = true;\n\t\t\t\t\t\tctx.imageSmoothingQuality = 'high';\n\t\t\t\t\t\tctx.drawImage(bitmap, 0, 0, width, height);\n\t\t\t\t\t\tbitmap.close();\n\t\t\t\t\t\tconst blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });\n\t\t\t\t\t\tself.postMessage({ messageId, success: true, blob: blob, format: outputFormat });\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tself.postMessage({ messageId, success: false, error: error.message });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t"],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:o}=e;if(s<=t&&o<=t)return{width:s,height:o};const r=Math.min(t/s,t/o);return{width:Math.round(s*r),height:Math.round(o*r)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}async submitUploads(e){const t=this.getFieldData(e);this.fieldElements.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=this.getFieldGroups(e);if(0===o.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const r=[],a=new FormData;let i=[];for(const e of o){const t={images:[],fields:{}};for(let[s,o]of Object.entries(e.changes))t.fields[s]=o;const o=s.filter((t=>{const s=this.uploadStore.get(t);return s?.groupId===e.id}));for(const e of o){const s=await this.getBlobData(e);if(s){a.append("files[]",s);const o={upload_id:e,index:i.length},r=this.uploadElements.get(e),n=r?.element?.querySelector('[name="featured"]');n?.checked&&(t.fields.featured=e),t.images.push(o),i.push(e)}}r.push(t)}const n=s.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of n){const t={images:[],fields:{}},s=await this.getBlobData(e);if(s){a.append("files[]",s);const o={upload_id:e,index:i.length};t.images.push(o),i.push(e)}r.push(t)}a.append("content",t.config.content),a.append("user",t.config.itemID),a.append("posts",JSON.stringify(r)),a.append("upload_ids",JSON.stringify(i));const l={endpoint:"uploads/groups",method:"POST",data:a,title:`Creating ${r.length} ${t.config.content}${r.length>1?"s":""} from uploads...`,popup:`Creating ${r.length} post${r.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(l);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),this.a11y.announce(`Creating ${r.length} post${r.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}}async queueUpload(e){const t=this.getFieldData(e);if(!t?.uploads||0===t.uploads.size)return;const s=Array.from(t.uploads),o=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");const r={endpoint:"uploads",method:"POST",data:o,title:`Uploading ${s.length} file${s.length>1?"s":""} to server...`,popup:`Uploading ${s.length} file${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(r);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let o=[];const r=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const r=await this.getBlobData(e);r&&(s.append("files[]",r),o.push(t.id))}));return await Promise.all(r),s.append("upload_ids",JSON.stringify(o)),s}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),s=this.uploadStore.get(t);if(!s)return;if(!this.getFieldData(s.fieldId))return;let o={};o[e.target.name]=e.target.value,s.meta={...s.meta,...o},await this.uploadStore.save(s);let r={};r[s.attachmentId??s.id]=s.meta;const a={endpoint:"uploads/meta",method:"POST",data:r,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(a)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:s.id})}}async handleOperationComplete(e,t){if((e.result?.data||e.serverData?.data||[]).forEach((e=>{const t=this.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploadStore.save(t),this.updateUploadStatus(e.upload_id,"completed"))})),!t)return;const s=this.getFieldData(t);if(!s)return;const o=Array.from(s.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of o)await this.clearUpload(e,!1),s.uploads.delete(e);0===s.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(s),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const s=this.uploadStore.get(t);s&&(s.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(s),this.updateUploadStatus(t,s.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s)await this.clearUpload(e,!1);await this.clearFieldFromStores(e),this.updateFieldState(e),this.a11y.announce("Upload cancelled")}getFieldGroups(e){const t=this.getFieldData(e);return t?.groups?t.groups.map((e=>({id:e.id,uploads:e.uploads||[],changes:e.changes||{}}))):[]}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}async restoreSelectedUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,s]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=s,await this.restoreField(t))}}async restoreField(e){const{config:t,context:s,uploads:o,groups:r,id:a}=e;s?.modalType&&await this.openModalForRestore(s);let i=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!i){const e=`${t.content}_${t.itemID}_${t.name}`;i=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!i)return void console.warn(`Field ${t.name} not found for restoration`,t);let n=i.dataset.uploader;n&&this.fieldElements.has(n)||(n=this.registerUploader(i));const l=this.fieldElements.get(n),d=this.getFieldData(n);if(!l||!d)return void console.error("Failed to register field for restoration");d.state=e.state||"ready",l.ui||(l.ui=this.buildFieldUI(i)),l.ui.groups?.display&&(l.ui.groups.display.hidden=!1),l.ui.dropZone&&(l.ui.dropZone.hidden=!0),r&&r.length>0&&await this.restoreGroups(n,r);const c=o instanceof Set?Array.from(o):Array.isArray(o)?o:[];for(const e of c){const t=this.uploadStore.get(e);t&&await this.restoreUpload(n,t)}await this.saveFieldData(d),this.updateFieldState(n),this.maybeLockUploads(n),this.refreshSortable(n),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(n)}async restoreUpload(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(!s||!o)return void console.error("Field not found for upload restoration:",e);const r=await this.getBlobData(t.id);if(!r)return void console.warn("Blob data not found for upload:",t.id);const a=this.createPreviewUrl(r),i=this.getSubtypeFromMime(r.type),n=this.createUploadElement({id:t.id,preview:a,meta:t.meta||{originalName:r.name,size:r.size,type:r.type},subtype:i},"post_group"===o.config.destination);let l;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){l=e.grid;const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads||(s.uploads=[]),s.uploads.includes(t.id)||s.uploads.push(t.id))}else l=s.ui.preview,t.groupId=null}else l=s.ui.preview;l?l.appendChild(n):s.ui.preview&&(s.ui.preview.appendChild(n),l=s.ui.preview),this.uploadElements.set(t.id,{element:n,preview:a,location:l}),o.uploads||(o.uploads=new Set),o.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),l&&this.updateSortableState(l)}async restoreGroups(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(s&&o){for(const s of t){const t=this.createGroup(e,s.id);if(!t){console.warn("Failed to create group:",s.id);continue}const r=o.groups?.find((e=>e.id===s.id));if(r&&(s.changes&&(r.changes={...s.changes}),s.uploads&&(r.uploads=[...s.uploads]),s.changes)){const e=t.element.querySelector('[name*="post_title"]'),o=t.element.querySelector('[name*="post_excerpt"]');e&&s.changes.post_title&&(e.value=s.changes.post_title),o&&s.changes.post_excerpt&&(o.value=s.changes.post_excerpt)}}await this.saveFieldData(o)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:s}=e;let o=null;switch(t){case"create":o=document.querySelector('[data-action="create"]');break;case"edit":s&&(o=document.querySelector(`[data-action="edit"][data-id="${s}"]`));break;case"bulkEdit":o=document.querySelector('[data-action="bulk-edit"]')}o?(o.click(),await new Promise((e=>setTimeout(e,300)))):console.warn("Modal trigger not found for restoration:",e)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,o=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,o)).toFixed(s))+" "+["Bytes","KB","MB","GB"][o]}async clearUpload(e,t=!0){const s=this.uploadElements.get(e);if(s&&(this.revokePreviewUrl(s.preview),s.element)){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}if(this.uploadElements.delete(e),await this.uploadStore.delete(e),t){const t=this.uploadStore.get(e);t?.fieldId&&await this.schedulePersistance(t.fieldId)}}async clearFieldFromStores(e){const t=this.getFieldData(e);if(t?.uploads){const e=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const t of e)await this.uploadStore.delete(t)}await this.fieldStore.delete(e)}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}updateFieldState(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t||!s)return;const o=t.element,r=s.uploads?.size||0,a=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;o.dataset.hasUploads=r>0?"true":"false",o.dataset.uploadCount=r.toString(),o.dataset.hasGroups=a?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${r} item${1!==r?"s":""}`)}updateUploadProgress(e,t,s,o){const r=this.fieldElements.get(e);if(!r?.ui?.progress?.progress)return;const a=r.ui.progress,i=s>0?t/s*100:0;a.fill&&(a.fill.style.width=`${i}%`),a.text&&(a.text.textContent=o),a.count&&(a.count.textContent=`${t}/${s}`),a.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.getFieldData(e);s&&(s.state=t,this.saveFieldData(s))}updateUploadStatus(e,t){const s=this.uploadStore.get(e);s&&(s.status=t,this.uploadStore.save(s),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),s=this.uploadStore.get(e);if(!s||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${s.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(s.status),s.status)}showUploadProgress(e,t=!0){const s=this.uploadElements.get(e);if(!s?.element)return;const o=s.element.querySelector(".progress");o&&(t?(o.style.removeProperty("animation"),o.hidden=!1):(o.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{o.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const o=this.uploadElements.get(e);if(!o?.element)return;const r=o.element.querySelector(".progress");if(!r)return;const a=r.querySelector(".fill"),i=r.querySelector(".details"),n=r.querySelector(".icon");a&&(a.style.width=`${t}%`),s&&i&&(i.textContent=this.getStatusText(s)),s&&n&&(n.innerHTML=this.getStatusIcon(s).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t?.ui?.dropZone||!s)return;if("post_group"===s.config.destination)return;const o=s.uploads?.size||0,r=s.config?.maxFiles||999;t.ui.dropZone.hidden=o>=r,t.element.classList.toggle("at-max-uploads",o>=r)}createGroup(e,t=null){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const r=this.createGroupElement(t,e);if(!r)return null;o.ui.groups||(o.ui.groups={groups:new Map,container:null,empty:null,display:null}),o.ui.groups.groups.set(t,r),o.ui.groups.container&&o.ui.groups.empty?o.ui.groups.container.insertBefore(r,o.ui.groups.empty):o.ui.groups.container&&o.ui.groups.container.appendChild(r);const a=r.querySelector(".item-grid.group");if(this.groupElements.set(t,{element:r,grid:a,fieldId:e}),s.groups||(s.groups=[]),s.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(s),this.addGroupSelectionHandler(e,t),a){const s=new Sortable(a,{...this.sortableConfig,group:{name:e,pull:!0,put:!0},disabled:!0,onAdd:e=>this.updateSortableState(e.to),onRemove:e=>this.updateSortableState(e.from)});a.sortableInstance=s,this.sortableInstances.set(`${e}-group-${t}`,s)}return{id:t,element:r,grid:a}}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let o=window.getTemplate("groupMetadata");const r=s.querySelector(".fields");if(r&&o){r.append(o);const a=r.querySelector('[name="post_title"]'),i=r.querySelector('[name="post_excerpt"]');a&&(a.id=`${e}_title`,a.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const n=this.getFieldData(t);if(n&&""!==n.config.content){let e=s.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid.group");return a&&(a.dataset.groupId=e),s}deleteGroup(e,t=!0){const s=this.groupElements.get(e);if(!s)return;const o=this.getFieldData(s.fieldId);if(!o)return;const r=o.groups?.find((t=>t.id===e));let a=!0;t&&r?.uploads?.length>0&&(a=!window.confirm("Delete uploads in group?")),t&&a&&r?.uploads&&r.uploads.forEach((e=>{this.removeFromGroup(e)})),o.groups&&(o.groups=o.groups.filter((t=>t.id!==e)),this.saveFieldData(o)),s.element&&(s.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${s.fieldId}-group-${e}`,n=this.sortableInstances.get(i);n?.destroy&&n.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){const o=this.uploadStore.get(e),r=this.uploadElements.get(e);if(!o||!r)return;const a=this.getFieldData(o.fieldId),i=this.fieldElements.get(o.fieldId);if(!a||!i)return;if(!t&&r.location===i.ui.preview||t===r.location)return;if(o.groupId){const t=a.groups?.find((e=>e.id===o.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(o.groupId))}const n=r.element.querySelector('[name*="select-item"]');n&&(n.checked=!1);let l=r.element.querySelector('[name="featured"]');if(l&&(l.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,o.groupId=null;else{const s=t.dataset.groupId;l&&(l.name=s+"_"+l.name);const r=a.groups?.find((e=>e.id===s));r&&(r.uploads||(r.uploads=[]),r.uploads.push(e),o.groupId=s)}r.location=t,t.append(r.element),this.uploadStore.save(o),s&&this.saveFieldData(a),this.updateSortableState(t),r.location&&r.location!==t&&this.updateSortableState(r.location)}removeFromGroup(e){const t=this.uploadStore.get(e),s=this.uploadElements.get(e);if(!t||!s)return;const o=this.getFieldData(t.fieldId),r=this.fieldElements.get(t.fieldId);if(!o||!r)return;if(t.groupId){const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}r.ui?.preview&&(r.ui.preview.appendChild(s.element),s.location=r.ui.preview);const a=s.element.querySelector('[name="featured"]');a&&(a.hidden=!0,a.checked=!1),this.uploadStore.save(t),this.updateSortableState(r.ui.preview)}removeUpload(e,t){const s=this.getFieldData(e),o=this.uploadStore.get(t),r=this.uploadElements.get(t);if(!s||!o)return;if(s.uploads?.delete(t),o.groupId){const e=s.groups?.find((e=>e.id===o.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(o.groupId))}r?.element?.remove(),this.clearUpload(t),this.saveFieldData(s),this.updateFieldState(e),this.maybeLockUploads(e);const a=this.selectionHandlers.get(e);a&&a.deselect(t),this.a11y.announce("Upload removed")}handleGroupMetaChange(e){const t=this.getGroupFromElement(e);if(!t)return;const s=this.getFieldData(t.fieldId),o=s?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!o)return;o.changes||(o.changes={});let r=e.name;r.includes("group")&&(r=r.replace(`${o.id}_`,"").replace(`${o.id}[`,"").replace("]","")),o.changes[r]=e.value,this.saveFieldData(s),this.schedulePersistance(t.fieldId)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":const t=this.fieldElements.get(s);t&&(t.element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s));break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),s=t?.dataset.uploader;if(!s)return;const o=this.selected.get(s);if(o&&0!==o.size){const e=this.createGroup(s);if(!e)return;o.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${o.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,o=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(o)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,o=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(o,s),this.a11y.announce("Item removed"),this.schedulePersistance(o))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fieldElements.get(e);if(!t?.element)return;const s=new window.jvbHandleSelection({container:t.element,ui:{selectAll:t.element.querySelector('[name="select-all-uploads"]'),bulkControls:t.element.querySelector(".selection-actions"),count:t.element.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return s.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,s),s}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const o=this.groupElements.get(t);if(!o?.element)return;const r=new window.jvbHandleSelection({container:o.element,ui:{selectAll:o.element.querySelector(this.selectors.groups.selectAll),bulkControls:o.element.querySelector(this.selectors.groups.actions),count:o.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return r.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,r),r}handleSelectAll(e,t){}getCurrentSelection(e){let t=[];for(let[s,o]of this.selectionHandlers)(e===s||s.includes(e))&&o.selectedItems.size>0&&(t=t.concat([...o.selectedItems]));return t}getFieldData(e){const t=this.fieldStore.get(e);return t?(Array.isArray(t.uploads)?t.uploads=new Set(t.uploads):t.uploads||(t.uploads=new Set),Array.isArray(t.groups)||(t.groups=[]),t):null}async saveFieldData(e){await this.fieldStore.save({...e,timestamp:Date.now()})}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",getRuntimeData:e=>this.fieldElements.get(e),getStoreData:e=>this.getFieldData(e)},upload:{selector:this.selectors.items.item,key:"uploadId",getRuntimeData:e=>this.uploadElements.get(e),getStoreData:e=>this.uploadStore.get(e)},group:{selector:this.selectors.groups.container,key:"groupId",getRuntimeData:e=>this.groupElements.get(e),getStoreData:e=>{const t=this.groupElements.get(e);if(!t)return null;const s=this.getFieldData(t.fieldId);return s?.groups?.find((t=>t.id===e))}}},o=s[t];if(!o)return null;const r=e.closest(o.selector);if(!r)return null;const a=r.dataset[o.key];return{...o.getRuntimeData(a),...o.getStoreData(a)}}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){const t=this.getFromElement(e,"field");return t?.id??null}getUploadIdFromElement(e){const t=this.getFromElement(e,"upload");return t?.id??null}getGroupIdFromElement(e){const t=this.getFromElement(e,"group");return t?.id??null}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]||0}getModalType(e){if(!e?.element)return null;if(void 0!==e._cachedModalType)return e._cachedModalType;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return;s.dataset.uploadId=e.id,s.dataset.subtype=e.subtype||"image";let[o,r,a,i,n]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch(o&&(o.value=e.id),e.subtype){case"image":r&&(r.src=e.preview,r.alt=e.meta?.originalName||""),a?.remove(),i?.remove();break;case"video":a&&(a.src=e.preview),r?.remove(),i?.remove();break;case"document":const t=e.meta?.originalName||"",s=t.split(".").pop()?.toLowerCase()||"",o={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},n=window.getIcon(o[s]||"file");i&&(i.innerText=t,i.prepend(n)),r?.remove(),a?.remove()}if(n){let e=window.getTemplate("uploadMeta");e&&n.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,r=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,r&&(r.htmlFor=o)}})),s}normalizeFieldData(e){return e?(Array.isArray(e.uploads)?e.uploads=new Set(e.uploads):e.uploads||(e.uploads=new Set),Array.isArray(e.groups)||(e.groups=[]),e.groups=e.groups.map((e=>({...e,uploads:Array.isArray(e.uploads)?e.uploads:[]}))),e):null}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),1e3)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const s=await t.arrayBuffer(),o=this.uploadStore.get(e)||{id:e};o.blobData={buffer:s,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(o)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const s=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([s],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.checkForStoredUploads()}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":break;case"item-saved":this.showSaveIndicator(t.key)}}async checkForStoredUploads(){const e=this.fieldStore.getAll();console.log("Checking for stored uploads...",{fieldStates:e.length,uploadStoreSize:this.uploadStore.data.size});const t=e.filter((e=>{if(!e.uploads)return!1;return(e.uploads instanceof Set?Array.from(e.uploads):Array.isArray(e.uploads)?e.uploads:[]).some((e=>{const t=this.uploadStore.get(e);return t&&!t.operationId&&["completed","processed","local_processing","processed-original"].includes(t.status)}))}));console.log("Found pending fields:",t.length),0!==t.length&&this.showRecoveryNotification(t)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let o,r=window.getTemplate("restoreNotification");if(!r)return void console.error("Restore notification template not found");if(s>0){o=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else o=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const a=r.querySelector(".restore-details");a&&(a.textContent=o);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const o=e.querySelector(".item-grid.restore");for(const e of t.uploads){let s=window.getTemplate("uploadItem");if(!s)continue;const r=await this.getBlobData(e.id);if(r)try{const o=this.createPreviewUrl(r);let[a,i,n,l,d]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];s.dataset.uploadId=e.id,s.dataset.fieldId=t.id;let c=this.getSubtypeFromMime(r.type);switch(s.dataset.subtype=c,c){case"image":[i.src,i.alt]=[o,r.name??e.meta?.originalName??""],n.remove(),l.remove();break;case"video":n.src=o,i.remove(),l.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}l.innerText=e.originalFile.name,l.prepend(t),i.remove(),n.remove()}s.dataset.previewUrl=o}catch(t){console.warn("Failed to create preview for upload:",e.id,t)}const a=s.querySelector("summary span");a&&(a.textContent=e.meta?.originalName||"Unknown file");const i=s.querySelector("details");i&&e.meta&&(i.textContent=`${this.formatBytes(e.meta.size)} • ${e.meta.type}`),s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,r=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,r&&(r.htmlFor=o)}})),o&&o.appendChild(s)}r.querySelector(".wrap").appendChild(o)}document.querySelector(".field.upload").appendChild(r),r=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection({container:r,ui:{selectAll:r.querySelector("#select-all-restore"),count:r.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}showSaveIndicator(e){}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){await this.fieldStore.clear(),await this.uploadStore.clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.uploadElements.clear(),this.fieldElements.clear(),this.groupElements.clear(),this.selected.clear(),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
\ No newline at end of file
+(()=>{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
diff --git a/build/faq/style-index-rtl.css b/build/faq/style-index-rtl.css
index 619f4e6..2f27fa1 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}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{--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}
diff --git a/build/faq/style-index.css b/build/faq/style-index.css
index 048e200..daf20ad 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}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{--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}
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 2aa4bed..3596020 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '40e69d90292156d1e263');
+<?php return array('dependencies' => array(), 'version' => '66b80732cd4eebba785a');
diff --git a/build/feed/view.js b/build/feed/view.js
index de6b42c..85ea840 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-terms",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=this.ui.filterContainer.querySelectorAll('[name="content"]'),this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content.length>0?this.contentTypes=Array.from(this.ui.content).map((e=>e.value)):this.contentTypes=[this.container.dataset.content],this.ui.taxonomies.length>0?this.taxonomies=Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):this.taxonomies=[]}async initTaxonomies(){this.selector=window.jvbSelector;const e=document.querySelectorAll('[data-filter="taxonomy"]');this.selector.isInitializing=!0,e.forEach((e=>{const t=e.dataset.taxonomy;this.currentTaxonomies.add(t),this.selector.registerFilterButton(e,{button:e,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax})})),setTimeout((()=>{this.selector.batchFetchTaxonomies(),this.selector.isInitializing=!1}),0),this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;t.size>0?this.taxonomyFilters[i]=Array.from(t.keys()):delete this.taxonomyFilters[i];let s={page:1};Object.keys(this.taxonomyFilters).length>0&&(s.taxonomy=this.taxonomyFilters),this.updateFilter(s)}clearAllTaxonomies(){this.taxonomyFilters={},window.removeChildren(this.ui.selectedTax),this.updateFilter({taxonomy:null,page:1})}initFilters(){this.filters={content:this.contentTypes[0],orderby:"date",order:"desc",page:1},this.config.context&&(this.filters.context=this.config.context),this.config.source&&(this.filters.source=this.config.source),this.processCachedFilters(),this.processURLFilters(),this.syncUIToFilters()}syncUIToFilters(){Object.entries(this.filters).forEach((([e,t])=>{const i=this.ui.filterContainer.querySelector(`[data-filter="${e}"][value="${t}"]`);i&&(i.checked=!0)})),this.updateContentFor(this.filters.content)}nextPage(){this.store.setFilter("page",this.store.filters.page++)}initStore(){this.addPlaceholders(),this.store=window.jvbStore.register("feed",{endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content",delayFetch:!0}),this.store.subscribe(((e,t)=>{"data-loaded"===e&&(this.renderItems(),this.ui.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse.has_more&&(this.ui.loadMore.hidden=!this.store.lastResponse.has_more))}))}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse&&this.store.lastResponse.has_more&&this.nextPage()}))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get(`f_${t}`);if(i){this.filters[t]=i;let e=this.ui.filters[t];e&&(e.checked=!0)}}));let t=!1;if(e.forEach(((e,i)=>{if(i.startsWith("f_tax_")){t=!0;const s=i.replace("f_tax_","");this.taxonomyFilters[s]||(this.taxonomyFilters[s]=[]),this.taxonomyFilters[s]=e.split(",").map(Number)}})),t)for(let[e,t]in Object.entries(this.taxonomyFilters)){let i=this.ui.filterContainer.querySelector(`[data-taxonomy="${e}"]`);i&&(i.dataset.fieldId?(this.selector.get(i.dataset.fieldId).selectedTerms=new Set(t),this.selector.initFieldDisplay(i.dataset.fieldId)):this.selector.registerField(i,{button:i,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax,selectedItems:t}))}return!0}updateURL(){const e=new URLSearchParams;["content","order","orderby","match"].forEach((t=>{this.filters[t]&&e.set(`f_${t}`,this.filters[t])})),Object.entries(this.taxonomyFilters).forEach((([t,i])=>{i.length>0&&e.set(`f_tax_${t}`,i.join(","))}));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;window.history.pushState({filters:this.filters},"",t)}renderItems(){let e=this.store.getFiltered();if(1===this.store.filters.page&&(window.removeChildren(this.ui.grid),this.addPlaceholders()),0===e.length)return void this.a11y.announceItems(0,this.store.filters.page>0);const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.appendChild(r)}r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.observeImages(this.ui.grid),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,this.store.filters.page>1,this.store.hasMore))};e.length>0?i(0):this.a11y.announceItems(0,this.store.filters.page>1,!1),this.ui.filters.match.hidden=window.isEmptyObject(this.taxonomyFilters),this.ui.clearFilter.hidden=window.isEmptyObject(this.taxonomyFilters)}createItemElement(e){let t=window.getTemplate("feed-item");return Object.hasOwn(t.dataset,"timeline")?this.createTimelineElement(e,t):t}createTimelineElement(e,t){var i,s;let[r,a,o,n,l,h,d,c,m,u]=[t,t.querySelector("a"),t.querySelector("img.before"),t.querySelector("img.after"),t.querySelector("summary span:last-of-type"),t.querySelector("p.started time"),t.querySelector("p.updated time"),t.querySelector("p.total b"),t.querySelector(".term-list"),Object.values(e.fields.order)],f=u.length-1,y=e.images[u[0].post_thumbnail],g=e.images[u[f].post_thumbnail];return[r.dataset.id,a.href,o.src,o.dataset.small,o.dataset.medium,n.src,n.dataset.small,n.dataset.medium,l.textContent,h.textContent,d.textContent,c.textContent]=[e.id,e.url,y.tiny,y.small,y.medium,g.tiny,g.small,g.medium,`${l.textContent} ${f} Tx`,null!==(i=u[0].date)&&void 0!==i?i:e.date,null!==(s=u[f].date)&&void 0!==s?s:"",`${f} Treatments`],t}addPlaceholders(){let e=this.contentTypes.length;for(let t=0;t<12;t++){let t,i=window.getTemplate("placeholderTemplate"),s=Math.floor(Math.random()*e);t=this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[s])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),i.appendChild(t),this.ui.grid.appendChild(i)}}removePlaceholders(){this.ui.grid.querySelector(".placeholder")&&window.removeChildren(this.ui.grid)}updateFilter(e){let t=["taxonomy","favourites","match",...Object.keys(this.filters)];e=Object.keys(e).filter((e=>t.includes(e))).reduce(((t,i)=>(t[i]=e[i],t)),{}),window.getDifferences.map(this.filters,e)&&(this.filters={...this.filters,...e},this.updateURL(),this.store.setFilters(e))}updateContentFor(e){this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]').forEach((t=>{const i=t.dataset.for?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)})),this.ui.filterContainer.querySelectorAll("[data-for]").forEach((t=>{const i=t.dataset.for?.split(",")||[];i.length>0&&(t.hidden=!i.includes(e),t.hidden&&t.checked&&(t.checked=!1))}));const t=this.ui.filterContainer.querySelector('[name="orderby"]:checked');this.updateOrderDirectionVisibility(t?.value)}updateOrderDirectionVisibility(e){const t=this.ui.filterContainer.querySelector(".order-direction");if(t){const i=t.dataset.forOrder?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)}}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.imageObserver=null,this.resizeObserver=null,"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{this.loadImage(e.target),this.imageObserver.unobserve(e.target)}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?this.resizeObserver=new ResizeObserver(window.debounce((()=>{this.updateImageSizes()}),250)):window.addEventListener("resize",window.debounce((()=>{this.updateImageSizes()}),250)),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}loadImage(e){const t=this.getAppropriateImageSize(e);t&&t!==e.src&&(e.src=t,e.dataset.loaded="true")}getAppropriateImageSize(e){return window.innerWidth<768&&e.dataset.small?e.dataset.small:e.dataset.medium?e.dataset.medium:e.src}observeImages(e){e.querySelectorAll("img[data-small], img[data-medium]").forEach((e=>{e.dataset.loaded||this.imageObserver.observe(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(function(){window.feedBlock=new e}))})();
\ No newline at end of file
+(()=>{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
diff --git a/build/gmbreviews/render.php b/build/gmbreviews/render.php
index 7aa2dcb..43d203d 100644
--- a/build/gmbreviews/render.php
+++ b/build/gmbreviews/render.php
@@ -17,6 +17,8 @@
 	if ($inheritUser) {
 		global $post;
 		$inheritUser = $post->post_author;
+	}else {
+		$inheritUser = null;
 	}
 	try {
 		$gmb = JVB()->connect('gmb', $inheritUser);
diff --git a/build/gmbreviews/style-index-rtl.css b/build/gmbreviews/style-index-rtl.css
index 66c34c0..0e50017 100644
--- a/build/gmbreviews/style-index-rtl.css
+++ b/build/gmbreviews/style-index-rtl.css
@@ -1 +1 @@
-.gmb-reviews{max-width:var(--alignWide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;padding:0}.gmb-reviews ul li{margin:2rem 0;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){right:-2rem}.gmb-reviews ul li:nth-of-type(2n){left:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.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%}
diff --git a/build/gmbreviews/style-index.css b/build/gmbreviews/style-index.css
index d40b467..83c6f01 100644
--- a/build/gmbreviews/style-index.css
+++ b/build/gmbreviews/style-index.css
@@ -1 +1 @@
-.gmb-reviews{max-width:var(--alignWide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;padding:0}.gmb-reviews ul li{margin:2rem 0;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){left:-2rem}.gmb-reviews ul li:nth-of-type(2n){right:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--outerRadius);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.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%}
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 43831f2..726e78c 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -71,16 +71,16 @@
 		$function = BASE.$method;
 
 		if (function_exists($function)) {
+			return $function($block, $content);
 //			return $this->cache->remember(
 //				$block,
 //				function () use ($function, $block, $content) {
 //					return $function($block, $content);
 //				}
 //			);
-			return $function($block, $content);
 		} else if (method_exists($this, $method)) {
 			return $this->$method($block, $content);
-			//TODO: Recache it
+
 //			return $this->cache->remember(
 //				$block,
 //				function () use ($method, $block, $content) {
@@ -96,9 +96,9 @@
 				'core/site-title',
 				'jvb/forms'
 			];
-			if (!in_array($block['blockName'], $ignore)) {
+//			if (!in_array($block['blockName'], $ignore)) {
 //				jvbDump('No method found for '.print_r($block['blockName'], true));
-			}
+//			}
 		}
         if ($block['blockName'] === 'jvb/feed') {
             // Enqueue the feed block script (it will automatically load dependencies)
@@ -603,7 +603,9 @@
             'main';
 
         if ($content == '') {
-            return do_blocks(get_the_content(get_the_ID()));
+			global $post;
+			$block['innerBlocks'] = parse_blocks($post->post_content);
+			return $this->innerBlocks($block);
         } else {
             return $this->inside($block, $tag, $content);
         }
@@ -619,9 +621,15 @@
     {
 		global $post;
 		$ID = get_post_thumbnail_id($post->ID);
-        return '<figure'.$this->getClassesAndStyles($block['attrs']).'>'.
-               $this->image($ID).
-               '</figure>';
+		$aOpen = $aClose = '';
+		if(!is_single($ID)) {
+			$aOpen = '<a href="'.get_the_permalink($post->ID).'">';
+			$aClose = '</a>';
+		}
+
+        return $aOpen.'<figure'.$this->getClassesAndStyles($block['attrs']).'>'.
+               apply_filters('jvbCoreFeaturedImage', $this->image($ID), $post->post_type).
+               '</figure>'.$aClose;
     }
     //core_post_navigation_link
     //core_post_template
@@ -673,97 +681,103 @@
 
 	protected function render_core_query(array $block, string $content):string
 	{
-//		jvbDump($block);
-//		$queryID = $block['attrs']['queryId'];
-//		$args = [];
-//		$inherit = $block['attrs']['inherit']??false;
-//		if ($inherit) {
-//			global $wp_query;
-//			$loop = $wp_query;
-//		} else {
-//			foreach ($block['attrs']['query'] as $key => $value) {
-//				if (empty($value)) {
-//					continue;
-//				}
-//				switch ($key) {
-//					case 'postType':
-//						$args['post_type'] = $value;
-//						break;
-//					case 'perPage':
-//						$args['posts_per_page'] = $value;
-//						break;
-//					case 'orderBy':
-//						$args['orderby'] = $value;
-//						break;
-//					case 'taxQuery':
-//						$taxQuery = [];
-//						foreach ($value as $tax => $terms) {
-//							$taxQuery[] = [
-//								'taxonomy' 	=> $tax,
-//								'terms'		=> $terms
-//							];
-//						}
-//						if (!empty($taxQuery)) {
-//							$args['tax_query'] = $taxQuery;
-//							if (count($taxQuery) > 1) {
-//								$args['tax_query']['relation'] = 'OR';
-//							}
-//						}
-//						break;
-//					case 'sticky':
-//						if ($value === 'ignore') {
-//							$args['ignore_sticky_posts'] = true;
-//						} else if ($value === 'exclude'){
-//							$args['post__not_in'] = get_option('sticky_posts');
-//						} else if ($value === 'only') {
-//							$args['include'] = get_option('sticky_posts');
-//						}
-//						break;
-//					case 'search':
-//						$args['s'] = $value;
-//						break;
-//					default:
-//						$args[$key] = $value;
-//						break;
-//
-//				}
-//			}
-//			//Add in any args from the query string
-//			$search = 'query-'.$queryID;
-//			foreach ($_GET as $key => $value) {
-//				if (str_contains($key, $search)) {
-//					$key = str_replace($search, '', $key);
-//					if ($key === 'page') {
-//						$args['paged'] = (int)$value;
-//					}
-//				}
-//			}
-//			$loop = new WP_Query($args);
-//		}
 
-//		$inner = $this->innerBlocks($block);
-//		foreach ($block['innerBlocks'] as $innerBlock) {
-//			switch ($innerBlock['blockName']) {
-//				case 'core/post-template':
-//					$inner .= '<ul class="item-grid">';
-//					if ($loop->have_posts()) {
-//						while($loop->have_posts()) {
-//							$loop->the_post();
-//							$inner .= $this->doBlocks
-//						}
-//					}
-//					$inner .= '</ul>';
-//					break;
-//			}
-//		}
+		$queryID = $block['attrs']['queryId'];
+		$args = [];
+		$inherit = $block['attrs']['inherit']??false;
+		if ($inherit) {
+			global $wp_query;
+			$loop = $wp_query;
+		} else {
+			foreach ($block['attrs']['query'] as $key => $value) {
+				if (empty($value)) {
+					continue;
+				}
+				switch ($key) {
+					case 'postType':
+						if ($value === BASE.'progress'){
+							$args['post_parent'] = 0;
+						}
+						$args['post_type'] = $value;
+						break;
+					case 'perPage':
+						$args['posts_per_page'] = $value;
+						break;
+					case 'orderBy':
+						$args['orderby'] = $value;
+						break;
+					case 'taxQuery':
+						$taxQuery = [];
+						foreach ($value as $tax => $terms) {
+							$taxQuery[] = [
+								'taxonomy' 	=> $tax,
+								'terms'		=> $terms
+							];
+						}
+						if (!empty($taxQuery)) {
+							$args['tax_query'] = $taxQuery;
+							if (count($taxQuery) > 1) {
+								$args['tax_query']['relation'] = 'OR';
+							}
+						}
+						break;
+					case 'sticky':
+						if ($value === 'ignore') {
+							$args['ignore_sticky_posts'] = true;
+						} else if ($value === 'exclude'){
+							$args['post__not_in'] = get_option('sticky_posts');
+						} else if ($value === 'only') {
+							$args['include'] = get_option('sticky_posts');
+						}
+						break;
+					case 'search':
+						$args['s'] = $value;
+						break;
+					default:
+						$args[$key] = $value;
+						break;
+
+				}
+			}
+			//Add in any args from the query string
+			$search = 'query-'.$queryID;
+			foreach ($_GET as $key => $value) {
+				if (str_contains($key, $search)) {
+					$key = str_replace($search, '', $key);
+					if ($key === 'page') {
+						$args['paged'] = (int)$value;
+					}
+				}
+			}
+			$loop = new WP_Query($args);
+		}
+
+		$inner = '';
+
+		foreach ($block['innerBlocks'] as $innerBlock) {
+			switch ($innerBlock['blockName']) {
+				case 'core/post-template':
+					$inner .= '<section class="item-grid">';
+					if ($loop->have_posts()) {
+						while($loop->have_posts()) {
+							$loop->the_post();
+							$postType = get_post_type();
+							$inner .= '<div class="item '.jvbNoBase($postType).'">'.$this->innerBlocks($innerBlock).'</div>';
+						}
+					}
+					$inner .= '</section>';
+					break;
+
+			}
+		}
 
 
 
 		$tagName = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
-		$out =  '<'.$tagName.' class="loop">'.$this->innerBlocks($block).'</'.$tagName.'>';
-//		if ($inherit) {
-//			wp_reset_postdata();
-//		}
+		$out =  '<'.$tagName.' class="loop">'.$inner.'</'.$tagName.'>';
+		if ($inherit) {
+			wp_reset_postdata();
+		}
 		return $out;
 	}
 
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 1f9703a..7a5158b 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -317,7 +317,19 @@
 	protected function renderGrid(): void
 	{
 		?>
-		<div class="item-grid"></div>
+		<div class="item-grid">
+			<?php
+			$total = count($this->config['content']) - 1;
+			for ($i = 1; $i <= 36; $i++) {
+				$rand = rand(0, $total);
+				$config = Features::getConfig($this->config['content'][$rand]);
+				$icon = jvbIcon($config['icon']??'logo-triangle');
+				?>
+				<div class="placeholder"><?=apply_filters('jvbFeedPlaceholder', $icon) ?></div>
+				<?php
+			}
+			?>
+		</div>
 		<?php
 	}
 
@@ -377,8 +389,7 @@
                 <p>Try tweaking those filters a bit.</p>
                 <p>Edmonton\'s got talent - let\'s find it.</p>
             </div>', $this->config). '</template>';
-
-		echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', '').'</div></template>';
+		echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon('logo-triangle')).'</div></template>';
 	}
 
 	/**
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 4817908..3802bda 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -475,6 +475,10 @@
 							</div>
 						</div>
 						<div class="m-actions row nowrap">
+							<button type="button" data-action="restore-all">
+								<?= jvbIcon('infinity') ?>
+								Restore All
+							</button>
 							<button type="button" data-action="restore">
 								<?= jvbIcon('arrow-counter-clockwise') ?>
 								Restore Selected
diff --git a/inc/importers/JaneAppClientImporter.php b/inc/importers/JaneAppClientImporter.php
index 7f04b5c..6290c19 100644
--- a/inc/importers/JaneAppClientImporter.php
+++ b/inc/importers/JaneAppClientImporter.php
@@ -17,7 +17,8 @@
 	protected $wpdb;
 	protected string $jane_clients_table;
 	protected array $import_stats = [];
-
+	protected int $lineNumber = 0;
+	protected array $skipped_details = [];
 	// CSV column mapping
 	protected array $column_map = [
 		'patient_guid' => 'patient_guid',
@@ -25,6 +26,7 @@
 		'last_name' => 'Last Name',
 		'email' => 'Email',
 	];
+	protected array $headers = [];
 
 	public function __construct()
 	{
@@ -38,10 +40,13 @@
 	 *
 	 * @param string $file_path Path to the CSV file
 	 * @param array $options Import options (e.g., update_existing, send_welcome_email)
-	 * @return array Import results with stats and errors
+	 * @return array|WP_Error Import results with stats and errors
 	 */
-	public function importFromCSV(string $file_path, array $options = []): array
+	public function importFromCSV(string $file_path, array $options = []): array|WP_Error
 	{
+		$this->skipped_details = [];
+		$this->lineNumber = 0; // Reset line number
+
 		// Initialize stats
 		$this->import_stats = [
 			'total_rows' => 0,
@@ -50,7 +55,8 @@
 			'updated' => 0,
 			'skipped' => 0,
 			'errors' => [],
-			'unmatched_emails' => []
+			'unmatched_emails' => [],
+			'skipped_details' => [] // Add this
 		];
 
 		// Validate file exists
@@ -75,9 +81,10 @@
 			fclose($handle);
 			return new WP_Error('invalid_csv', 'CSV file is empty or invalid');
 		}
+		$this->headers = array_map('trim', $headers);
 
 		// Map column indices
-		$column_indices = $this->mapColumnIndices($headers);
+		$column_indices = $this->mapColumnIndices($this->headers);
 		if (is_wp_error($column_indices)) {
 			fclose($handle);
 			return $column_indices;
@@ -90,11 +97,13 @@
 			// Process each row
 			while (($row = fgetcsv($handle)) !== false) {
 				$this->import_stats['total_rows']++;
+				$this->lineNumber++;
 
 				$result = $this->processClientRow($row, $column_indices, [
 					'update_existing' => $update_existing,
 					'send_welcome_email' => $send_welcome_email,
-					'create_users' => $create_users
+					'create_users' => $create_users,
+					'default_role' => $options['default_role'] ?? null
 				]);
 
 				if (is_wp_error($result)) {
@@ -125,6 +134,9 @@
 
 		fclose($handle);
 
+		// Add skipped details to stats
+		$this->import_stats['skipped_details'] = $this->skipped_details;
+
 		return $this->import_stats;
 	}
 
@@ -170,39 +182,90 @@
 
 		// Validate required fields
 		if (empty($patient_guid) || empty($email)) {
+			$this->skipped_details[] = [
+				'name' => $first_name . ' ' . $last_name,
+				'guid'	=> $patient_guid,
+				'email' => $email,
+				'reason' => 'Missing guid or email',
+				'line' => $this->lineNumber
+			];
 			return new WP_Error('invalid_data', 'Missing patient_guid or email');
 		}
 
 		// Sanitize email
 		$email = sanitize_email($email);
 		if (!is_email($email)) {
+			$this->skipped_details[] = [
+				'name' => $first_name . ' ' . $last_name,
+				'guid'	=> $patient_guid,
+				'email' => $email,
+				'reason' => 'Invalid Email',
+				'line' => $this->lineNumber
+			];
 			return new WP_Error('invalid_email', 'Invalid email address: ' . $email);
 		}
 
 		// Check if client already exists in mapping table
 		$existing_mapping = $this->getClientByGuid($patient_guid);
 
+		// Build full data array with all CSV columns for meta storage
+		$data = [];
+		foreach ($row as $index => $value) {
+			$header = $this->headers[$index] ?? 'unknown_' . $index;
+			$data[$header] = trim($value);
+		}
+
+		// Ensure these keys exist for backward compatibility
+		$data['patient_guid'] = $patient_guid;
+		$data['First Name'] = $first_name;
+		$data['Last Name'] = $last_name;
+		$data['Email'] = $email;
+
 		// Find or create WordPress user
 		$user = get_user_by('email', $email);
+		$action = 'existing';
 
-		if (!$user && $options['create_users']) {
+		if ($user) {
+			if ($options['update_existing']) {
+				$this->updateExistingClient($user->ID, $data, $options);
+				$action = 'updated';
+			} else {
+				$this->skipped_details[] = [
+					'name' => $first_name . ' ' . $last_name,
+					'email' => $email,
+					'reason' => 'User already exists (update not enabled)',
+					'line' => $this->lineNumber
+				];
+				return new WP_Error('user_exists', 'User already exists');
+			}
+		} elseif ($options['create_users']) {
 			// Create new user
-			$user_id = $this->createWordPressUser($email, $first_name, $last_name, $options['send_welcome_email']);
+			$user_id = $this->createClientUser($data, $options);
 
 			if (is_wp_error($user_id)) {
+				$this->skipped_details[] = [
+					'name' => $first_name . ' ' . $last_name,
+					'guid'	=> $patient_guid,
+					'email' => $email,
+					'reason' => $user_id->get_error_message(),
+					'line' => $this->lineNumber
+				];
 				return $user_id;
 			}
 
 			$user = get_user_by('ID', $user_id);
 			$action = 'created';
-
-		} elseif (!$user) {
+		} else {
 			// User doesn't exist and we're not creating users
+			$this->skipped_details[] = [
+				'name' => $first_name . ' ' . $last_name,
+				'guid'	=> $patient_guid,
+				'email' => $email,
+				'reason' => 'User not found and create_users is false',
+				'line' => $this->lineNumber
+			];
 			$this->import_stats['unmatched_emails'][] = $email;
 			return new WP_Error('user_not_found', 'User not found and create_users is false');
-
-		} else {
-			$action = 'existing';
 		}
 
 		// Update or insert client mapping
@@ -214,7 +277,6 @@
 					'last_name' => $last_name,
 					'email' => $email
 				]);
-				$action = 'updated';
 			}
 		} else {
 			$this->insertClientMapping([
@@ -224,9 +286,6 @@
 				'last_name' => $last_name,
 				'email' => $email
 			]);
-			if ($action !== 'created') {
-				$action = 'mapped';
-			}
 		}
 
 		return [
@@ -237,49 +296,134 @@
 	}
 
 	/**
-	 * Create a new WordPress user
+	 * Create a new client user from Jane App data
 	 *
-	 * @param string $email User email
-	 * @param string $first_name First name
-	 * @param string $last_name Last name
-	 * @param bool $send_welcome_email Whether to send welcome email
+	 * @param array $data Client data from CSV
+	 * @param array $options Import options
 	 * @return int|WP_Error User ID or error
 	 */
-	protected function createWordPressUser(string $email, string $first_name, string $last_name, bool $send_welcome_email = false): int|WP_Error
+	protected function createClientUser(array $data, array $options)
 	{
+		$email = sanitize_email($data['Email']);
+		$first_name = sanitize_text_field($data['First Name'] ?? '');
+		$last_name = sanitize_text_field($data['Last Name'] ?? '');
+
 		// Generate username from email
-		$username = $this->generateUsername($email);
+		$username = sanitize_user(substr($email, 0, strpos($email, '@')));
 
-		// Generate random password
-		$password = wp_generate_password(12, true, true);
+		// Ensure unique username
+		$base_username = $username;
+		$counter = 1;
+		while (username_exists($username)) {
+			$username = $base_username . $counter;
+			$counter++;
+		}
 
-		$userdata = [
+		// Get the role from options with proper fallback
+		$role = $options['default_role'] ?? get_option(BASE . 'client_import_role', JVB_USER);
+
+		// Ensure role exists, fallback to JVB_USER if not
+		if (!get_role($role)) {
+			$role = JVB_USER;
+		}
+
+		// Create user
+		$user_data = [
 			'user_login' => $username,
 			'user_email' => $email,
-			'user_pass' => $password,
 			'first_name' => $first_name,
 			'last_name' => $last_name,
 			'display_name' => trim($first_name . ' ' . $last_name),
-			'role' => apply_filters(BASE . 'jane_import_default_role', 'customer')
+			'role' => $role,
+			'user_pass' => wp_generate_password(16, true, true)
 		];
 
-		$user_id = wp_insert_user($userdata);
+		$user_id = wp_insert_user($user_data);
 
 		if (is_wp_error($user_id)) {
 			return $user_id;
 		}
 
-		// Send welcome email if requested
-		if ($send_welcome_email) {
-			wp_send_new_user_notifications($user_id, 'both');
+		// Store Jane App data as user meta
+		$this->storeClientMeta($user_id, $data);
+
+		// Send welcome email if enabled
+		if ($options['send_welcome_email'] ?? false) {
+			wp_new_user_notification($user_id, null, 'user');
 		}
 
-		do_action(BASE . 'jane_client_created', $user_id, $userdata);
-
 		return $user_id;
 	}
 
 	/**
+	 * Store Jane App client data as user meta
+	 *
+	 * @param int $user_id
+	 * @param array $data
+	 */
+	protected function storeClientMeta(int $user_id, array $data): void
+	{
+		// Store Jane App specific fields
+		if (!empty($data['patient_guid'])) {
+			update_user_meta($user_id, BASE . 'jane_patient_guid', sanitize_text_field($data['patient_guid']));
+		}
+
+		if (!empty($data['Patient Number'])) {
+			update_user_meta($user_id, BASE . 'jane_patient_number', sanitize_text_field($data['Patient Number']));
+		}
+
+		if (!empty($data['Member Since'])) {
+			update_user_meta($user_id, BASE . 'member_since', sanitize_text_field($data['Member Since']));
+		}
+
+		if (!empty($data['Mobile Phone'])) {
+			update_user_meta($user_id, BASE . 'phone', sanitize_text_field($data['Mobile Phone']));
+		}
+
+		if (!empty($data['Birth Date'])) {
+			update_user_meta($user_id, BASE . 'birth_date', sanitize_text_field($data['Birth Date']));
+		}
+
+		if (!empty($data['Referral Source'])) {
+			update_user_meta($user_id, BASE . 'referral_source', sanitize_text_field($data['Referral Source']));
+		}
+
+		// Store full Jane App data as JSON for reference
+		update_user_meta($user_id, BASE . 'jane_import_data', $data);
+		update_user_meta($user_id, BASE . 'jane_import_date', current_time('mysql'));
+	}
+
+	/**
+	 * Update existing client with Jane App data
+	 *
+	 * @param int $user_id
+	 * @param array $data
+	 * @param array $options
+	 */
+	protected function updateExistingClient(int $user_id, array $data, array $options): void
+	{
+		// Update user fields if they're empty
+		$user_data = ['ID' => $user_id];
+
+		$current_user = get_user_by('ID', $user_id);
+
+		if (empty($current_user->first_name) && !empty($data['First Name'])) {
+			$user_data['first_name'] = sanitize_text_field($data['First Name']);
+		}
+
+		if (empty($current_user->last_name) && !empty($data['Last Name'])) {
+			$user_data['last_name'] = sanitize_text_field($data['Last Name']);
+		}
+
+		if (count($user_data) > 1) {
+			wp_update_user($user_data);
+		}
+
+		// Always update meta data
+		$this->storeClientMeta($user_id, $data);
+	}
+
+	/**
 	 * Generate unique username from email
 	 *
 	 * @param string $email Email address
diff --git a/inc/importers/JaneAppSalesImporter.php b/inc/importers/JaneAppSalesImporter.php
index 16fc8e8..92c5c9a 100644
--- a/inc/importers/JaneAppSalesImporter.php
+++ b/inc/importers/JaneAppSalesImporter.php
@@ -22,6 +22,7 @@
 	protected string $rewards_table;
 	protected ReferralManager $referral_manager;
 	protected array $import_stats = [];
+	protected array $skipped_details = [];
 
 	// CSV column mapping
 	protected array $column_map = [
@@ -65,9 +66,9 @@
 	 *
 	 * @param string $file_path Path to the CSV file
 	 * @param array $options Import options
-	 * @return array Import results with stats and errors
+	 * @return array|WP_Error Import results with stats and errors
 	 */
-	public function importFromCSV(string $file_path, array $options = []): array
+	public function importFromCSV(string $file_path, array $options = []): array|WP_Error
 	{
 		// Initialize stats
 		$this->import_stats = [
@@ -102,6 +103,9 @@
 			return new WP_Error('invalid_csv', 'CSV file is empty or invalid');
 		}
 
+		// Trim whitespace from headers to handle inconsistent CSV formats
+		$headers = array_map('trim', $headers);
+
 		// Map column indices
 		$column_indices = $this->mapColumnIndices($headers);
 		if (is_wp_error($column_indices)) {
@@ -109,12 +113,17 @@
 			return $column_indices;
 		}
 
+		// Get Patient column index for error reporting
+		$patient_name_index = array_search('Patient', $headers);
+
 		// Start transaction for data integrity
 		$this->wpdb->query('START TRANSACTION');
 
 		try {
 			// Process each row
+			$row_number = 1; // Header is row 1, data starts at row 2
 			while (($row = fgetcsv($handle)) !== false) {
+				$row_number++;
 				$this->import_stats['total_rows']++;
 
 				$result = $this->processSalesRow($row, $column_indices, [
@@ -122,10 +131,15 @@
 				]);
 
 				if (is_wp_error($result)) {
-					$this->import_stats['errors'][] = [
-						'row' => $this->import_stats['total_rows'],
-						'error' => $result->get_error_message()
+					$error_data = [
+						'row' => $row_number,
+						'patient_guid' => trim($row[$column_indices['patient_guid']] ?? 'Unknown'),
+						'patient_name' => trim($row[$patient_name_index] ?? 'Unknown'),
+						'item' => trim($row[$column_indices['item']] ?? 'Unknown'),
+						'reason' => $result->get_error_message()
 					];
+
+					$this->import_stats['errors'][] = $error_data;
 					$this->import_stats['skipped']++;
 				} else {
 					$this->import_stats['processed']++;
diff --git a/inc/integrations/Cloudflare.php b/inc/integrations/Cloudflare.php
index 6428424..4a44686 100644
--- a/inc/integrations/Cloudflare.php
+++ b/inc/integrations/Cloudflare.php
@@ -101,10 +101,8 @@
 
 		$result = json_decode(wp_remote_retrieve_body($response), true);
 
-		error_log('Cloudflare Result: '.print_r($result, true));
-
 		// If we get a response with success field (even if false), connection works
-		return array_key_exists('success', $result) && $result['success'];
+		return array_key_exists('success', $result);
 	}
 
 	/**
diff --git a/inc/integrations/GoogleMyBusiness.php b/inc/integrations/GoogleMyBusiness.php
index e42e0b3..6ac8ed5 100644
--- a/inc/integrations/GoogleMyBusiness.php
+++ b/inc/integrations/GoogleMyBusiness.php
@@ -3022,4 +3022,6 @@
 			]
 		];
 	}
+
+
 }
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 6e0d674..71392bb 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -62,7 +62,7 @@
 	 */
 	protected array $credentials = [];      // Service credentials (API keys, tokens, etc.)
 	protected ?int $userID = null;          // User context for user-specific integrations
-
+	private bool $token_refresh_attempted = false;  // Circuit breaker for token refresh
 
 	protected array $fields = []; 		// The fields to generate that become credentials
 	protected array $advanced = []; 	// The fields that are optional settings
@@ -258,10 +258,6 @@
 		if (!empty($this->credentials['expires_at'])) {
 			$expires_at = intval($this->credentials['expires_at']);
 			if ($expires_at <= time()) {
-				// Token expired, try to refresh
-				if (!empty($this->credentials['refresh_token'])) {
-					return $this->refreshOAuthToken();
-				}
 				return false;
 			}
 		}
@@ -1395,17 +1391,29 @@
 			if ($this->isOAuthService && $this->hasOAuthCredentials()) {
 				// Check if token is expired first
 				if (!$this->isOAuthValid()) {
-					$this->logDebug('OAuth token expired, attempting refresh');
-					if (!$this->refreshOAuthToken()) {
-						$this->logError('Failed to refresh expired OAuth token');
+					// Only attempt refresh once per request
+					if (!$this->token_refresh_attempted) {
+						$this->token_refresh_attempted = true;
+						$this->logDebug('OAuth token expired, attempting refresh');
+
+						if (!$this->refreshOAuthToken()) {
+							$this->logError('Failed to refresh expired OAuth token - stopping execution');
+							// Token refresh failed - DO NOT continue making API requests
+							return;
+						}
+					} else {
+						// Already attempted refresh in this request
+						$this->logDebug('Token refresh already attempted, skipping');
+						return;
 					}
 				}
 				// Check if we should proactively refresh (before expiry)
-				elseif ($this->shouldRefreshToken()) {
+				elseif ($this->shouldRefreshToken() && !$this->token_refresh_attempted) {
+					$this->token_refresh_attempted = true;
 					$this->logDebug('OAuth token should be refreshed proactively');
 					if (!$this->refreshOAuthToken()) {
 						$this->logError('Failed to proactively refresh OAuth token');
-						// Not critical - token is still valid
+						// Not critical - token is still valid, so continue
 					}
 				}
 			}
@@ -1429,6 +1437,7 @@
 		// Switch context
 		$this->userID = $user_id;
 		$this->credentials = [];
+		$this->resetTokenRefreshFlag();  // ADD THIS LINE
 
 		$this->ensureInitialized();
 	}
@@ -1935,7 +1944,7 @@
 		$response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data);
 
 		if (is_wp_error($response)) {
-			$this->logError('Failed to refresh Square token', [
+			$this->logError('Failed to refresh OAuth token for '.$this->service_name, [
 				'error' => $response->get_error_message()
 			]);
 			return false;
@@ -1945,7 +1954,7 @@
 			$this->credentials['access_token'] = $response['access_token'];
 			$this->credentials['expires_at'] = time() + ($response['expires_in'] ?? 2592000); // 30 days
 
-			// Note: Square returns the SAME refresh token
+			// Note: Some services return the SAME refresh token
 			if (isset($response['refresh_token'])) {
 				$this->credentials['refresh_token'] = $response['refresh_token'];
 			}
@@ -3529,4 +3538,12 @@
 			throw new Exception('Failed to save JPEG image');
 		}
 	}
+	/**
+	 * Reset token refresh attempt flag
+	 * Called automatically when switching users
+	 */
+	protected function resetTokenRefreshFlag(): void
+	{
+		$this->token_refresh_attempted = false;
+	}
 }
diff --git a/inc/managers/AdminPages.php b/inc/managers/AdminPages.php
index 5145e51..0ae75d4 100644
--- a/inc/managers/AdminPages.php
+++ b/inc/managers/AdminPages.php
@@ -34,7 +34,7 @@
             'icon' => jvbCSSIcon('settings'),
             'position' => 0
         ];
-		$this->subpages = apply_filters('jvbAdminSubpages', []);
+		$this->subpages = get_option(BASE.'adminSubpage', []);
 //        delete_option(BASE.'admin_actions');
 //        delete_option(BASE.'admin_subpages');
 //        $this->getSubpages();
@@ -48,6 +48,9 @@
 		add_filter(BASE.'admin_action_filter', [$this, 'handleCacheActions'], 10, 3);
 
 		add_action('rest_api_init', [$this, 'registerRestRoutes']);
+		// Handle form submissions
+		add_action('admin_init', [$this, 'handleAdminPageSubmission']);
+		add_action('admin_notices', [$this, 'displayAdminNotices']);
     }
 
 	/**
@@ -1113,4 +1116,70 @@
 		</script>
 		<?php
 	}
+
+	public static function addSubpage(string $key, array $value):void
+	{
+		$option = get_option(BASE.'adminSubpage', []);
+		if (empty($option) || !array_key_exists($key, $option)) {
+			$option[$key] = $value;
+			update_option(BASE.'adminSubpage', $option);
+		}
+	}
+
+	/**
+	 * Handle admin page form submissions
+	 * Fires after WordPress admin_init
+	 */
+	public function handleAdminPageSubmission(): void
+	{
+		// Only process on our admin pages
+		if (!isset($_GET['page']) || strpos($_GET['page'], BASE) !== 0) {
+			return;
+		}
+
+		// Check for form submission
+		if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['submit'])) {
+			return;
+		}
+
+		// Verify nonce
+		$nonce_field = BASE . 'admin_page_nonce';
+		if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], $nonce_field)) {
+			add_action('admin_notices', function() {
+				echo '<div class="notice notice-error"><p>Security check failed. Please try again.</p></div>';
+			});
+			return;
+		}
+
+		$page_slug = sanitize_text_field($_GET['page']);
+
+		// Allow other classes to handle their page submissions
+		$result = apply_filters('jvb_admin_page_submission', null, $page_slug, $_POST);
+
+		// Store result in transient for after redirect
+		if (is_array($result)) {
+			set_transient(BASE . 'admin_notice_' . get_current_user_id(), $result, 30);
+		}
+
+		// Redirect to prevent form resubmission (POST-Redirect-GET pattern)
+		wp_safe_redirect(add_query_arg(['page' => $page_slug], admin_url('admin.php')));
+		exit;
+	}
+
+	/**
+	 * Display admin notices from form submissions
+	 */
+	public function displayAdminNotices(): void
+	{
+		$notice = get_transient(BASE . 'admin_notice_' . get_current_user_id());
+
+		if ($notice && is_array($notice)) {
+			delete_transient(BASE . 'admin_notice_' . get_current_user_id());
+
+			$type = $notice['success'] ? 'success' : 'error';
+			$message = $notice['message'] ?? 'Settings saved.';
+
+			echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
+		}
+	}
 }
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index a7e6790..69e485e 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -175,10 +175,10 @@
 			if ($page === '' || $page === 'dash') {
 				return;
 			}
-
+			$page = $this->getCurrentPageTitle();
 			// Check if page exists in allowed pages
 			$allowedPages = $this->getUserAllowedPages();
-			if (!array_key_exists($page, $allowedPages)) {
+			if (!in_array($page, $allowedPages)) {
 				error_log("User not allowed to access page: {$page}");
 				$this->redirectToDashboard();
 				return;
@@ -197,14 +197,13 @@
 			if ($page === 'dash') {
 				continue;
 			}
+			$slug = $this->getSlug($slug, $page);
 			$existing = get_page_by_path($slug, OBJECT, BASE.'dash');
 			if ($existing) {
 				continue;
 			}
             $title = $page;
 
-			$slug = $this->getSlug($slug, $page);
-
             $ID = wp_insert_post(array(
                 'post_title'    => $title,
                 'post_name'     => $slug,
@@ -484,34 +483,7 @@
             return;
         }
 
-		wp_enqueue_script('jvb-loading');
 		wp_enqueue_script('jvb-form');
-
-		// Consolidate all dashboard settings
-		wp_localize_script('jvb-loading', 'dashboardSettings', array(
-			'loadingMessages' => array(
-				'default' => 'Loading...',
-				'error' => 'Failed to load page'
-			),
-			'strings' => array(
-				'deleteConfirm' => 'Are you sure you want to delete this item?',
-				'bulkDeleteConfirm' => 'Are you sure you want to delete these items?',
-				'deleteSuccess' => 'Item(s) deleted successfully',
-				'deleteError' => 'Error deleting item(s)',
-				'saveSuccess' => 'Changes saved successfully',
-				'saveError' => 'Error saving changes',
-				'loadError' => 'Error loading content'
-			),
-			'currentUser' => array(
-				'id' => $this->user->ID,
-				'name' => $this->user->display_name,
-				'role' => array_values($this->user->roles)[0] ?? '',
-				'type' => str_replace(BASE, '', array_values($this->user->roles)[0]),
-				'city' => '', // Add if needed,
-				'artistID'  => $this->userLink,
-			)
-		));
-
 		wp_enqueue_script('jvb-selector');
 		wp_enqueue_script('jvb-uploader');
 		wp_enqueue_script('jvb-content');
diff --git a/inc/managers/EmailManager.php b/inc/managers/EmailManager.php
index 7f2315e..e8084fd 100644
--- a/inc/managers/EmailManager.php
+++ b/inc/managers/EmailManager.php
@@ -338,12 +338,12 @@
 		$message = apply_filters('jvbNewUserEmail', $message, $user);
 
 		$message .= $this->signature;
-		$wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, JVB_EMAIL['subjects']['newUser']);
+		$wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, JVB_EMAIL['types']['newUser']['subject']?:'New User');
 
         // Change the subject line
 		$prefix = JVB_EMAIL['types']['newUser']['showPrefix']??true;
 		$prefix = ($prefix) ? $this->prefix : '';
-        $wp_new_user_notification_email['subject'] = $prefix.JVB_EMAIL['types']['newUser']['subject'];
+        $wp_new_user_notification_email['subject'] = $prefix.JVB_EMAIL['types']['newUser']['subject']?:'New User';
 
         return $wp_new_user_notification_email;
     }
@@ -413,7 +413,7 @@
     {
 		$prefix = JVB_EMAIL['types']['resetPass']['showPrefix']??true;
 		$prefix = ($prefix) ? $this->prefix : '';
-		return $prefix.JVB_EMAIL['types']['resetPass']['subject'];
+		return $prefix.JVB_EMAIL['types']['resetPass']['subject']?:'Password Reset';
 
 	}
 
@@ -445,7 +445,7 @@
         $email_change_email['message'] = $this->getEmailTemplate($content, 'Email Address Changed');
 		$prefix = JVB_EMAIL['types']['emailChange']['showPrefix']??true;
 		$prefix = ($prefix) ? $this->prefix : '';
-		$email_change_email['subject'] = $prefix.JVB_EMAIL['types']['emailChange']['subject'];
+		$email_change_email['subject'] = $prefix.JVB_EMAIL['types']['emailChange']['subject']?:'Email Address Changed';
 
 
         return $email_change_email;
@@ -507,7 +507,7 @@
         $pass_change_email['message'] = $this->getEmailTemplate($content, 'Password Changed');
 		$prefix = JVB_EMAIL['types']['passwordChange']['showPrefix']??true;
 		$prefix = ($prefix) ? $this->prefix : '';
-		$pass_change_email['subject'] = $prefix.JVB_EMAIL['types']['passwordChange']['subject'];
+		$pass_change_email['subject'] = $prefix.JVB_EMAIL['types']['passwordChange']['subject']?:'Password Changed';
 
         return $pass_change_email;
     }
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 3ac4511..3f000e4 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -44,8 +44,8 @@
 		global $wpdb;
 		$this->wpdb = $wpdb;
 		$this->cache = CacheManager::for('referrals', WEEK_IN_SECONDS);
-		$this->referrals_table = BASE . 'referrals';
-		$this->rewards_table = BASE . 'referral_rewards';
+		$this->referrals_table = $wpdb->prefix . BASE . 'referrals';
+		$this->rewards_table = $wpdb->prefix . BASE . 'referral_rewards';
 		$this->magic_link = new MagicLinkManager();
 
 		$this->referralPage = $this->getReferralPageId();
@@ -73,9 +73,6 @@
 		// Schedule cron jobs for reports
 		$this->registerCronJobs();
 
-		// Register admin subpage
-//		add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1);
-
 		// Add admin bar label for referral page
 		add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999);
 
@@ -87,6 +84,8 @@
 
 		// Handle settings save
 		add_action('admin_init', [$this, 'registerSettings']);
+		// Handle admin page form submission
+		add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3);
 	}
 
 	public function addLoginInputs(string $action):void
@@ -168,7 +167,7 @@
 	 */
 	public function getUserReferralCode(int $user_id, ?string $custom_code = null)
 	{
-		$user = get_user_by('ID', $user_id);
+		$user = get_userdata($user_id);
 		if (!$user) {
 			return new WP_Error('invalid_user', 'User not found');
 		}
@@ -982,85 +981,116 @@
 		return $actions;
 	}
 
+	/**
+	 * Display referral sidebar for non-logged-in users
+	 */
 	function getUnloggedInReferral(): string
 	{
 		ob_start();
 		JVB()->connect('cloudflare')->renderTurnstile();
 		$turnstile = ob_get_clean();
+
 		$meta = new MetaForm();
-		$codeForm = '<form id="referral-code-form">
-					'.jvbFormStatus().$meta->return('referral_name', null, [
-						'required'	=> true,
-						'type'		=> 'text',
-						'label'		=> 'Your Name',
-						'placeholder'=> 'Mister Meeseeks',
-						'autocomplete'=>'name'
-					]).
-					$meta->return('referral_email', null, [
-						'required'	=> true,
-						'type'		=> 'email',
-						'label'		=> 'Your Email',
-						'placeholder'=> 'look@me.com',
-						'autocomplete'=> 'email'
-					]).
-					$meta->return('referral_code', null, [
-						'required'	=> true,
-						'type'		=> 'text',
-						'label'		=> 'Referral Code',
-						'pattern'	=> '[A-Za-z0-9]+',
-						'maxLength'	=> 20,
-						'autocomplete'=>'off'
-					]).'
-					<button type="submit">
-						Get Started
-					</button>
+		$reward_text = $this->getRewardText(true);
 
-					<p class="helper-text">
-						We\'ll send you a link to complete your registration.
-					</p>
-					'.$turnstile.'
-				</form><div class="success-content" hidden>
-					<h3>Check Your Email!</h3>
-					<p>We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!</p>
-					<p class="hint">Can\'t find it? Check your spam folder.</p>
-				</div>';
+		// Pre-fill code if from referral link
+		$prefill_code = $_GET['ref'] ?? '';
+		$referrer_name = '';
+		if ($prefill_code) {
+			$referrer = $this->getUserByReferralCode($prefill_code);
+			$referrer_name = $referrer ? strtok($referrer->display_name, ' ') : '';
+		}
 
-		$loginForm = '<form id ="login-form">
-		'.jvbFormStatus().$meta->return('login_email', null, [
+		$codeForm = '<div class="referral-reward-banner">
+		'.jvbIcon('confetti').'
+		<h4>Get ' . esc_html($reward_text) . '!</h4>
+		' . ($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, [
+				'required'	=> true,
+				'type'		=> 'text',
+				'label'		=> 'Your Name',
+				'placeholder'=> 'Mister Meeseeks',
+				'autocomplete'=>'name'
+			]).
+			$meta->return('referral_email', null, [
+				'required'	=> true,
+				'type'		=> 'email',
+				'label'		=> 'Your Email',
+				'placeholder'=> 'look@me.com',
+				'autocomplete'=> 'email'
+			]).
+			$meta->return('referral_code', $prefill_code, [
+				'required'	=> true,
+				'type'		=> 'text',
+				'label'		=> 'Referral Code',
+				'pattern'	=> '[A-Za-z0-9]+',
+				'maxLength'	=> 20,
+				'autocomplete'=>'off',
+				'data-referrer' => $referrer_name
+			]).'
+				<button type="button" class="button-secondary check-code-btn">
+					'.jvbIcon('check-circle', ['size' => 16]).' Verify Code
+				</button>
+				<div class="code-status" hidden></div>
+				<button type="submit">
+					Get Started
+				</button>
+
+				<p class="helper-text">
+					We\'ll send you a link to complete your registration.
+				</p>
+				'.$turnstile.'
+			</form>
+			<div class="success-content" hidden>
+				<h3>Check Your Email!</h3>
+				<p>We\'ve sent you a magic link to complete your registration. Click the link to activate your account and claim your reward!</p>
+				<p class="hint">Can\'t find it? Check your spam folder.</p>
+			</div>';
+
+		$loginForm = '<form id="login-form">
+	'.jvbFormStatus().$meta->return('login_email', null, [
 				'required'	=> true,
 				'type'		=> 'email',
 				'label'		=> 'Your Email',
 				'autocomplete'=>'email'
 			]).'
-		'.$turnstile.'
-		<button type="submit">Login With Magic Link</button>
+	'.$turnstile.'
+	<button type="submit">Login With Magic Link</button>
 </form>
-	<div class="success-content" hidden>
-		<h3>Check Your Email!</h3>
-		<p>We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.</p>
-		<p class="hint">Can\'t find it? Check your spam folder.</p>
-	</div>';
+<div class="success-content" hidden>
+	<h3>Check Your Email!</h3>
+	<p>We\'ve sent you a magic link to log in - no password required! Click the link in your email to log in.</p>
+	<p class="hint">Can\'t find it? Check your spam folder.</p>
+</div>';
 
+		$footer = '<div class="referral-footer">
+		<a href="' . wp_login_url() . '" class="text-link">Prefer to use a password?</a>
+	</div>';
 		$tabs = [
 			'enterCode' => [
 				'title'	=> 'Have a Code?',
 				'description'	=> [
-					'Enter the code given to you to get 20% off your first treatment!'
+					'Enter your referral code to get started'
 				],
 				'content'	=> $codeForm
 			],
 			'login'	=> [
 				'title'		=> 'Login',
 				'description'	=> [
-					'Login to see your rewards'
+					'Already have an account? Log in to see your rewards'
 				],
-				'content'	=> $loginForm
+				'content'	=> $loginForm.$footer
 			]
 		];
 
+
+
 		return jvbRenderTabs($tabs, true);
 	}
 
+
 	protected function getReferralSuccessMessage(string $code): string
 	{
 		$referrer = $this->getUserByReferralCode($code);
@@ -1106,86 +1136,73 @@
 		return ob_get_clean();
 	}
 
-	public function getLoggedInReferral(int $user_id):string
+	/**
+	 * Display referral sidebar for logged-in users
+	 */
+	public function getLoggedInReferral(int $user_id): string
 	{
-		// Logged-in user widget
-		$referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
-
-		// Generate code if user doesn't have one
-		if (empty($referral_code)) {
-			$referral_code = $this->getUserReferralCode($user_id);
-			if (is_wp_error($referral_code)) {
-				return '';
-			}
+		$referral_code = $this->getUserReferralCode($user_id);
+		if (is_wp_error($referral_code)) {
+			return '';
 		}
 
 		$share_url = $this->getShareURL($referral_code);
-
 		ob_start();
 		?>
-			<header>
-				<h3>Share the ♡</h3>
-				<p>Invite your friends.</p>
-				<p>Earn rewards when they book!</p>
-			</header>
+		<header>
+			<h3>Share the ♡</h3>
+			<p>Invite friends. Earn rewards.</p>
+		</header>
 
-		<div class="row even share-buttons">
-			<a href="mailto:?subject=<?php echo urlencode('Check out ' . get_bloginfo('name')); ?>&body=<?php echo urlencode('I thought you might be interested: ' . $share_url); ?>"
-			   class="share-btn email-share">
-				<?php echo jvbIcon('envelope', ['size' => 20]); ?>
-				Email
-			</a>
-			<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>"
-			   target="_blank"
-			   rel="noopener noreferrer"
-			   class="share-btn facebook-share">
-				<?php echo jvbIcon('facebook-logo', ['size' => 20]); ?>
-				Facebook
-			</a>
-			<a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode('Check this out!'); ?>"
-			   target="_blank"
-			   rel="noopener noreferrer"
-			   class="share-btn twitter-share">
-				<?php echo jvbIcon('twitter-logo', ['size' => 20]); ?>
-				Twitter
-			</a>
-		</div>
+		<?php $this->getShareButtons($user_id); ?>
 
+		<div class="copy-section">
 			<h4>Your Referral Link</h4>
-			<div class="row btw">
-				<code id="your-referral-link"><?= esc_url($share_url)?></code>
-				<button type="button" class="copy" data-target="your-referral-link">
-					Copy Link
+			<div class="copy-group">
+				<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>
 
-
 			<h4>Your Code</h4>
-			<div class="row btw">
-				<code id="your-referral-code"><?=esc_html($referral_code)?></code>
-				<button type="button" class="copy" data-target="your-referral-code">
-					Copy Code
+			<div class="copy-group">
+				<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>
+		</div>
 
-			<div class="row btw referral-stats">
-				<div class="stat-item">
-					<span class="stat-value" data-stat="total">-</span>
-					<span class="stat-label">Total Referrals</span>
-				</div>
-				<div class="stat-item">
-					<span class="stat-value" data-stat="treated">-</span>
-					<span class="stat-label">Successful</span>
-				</div>
-				<div class="stat-item">
-					<span class="stat-value" data-stat="pending">-</span>
-					<span class="stat-label">Pending</span>
-				</div>
-				<div class="stat-item">
-					<span class="stat-value" data-stat="rewards">$0.00</span>
-					<span class="stat-label">Available Rewards</span>
-				</div>
+		<div class="recent-referrals-section">
+			<h4>Recent Referrals</h4>
+			<div class="recent-referrals-list" data-user-id="<?= $user_id ?>">
+				<div class="loading">Loading...</div>
 			</div>
+		</div>
+
+		<div class="stats-summary">
+			<div class="stat-row">
+				<span class="stat-label">Total Referrals</span>
+				<span class="stat-value" data-stat="total">-</span>
+			</div>
+			<div class="stat-row">
+				<span class="stat-label">Successful</span>
+				<span class="stat-value" data-stat="treated">-</span>
+			</div>
+			<div class="stat-row">
+				<span class="stat-label">Pending</span>
+				<span class="stat-value" data-stat="pending">-</span>
+			</div>
+			<div class="stat-row highlight">
+				<span class="stat-label">Available Rewards</span>
+				<span class="stat-value" data-stat="rewards">$0.00</span>
+			</div>
+		</div>
+
+		<a href="<?= get_home_url(null, '/dash/referrals')?>" class="view-dashboard-btn">
+			Dashboard <?= jvbIcon('arrow-right', ['size' => 16]); ?>
+		</a>
 
 		<?php
 		return ob_get_clean();
@@ -1563,18 +1580,26 @@
 	 * @param array $subpages
 	 * @return array
 	 */
-	public function addSubpage(array $subpages): array
+	public static function addSubpage():void
 	{
-		$subpages[] = [
-			'page_title' => 'Referral Settings',
+		$subpage = [
+			'page_title' => 'Referral System',
 			'menu_title' => 'Referrals',
 			'capability' => 'manage_options',
-			'menu_slug'  => 'jvb-referrals',
-			'callback'   => [$this, 'renderAdminPage'],
-			'icon'       => 'users',
+			'menu_slug' => BASE . 'referral-admin',
+			'callback' => [self::class, 'renderAdminPageStatic']
 		];
+		AdminPages::addSubPage(BASE.'referral-admin', $subpage);
+	}
 
-		return $subpages;
+	/**
+	 * Static wrapper for renderAdminPage
+	 * Called by WordPress when admin page is rendered
+	 */
+	public static function renderAdminPageStatic(): void
+	{
+		// Get the properly initialized instance from JVB singleton
+		JVB()->referrals()->renderAdminPage();
 	}
 
 	/**
@@ -1631,25 +1656,389 @@
 	 */
 	public function renderAdminPage(): void
 	{
-		// Handle form submission
-		if (isset($_POST['submit']) && check_admin_referer(BASE . 'referral_settings_nonce')) {
-			update_option(BASE . 'referral_page_id', absint($_POST[BASE . 'referral_page_id'] ?? 0));
+		?>
+		<div class="wrap jvb-admin-wrap">
+			<h1>Referral System Management</h1>
 
-			$reward_settings = [
-				'referrer_reward_applies_to' => sanitize_text_field($_POST['referrer_reward_applies_to'] ?? 'per_user'),
-				'referrer_reward_amount' => floatval($_POST['referrer_reward_amount'] ?? 25.00),
-				'referrer_reward_type' => sanitize_text_field($_POST['referrer_reward_type'] ?? 'fixed'),
-				'referee_reward_type' => sanitize_text_field($_POST['referee_reward_type'] ?? 'percentage'),
-				'referee_reward_amount' => floatval($_POST['referee_reward_amount'] ?? 20),
-				'referee_reward_applies_to' => sanitize_text_field($_POST['referee_reward_applies_to'] ?? 'first_order'),
-			];
+			<!-- CSV Upload Section -->
+			<div class="card">
+				<h2>Import Data from Jane App</h2>
+				<p>Upload your exported CSV files from Jane App to sync client and sales data.</p>
 
-			update_option(BASE . 'referral_reward_settings', $this->sanitizeRewardSettings($reward_settings));
+				<div class="jvb-upload-section" style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
+					<!-- Client List Upload -->
+					<div class="jvb-upload-box">
+						<h3>Client List</h3>
+						<form id="client-upload-form" enctype="multipart/form-data">
+							<input type="file"
+								   name="client_file"
+								   id="client_file"
+								   accept=".csv"
+								   required />
+							<button type="submit" class="button button-primary" style="margin-top: 10px;">
+								Upload Clients
+							</button>
+						</form>
+						<div id="client-upload-status" style="margin-top: 10px;"></div>
+					</div>
 
-			echo '<div class="notice notice-success is-dismissible"><p>Settings saved successfully.</p></div>';
-		}
+					<!-- Sales Export Upload -->
+					<div class="jvb-upload-box">
+						<h3>Sales Export</h3>
+						<form id="sales-upload-form" enctype="multipart/form-data">
+							<input type="file"
+								   name="sales_file"
+								   id="sales_file"
+								   accept=".csv"
+								   required />
+							<button type="submit" class="button button-primary" style="margin-top: 10px;">
+								Upload Sales
+							</button>
+						</form>
+						<div id="sales-upload-status" style="margin-top: 10px;"></div>
+					</div>
+				</div>
+			</div>
 
-		echo $this->renderAdminHTML();
+			<!-- Referrals Table -->
+			<div class="card" style="margin-top: 20px;">
+				<h2>Referrals Management</h2>
+
+				<div class="jvb-table-controls" style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center;">
+					<label>
+						Filter by Status:
+						<select id="referral-status-filter">
+							<option value="">All Statuses</option>
+							<option value="pending">Pending</option>
+							<option value="consulted">Consulted</option>
+							<option value="treated">Treated</option>
+							<option value="cancelled">Cancelled</option>
+						</select>
+					</label>
+					<input type="text"
+						   id="referral-search"
+						   placeholder="Search by name or email..."
+						   style="min-width: 250px;" />
+					<button type="button" class="button" id="refresh-table">Refresh</button>
+				</div>
+
+				<div id="referrals-table-container">
+					<div class="jvb-loading">Loading referrals...</div>
+				</div>
+			</div>
+
+			<!-- Settings Section -->
+			<?= $this->renderAdminHTML() ?>
+		</div>
+
+		<style>
+			.jvb-upload-box {
+				padding: 20px;
+				background: #f9f9f9;
+				border: 1px solid #ddd;
+				border-radius: 4px;
+			}
+			.jvb-upload-box h3 {
+				margin-top: 0;
+			}
+			.referrals-table {
+				width: 100%;
+				border-collapse: collapse;
+			}
+			.referrals-table th,
+			.referrals-table td {
+				padding: 12px;
+				text-align: left;
+				border-bottom: 1px solid #ddd;
+			}
+			.referrals-table th {
+				background: #f5f5f5;
+				font-weight: 600;
+			}
+			.referrals-table tr:hover {
+				background: #f9f9f9;
+			}
+			.referral-status {
+				padding: 4px 8px;
+				border-radius: 3px;
+				font-size: 12px;
+				font-weight: 500;
+			}
+			.referral-status.pending {
+				background: #fff3cd;
+				color: #856404;
+			}
+			.referral-status.consulted {
+				background: #d1ecf1;
+				color: #0c5460;
+			}
+			.referral-status.treated {
+				background: #d4edda;
+				color: #155724;
+			}
+			.referral-actions {
+				display: flex;
+				gap: 5px;
+			}
+			.notice.notice-success,
+			.notice.notice-error {
+				margin: 10px 0;
+			}
+		</style>
+
+		<script>
+			jQuery(document).ready(function($) {
+				// Client upload
+				// Client upload
+				$('#client-upload-form').on('submit', function(e) {
+					e.preventDefault();
+					const formData = new FormData(this);
+					formData.append('file', $('#client_file')[0].files[0]);
+
+					$('#client-upload-status').html('<span class="spinner is-active"></span> Uploading...');
+
+					$.ajax({
+						url: '<?= rest_url('jvb/v1/referrals/upload-clients') ?>',
+						method: 'POST',
+						data: formData,
+						processData: false,
+						contentType: false,
+						beforeSend: function(xhr) {
+							xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
+						},
+						success: function(response) {
+							if (response.success) {
+								let message = '<div class="notice notice-success"><p>' + response.message;
+								message += '<br>Created: ' + (response.stats.created || 0);
+								message += ', Updated: ' + (response.stats.updated || 0);
+								message += ', Skipped: ' + (response.stats.skipped || 0) + '</p>';
+
+								// Show skipped details if any
+								if (response.stats.skipped_details && response.stats.skipped_details.length > 0) {
+									message += '<details style="margin-top: 10px;"><summary>View skipped records (' + response.stats.skipped_details.length + ')</summary>';
+									message += '<table class="widefat" style="margin-top: 10px;"><thead><tr>';
+									message += '<th>Line</th><th>Name</th><th>Email</th><th>GUID</th><th>Reason</th>';
+									message += '</tr></thead><tbody>';
+									response.stats.skipped_details.forEach(function(item) {
+										message += '<tr>';
+										message += '<td>' + (item.line || '-') + '</td>';
+										message += '<td>' + (item.name || '-') + '</td>';
+										message += '<td>' + (item.email || '-') + '</td>';
+										message += '<td>' + (item.guid || '-') + '</td>';
+										message += '<td>' + item.reason + '</td>';
+										message += '</tr>';
+									});
+									message += '</tbody></table></details>';
+								}
+
+								message += '</div>';
+
+								$('#client-upload-status').html(message);
+								$('#client-upload-form')[0].reset();
+								loadReferralsTable();
+							} else {
+								$('#client-upload-status').html(
+									'<div class="notice notice-error"><p>' + response.message + '</p></div>'
+								);
+							}
+						},
+						error: function(xhr) {
+							$('#client-upload-status').html(
+								'<div class="notice notice-error"><p>Upload failed</p></div>'
+							);
+						}
+					});
+				});
+
+				// Sales upload
+				$('#sales-upload-form').on('submit', function(e) {
+					e.preventDefault();
+					const formData = new FormData(this);
+					formData.append('file', $('#sales_file')[0].files[0]);
+
+					$('#sales-upload-status').html('<span class="spinner is-active"></span> Uploading...');
+
+					$.ajax({
+						url: '<?= rest_url('jvb/v1/referrals/upload-sales') ?>',
+						method: 'POST',
+						data: formData,
+						processData: false,
+						contentType: false,
+						beforeSend: function(xhr) {
+							xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
+						},
+						success: function(response) {
+							if (response.success) {
+								$('#sales-upload-status').html(
+									'<div class="notice notice-success"><p>' + response.message +
+									'<br>Consultations: ' + response.stats.consultations +
+									', Treatments: ' + response.stats.treatments +
+									', Skipped: ' + response.stats.skipped + '</p></div>'
+								);
+								$('#sales-upload-form')[0].reset();
+								loadReferralsTable();
+							} else {
+								$('#sales-upload-status').html(
+									'<div class="notice notice-error"><p>' + response.message + '</p></div>'
+								);
+							}
+						},
+						error: function(xhr) {
+							$('#sales-upload-status').html(
+								'<div class="notice notice-error"><p>Upload failed</p></div>'
+							);
+						}
+					});
+				});
+
+				// Load referrals table
+				function loadReferralsTable(page = 1) {
+					const status = $('#referral-status-filter').val();
+					const search = $('#referral-search').val();
+
+					$.ajax({
+						url: '<?= rest_url('jvb/v1/referrals/list') ?>',
+						method: 'GET',
+						data: {
+							page: page,
+							per_page: 20,
+							status: status,
+							search: search
+						},
+						beforeSend: function(xhr) {
+							xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
+							$('#referrals-table-container').html('<div class="jvb-loading">Loading...</div>');
+						},
+						success: function(response) {
+							if (response.success) {
+								renderReferralsTable(response);
+							}
+						}
+					});
+				}
+
+				// Render table
+				function renderReferralsTable(data) {
+					let html = '<table class="referrals-table widefat">';
+					html += '<thead><tr>';
+					html += '<th>Referrer</th>';
+					html += '<th>Referee</th>';
+					html += '<th>Email</th>';
+					html += '<th>Status</th>';
+					html += '<th>Referred Date</th>';
+					html += '<th>Total Referrals</th>';
+					html += '<th>Actions</th>';
+					html += '</tr></thead><tbody>';
+
+					if (data.referrals.length === 0) {
+						html += '<tr><td colspan="7" style="text-align: center;">No referrals found</td></tr>';
+					} else {
+						data.referrals.forEach(function(ref) {
+							html += '<tr>';
+							html += '<td>' + (ref.referrer_name || 'Unknown') + '</td>';
+							html += '<td>' + (ref.referee_display_name || ref.referee_name) + '</td>';
+							html += '<td>' + (ref.referee_display_email || ref.referee_email) + '</td>';
+							html += '<td><span class="referral-status ' + ref.status + '">' + ref.status + '</span></td>';
+							html += '<td>' + new Date(ref.referred_at).toLocaleDateString() + '</td>';
+							html += '<td>' + (ref.referrer_total_referrals || 0) + '</td>';
+							html += '<td class="referral-actions">';
+
+							if (ref.status === 'pending') {
+								html += '<button class="button button-small mark-consulted" data-id="' + ref.id + '">Mark Consulted</button>';
+							}
+							if (ref.status !== 'treated') {
+								html += '<button class="button button-small mark-treated" data-id="' + ref.id + '">Mark Treated</button>';
+							}
+
+							html += '</td>';
+							html += '</tr>';
+						});
+					}
+
+					html += '</tbody></table>';
+
+					// Add pagination
+					if (data.total_pages > 1) {
+						html += '<div class="tablenav"><div class="tablenav-pages">';
+						for (let i = 1; i <= data.total_pages; i++) {
+							const active = i === data.page ? 'button-primary' : 'button';
+							html += '<button class="button ' + active + ' page-link" data-page="' + i + '">' + i + '</button> ';
+						}
+						html += '</div></div>';
+					}
+
+					$('#referrals-table-container').html(html);
+				}
+
+				// Event handlers for actions
+				$(document).on('click', '.mark-consulted', function() {
+					const id = $(this).data('id');
+					if (!confirm('Mark this referral as consulted? This will create the consultation reward.')) return;
+
+					$.ajax({
+						url: '<?= rest_url('jvb/v1/referrals/mark-consulted') ?>',
+						method: 'POST',
+						data: JSON.stringify({ referral_id: id }),
+						contentType: 'application/json',
+						beforeSend: function(xhr) {
+							xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
+						},
+						success: function(response) {
+							if (response.success) {
+								alert(response.message);
+								loadReferralsTable();
+							} else {
+								alert('Error: ' + response.message);
+							}
+						}
+					});
+				});
+
+				$(document).on('click', '.mark-treated', function() {
+					const id = $(this).data('id');
+					if (!confirm('Mark this referral as treated? This will create rewards for both parties.')) return;
+
+					$.ajax({
+						url: '<?= rest_url('jvb/v1/referrals/mark-treated') ?>',
+						method: 'POST',
+						data: JSON.stringify({ referral_id: id }),
+						contentType: 'application/json',
+						beforeSend: function(xhr) {
+							xhr.setRequestHeader('X-WP-Nonce', '<?= wp_create_nonce('wp_rest') ?>');
+						},
+						success: function(response) {
+							if (response.success) {
+								alert(response.message);
+								loadReferralsTable();
+							} else {
+								alert('Error: ' + response.message);
+							}
+						}
+					});
+				});
+
+				$(document).on('click', '.page-link', function() {
+					loadReferralsTable($(this).data('page'));
+				});
+
+				$('#referral-status-filter, #refresh-table').on('change click', function() {
+					loadReferralsTable();
+				});
+
+				// Search with debounce
+				let searchTimeout;
+				$('#referral-search').on('keyup', function() {
+					clearTimeout(searchTimeout);
+					searchTimeout = setTimeout(function() {
+						loadReferralsTable();
+					}, 500);
+				});
+
+				// Initial load
+				loadReferralsTable();
+			});
+		</script>
+		<?php
 	}
 
 	protected function renderAdminHTML():string
@@ -1660,7 +2049,7 @@
 			<h1>Referral Settings</h1>
 
 			<form method="post" action="">
-				<?php wp_nonce_field(BASE . 'referral_settings_nonce'); ?>
+				<?php wp_nonce_field(BASE . 'admin_page_nonce'); ?>
 
 				<div class="card">
 					<h2>Referral Page</h2>
@@ -1673,6 +2062,9 @@
 							</th>
 							<td>
 								<?php
+								if (!$this->referralPage) {
+									$this->referralPage = $this->getReferralPageId();
+								}
 								wp_dropdown_pages([
 									'name' => BASE . 'referral_page_id',
 									'id' => BASE . 'referral_page_id',
@@ -1772,6 +2164,27 @@
 								</select>
 							</td>
 						</tr>
+						<tr>
+							<th scope="row">
+								<label for="<?= BASE ?>client_import_role">Client Import Role</label>
+							</th>
+							<td>
+								<?php
+								$selected_role = get_option(BASE . 'client_import_role', '');
+								$roles = wp_roles()->get_names();
+								?>
+								<select name="<?= BASE ?>client_import_role" id="<?= BASE ?>client_import_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) ?>
+										</option>
+									<?php endforeach; ?>
+								</select>
+								<p class="description">
+									Role assigned to users imported from Jane App client list.
+								</p>
+							</td>
+						</tr>
 					</table>
 				</div>
 
@@ -1789,38 +2202,62 @@
 	/**
 	 * Render referral statistics
 	 */
-	protected function renderReferralStats(bool $wrapCard = false):string
+	protected function renderReferralStats(bool $wrapCard = false): string
 	{
-		ob_start();
-		global $wpdb;
+		global $wpdb; // Use fresh global instead of stored reference
 
-		$total_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table}");
-		$pending_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'pending'");
-		$treated_referrals = $wpdb->get_var("SELECT COUNT(*) FROM {$this->referrals_table} WHERE status = 'treated'");
+		ob_start();
+
+		// Get fresh table name references
+		$referrals_table = $wpdb->prefix . BASE . 'referrals';
+
+		// Use proper WordPress prepare even for COUNT
+		$total_referrals = $wpdb->get_var(
+			$wpdb->prepare(
+				"SELECT COUNT(*) FROM `{$referrals_table}` WHERE 1=%d",
+				1
+			)
+		);
+
+		$pending_referrals = $wpdb->get_var(
+			$wpdb->prepare(
+				"SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s",
+				'pending'
+			)
+		);
+
+		$treated_referrals = $wpdb->get_var(
+			$wpdb->prepare(
+				"SELECT COUNT(*) FROM `{$referrals_table}` WHERE status = %s",
+				'treated'
+			)
+		);
 
 		?>
 		<table class="widefat">
 			<tr>
 				<th>Total Referrals</th>
-				<td><?= esc_html($total_referrals) ?></td>
+				<td><?= esc_html($total_referrals ?? 0) ?></td>
 			</tr>
 			<tr>
 				<th>Pending</th>
-				<td><?= esc_html($pending_referrals) ?></td>
+				<td><?= esc_html($pending_referrals ?? 0) ?></td>
 			</tr>
 			<tr>
 				<th>Treated</th>
-				<td><?= esc_html($treated_referrals) ?></td>
+				<td><?= esc_html($treated_referrals ?? 0) ?></td>
 			</tr>
 		</table>
 		<?php
 		$table = ob_get_clean();
+
 		if ($wrapCard) {
 			$table = '<div class="card">
-				<h2>Referral Statistics</h2>
-				'.$table.'
-			</div>';
+            <h2>Referral Statistics</h2>
+            ' . $table . '
+        </div>';
 		}
+
 		return $table;
 	}
 
@@ -1836,6 +2273,9 @@
 		}
 
 		if (!$this->referralPage) {
+			$this->referralPage = $this->getReferralPageId();
+		}
+		if (!$this->referralPage) {
 			return;
 		}
 
@@ -1875,7 +2315,9 @@
 		if ('post.php' !== $pagenow || !$post) {
 			return;
 		}
-
+		if (!$this->referralPage) {
+			$this->referralPage = $this->getReferralPageId();
+		}
 		if ($post->ID === $this->referralPage) {
 			echo '<div class="notice notice-info">';
 			echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvbase') . '</p>';
@@ -1883,18 +2325,253 @@
 		}
 	}
 
-	public function renderDashPage(string $content, string $page):string
+	public function renderDashPage(string $content, string $page): string
 	{
-		if ($page !== 'referrals') {
+		if ($page !== 'Referrals') {
 			return $content;
 		}
-		$out = '';
-		if (current_user_can('manage_options')) {
-			$out .= $this->renderAdminHTML();
-		} else {
-			$out .= $this->renderReferralStats(true);
+
+		// Regular users get their referral dashboard
+		$user_id = get_current_user_id();
+		$referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
+
+		if (!$referral_code) {
+			$referral_code = $this->getUserReferralCode($user_id);
 		}
-		return ($out === '') ? $content : '<form id="referrals" class="col" data-save="referrals">'.$out.'</form>';
+
+		$stats = $this->getUserStats($user_id);
+		$referrals = $this->getUserReferrals($user_id, ['limit' => 20]);
+
+		ob_start();
+		?>
+		<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>
+
+			<?php $this->getShareButtons($user_id); ?>
+
+			<!-- 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>
+			</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',
+						]
+					]
+				];
+				$meta->render('invite', [], $field);
+				?>
+			</form>
+
+			<!-- 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>
+			</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>
+		</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
+		return ob_get_clean();
+	}
+
+	/**
+	 * Handle admin page form submission
+	 *
+	 * @param mixed $result Previous result
+	 * @param string $page_slug Current page slug
+	 * @param array $post_data POST data
+	 * @return array|null Result array or null if not our page
+	 */
+	public function handleAdminSubmission($result, string $page_slug, array $post_data): ?array
+	{
+		// Only handle our page
+		if ($page_slug !== BASE . 'referral-admin') {
+			return $result;
+		}
+
+		try {
+			// Save referral page
+			$page_id = isset($post_data[BASE . 'referral_page_id']) ? absint($post_data[BASE . 'referral_page_id']) : 0;
+			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);
+
+			// Save reward settings
+			$settings = [
+				'referrer_reward_type' => sanitize_text_field($post_data['referrer_reward_type'] ?? 'fixed'),
+				'referrer_reward_amount' => floatval($post_data['referrer_reward_amount'] ?? 25.00),
+				'referrer_reward_applies_to' => sanitize_text_field($post_data['referrer_reward_applies_to'] ?? 'per_user'),
+				'referee_reward_type' => sanitize_text_field($post_data['referee_reward_type'] ?? 'percentage'),
+				'referee_reward_amount' => floatval($post_data['referee_reward_amount'] ?? 20),
+				'referee_reward_applies_to' => sanitize_text_field($post_data['referee_reward_applies_to'] ?? 'first_order')
+			];
+
+			update_option(BASE . 'referral_settings', $settings);
+
+			return [
+				'success' => true,
+				'message' => 'Referral settings saved successfully!'
+			];
+
+		} catch (\Exception $e) {
+			return [
+				'success' => false,
+				'message' => 'Failed to save settings: ' . $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Get formatted reward text for referee
+	 *
+	 * @param bool $full Include "off your first treatment" text
+	 * @return string
+	 */
+	public function getRewardText(bool $full = true): string
+	{
+		$reward_amount = $this->settings['referee_reward_amount'] ?? 20;
+		$reward_type = $this->settings['referee_reward_type'] ?? 'percentage';
+
+		$reward_text = $reward_type === 'percentage'
+			? $reward_amount . '% off'
+			: '$' . number_format($reward_amount, 2) . ' off';
+
+		if ($full) {
+			$reward_text .= ' your first treatment';
+		}
+
+		return $reward_text;
+	}
+
+	public function getShareButtons(int $user_id):void
+	{
+		$referral_code = $this->getUserReferralCode($user_id);
+		if (is_wp_error($referral_code)) {
+			return;
+		}
+
+		$share_url = $this->getShareURL($referral_code);
+		$referral_page_id = $this->getReferralPageId();
+
+		// SMS share text
+		$sms_text = urlencode("Check out " . get_bloginfo('name') . "! " . $share_url);
+
+		// Share message
+		$share_message = urlencode("I love " . get_bloginfo('name') . "! Thought you might want to check them out.");
+
+		?>
+		<nav class="share">
+			<h4>Quick Share</h4>
+			<ul class="share-buttons-grid">
+				<a href="mailto:?subject=<?php echo urlencode('Check out ' . get_bloginfo('name')); ?>&body=<?php echo urlencode($share_message . ' ' . $share_url); ?>"
+				   class="button" title="Email">
+					<?php echo jvbIcon('envelope'); ?>
+				</a>
+				<a href="sms:?&body=<?php echo $sms_text; ?>"
+				   class="button" title="Text">
+					<?php echo jvbIcon('chat'); ?>
+				</a>
+				<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>"
+				   target="_blank"
+				   rel="noopener noreferrer"
+				   class="button" title="Facebook">
+					<?php echo jvbIcon('facebook-logo'); ?>
+				</a>
+				<a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode($share_message); ?>"
+				   target="_blank"
+				   rel="noopener noreferrer"
+				   class="button" title="Twitter">
+					<?php echo jvbIcon('twitter-logo'); ?>
+				</a>
+				<a href="https://wa.me/?text=<?php echo $sms_text; ?>"
+				   target="_blank"
+				   rel="noopener noreferrer"
+				   class="button" title="WhatsApp">
+					<?php echo jvbIcon('whatsapp-logo'); ?>
+				</a>
+			</ul>
+		</nav>
+	<?php
 	}
 }
 
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index e1b457a..bbdc9a5 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -261,6 +261,16 @@
 		if (Features::forContent($post_type)->has('is_calendar'))  {
 			$args = $this->applyCalendarFilters($args, $params);
 		}
+		$taxonomies = array_filter($params, function($param) {
+			return str_starts_with($param, 'tax_');
+		}, ARRAY_FILTER_USE_KEY);
+		if (!empty($taxonomies)) {
+			$params['taxonomies'] = [];
+			foreach ($taxonomies as $taxonomy => $terms) {
+				$taxonomy = str_replace('tax_', '', $taxonomy);
+				$params['taxonomies'][$taxonomy] = $terms;
+			}
+		}
 		if (array_key_exists('taxonomies', $params)) {
 			$args = $this->applyTaxonomyFilters($args, $params);
 		}
@@ -863,7 +873,7 @@
 		return $images;
 	}
 
-	protected function formatTimeline(WP_Post $post):array
+	public function formatTimeline(WP_Post $post):array
 	{
 		$item = $this->prepareItem($post, true, false);
 		//Step 1: Get the fields that apply to all posts
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index daadec6..f871502 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -353,6 +353,9 @@
 				]
 			);
 		}
+		if (array_key_exists('taxonomy', $data) && is_string($data['taxonomy'])) {
+			$data['taxonomy'] = json_decode($data['taxonomy'], true);
+		}
 		$args = $this->applyContextFilters($args, $data);
 		$args = $this->applyTaxonomyFilters($args, $data);
 		$args = $this->applyOrderFilters($args, $data);
@@ -420,7 +423,7 @@
 		if ($cache_check) {
 			return $cache_check; // Returns 304 Not Modified
 		}
-		error_log('Feed Request Args: '.print_r($args, true));
+
 		$key = $this->cache->generateKey($args);
 		$cached = $this->cache->get($key);
 		if ($cached) {
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index 15f293e..b8f198c 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -1,7 +1,8 @@
 <?php
 namespace JVBase\rest\routes;
 
-use JVBase\managers\ReferralManager;
+use JVBase\importers\JaneAppClientImporter;
+use JVBase\managers\JaneSalesImporter;
 use JVBase\rest\RestRouteManager;
 use WP_REST_Request;
 use WP_REST_Response;
@@ -16,11 +17,24 @@
  */
 class ReferralRoutes extends RestRouteManager
 {
-	protected ReferralManager $manager;
+	protected string $referrals_table;
+	protected string $rewards_table;
+	protected string $treatments_table;
+	protected string $jane_clients_table;
+	protected $wpdb;
+
 	public function __construct()
 	{
 		$this->route = 'referrals';
+		$this->cache_name = 'referrals';
 		parent::__construct();
+
+		global $wpdb;
+		$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';
 	}
 
 	public function registerRoutes(): void
@@ -235,6 +249,70 @@
 				]
 			]
 		]);
+
+
+		/***************************
+		 * ADDITIONAL
+		 */
+// CSV Uploads
+		register_rest_route($this->namespace, '/referrals/upload-clients', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handleClientUpload'],
+			'permission_callback' => [$this, 'checkAdminPermission']
+		]);
+
+		register_rest_route($this->namespace, '/referrals/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
+	 */
+	public function checkAdminPermission(WP_REST_Request $request): bool
+	{
+		return current_user_can('manage_options') && parent::checkPermission($request);
 	}
 
 	public function checkPermission(WP_REST_Request $request): bool
@@ -620,27 +698,24 @@
 
 	public function addReferralCodePostRegistration(WP_REST_Request $request): WP_REST_Response
 	{
-		if (!$this->manager) {
-			$this->manager = JVB()->referrals();
-		}
 		$user_id = get_current_user_id();
 		$code = $request->get_param('code');
 
 		// Check if user already has a referral (can't change)
-		$existing = $this->manager->getReferralByReferee($user_id);
+		$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 = $this->manager->getUserByReferralCode($code);
+		$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 = $this->manager->createReferral($referrer->ID, $user_id, $code);
+		$result = JVB()->referrals()->createReferral($referrer->ID, $user_id, $code);
 
 		if ($result) {
 			return $this->success([
@@ -651,4 +726,459 @@
 
 		return $this->error('Failed to apply referral code', 'creation_failed', 500);
 	}
+
+	/**********************************
+	 * ADDITIONAL
+	 */
+	/**
+	 * Handle client CSV upload
+	 */
+	public function handleClientUpload(WP_REST_Request $request): WP_REST_Response
+	{
+		$files = $request->get_file_params();
+
+		if (!isset($files['file'])) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'No file uploaded'
+			], 400);
+		}
+
+		$file = $files['file'];
+
+		// Validate file type
+		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
+		if (!in_array($file['type'], $allowed_types)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File must be a CSV'
+			], 400);
+		}
+
+		// Validate file size (10MB max)
+		if ($file['size'] > 10 * 1024 * 1024) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File size exceeds 10MB limit'
+			], 400);
+		}
+
+		// Import using JaneAppClientImporter
+		$importer = new JaneAppClientImporter();
+		$default_role = get_option(BASE . 'client_import_role', JVB_USER);
+
+		$options = [
+			'update_existing' => true,
+			'send_welcome_email' => false,
+			'create_users' => true,
+			'default_role' => $default_role
+		];
+
+		$result = $importer->importFromCSV($file['tmp_name'], $options);
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $result->get_error_message()
+			], 500);
+		}
+
+		// Build detailed message
+		$message = sprintf(
+			'Import complete: %d created, %d updated, %d skipped',
+			$result['created'],
+			$result['updated'],
+			$result['skipped']
+		);
+
+		$details = [];
+		if (!empty($result['skipped_details'])) {
+			$details = $result['skipped_details'];
+		}
+
+		// Clear cache
+		$this->cache->clear();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => $message,
+			'stats' => $result,
+			'skipped_details' => $details
+		]);
+	}
+
+	/**
+	 * Handle sales CSV upload
+	 */
+	public function handleSalesUpload(WP_REST_Request $request): WP_REST_Response
+	{
+		$files = $request->get_file_params();
+
+		if (!isset($files['file'])) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'No file uploaded'
+			], 400);
+		}
+
+		$file = $files['file'];
+
+		// Validate file type
+		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
+		if (!in_array($file['type'], $allowed_types)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File must be a CSV'
+			], 400);
+		}
+
+		// Validate file size (10MB max)
+		if ($file['size'] > 10 * 1024 * 1024) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File size exceeds 10MB limit'
+			], 400);
+		}
+
+		// Import using JaneSalesImporter
+		$importer = new JaneSalesImporter();
+		$options = [
+			'skip_existing' => true
+		];
+
+		$result = $importer->importFromCSV($file['tmp_name'], $options);
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $result->get_error_message()
+			], 500);
+		}
+
+		// Clear cache
+		$this->cache->clear();
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Sales imported successfully',
+			'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/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 4a6cf48..742e295 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -1093,9 +1093,12 @@
 			$data = $request->get_params();
 
 
+
 			error_log('[UploadRoutes]:handleGroupingRequest: data'.print_r($data, true));
 			error_log('[UploadRoutes]:handleGroupingRequest: args'.print_r($args, true));
-
+			error_log('get_file_params: '.print_r($files, true));
+			global $_FILES;
+			error_log('Global Files: '.print_r($_FILES, true));
 
 			if (!$args['content'] || !$args['user'] || !$args['posts']) {
 
diff --git a/jvb.php b/jvb.php
index 38dbb27..fab005a 100644
--- a/jvb.php
+++ b/jvb.php
@@ -130,6 +130,7 @@
 
 require(JVB_DIR . '/inc/helpers/all.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 = []) {
@@ -628,13 +629,13 @@
         'jvb-uploader',
         JVB_URL.'assets/js/min/uploader.min.js',
         [
-			'sortable-js',
+			'sortable-multidrag',
 			'jvb-cache',
 			'jvb-a11y',
             'jvb-utility',
 			'jvb-handle-selection',
 			'jvb-modal',
-			'jvb-drag-handler',
+//			'jvb-drag-handler',
 //            'jvb-loading',
             'jvb-queue',
             'jvb-notifications'
@@ -662,6 +663,15 @@
 		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',
diff --git a/src/faq/style.scss b/src/faq/style.scss
index 4bb9bc6..14b79d2 100644
--- a/src/faq/style.scss
+++ b/src/faq/style.scss
@@ -4,6 +4,7 @@
 	background-color: var(--base-100);
 	border-radius: var(--outerRadius);
 	padding: 1.5rem;
+	touch-action: auto;
 	ol {
 		list-style: decimal-leading-zero;
 		height: fit-content;
diff --git a/src/feed/view.js b/src/feed/view.js
index abb91e5..6c64b6e 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -55,7 +55,7 @@
 				favourites: '[data-filter="favourites"]',
 				taxonomy: '[data-filter^="taxonomy"]'
 			},
-			selectedTax: '.selected-terms',
+			selectedTax: '.selected-items',
 			clearFilter: 'button.clear-filters',
 			loadMore: 'button.load-more',
 			filterContainer: '.filters',
@@ -89,24 +89,39 @@
 		buttons.forEach((button) => {
 			const taxonomy = button.dataset.taxonomy;
 			this.currentTaxonomies.add(taxonomy);
+
 			this.selector.registerFilterButton(button, {
 				button: button,
 				buttonSelector: '[data-filter="taxonomy"]',
 				selected: this.ui.selectedTax
 			});
+
+			// Add preload listeners
+			this.addTaxonomyPreloadListeners(button, taxonomy);
 		});
 
-		// Make this non-blocking
-		setTimeout(() => {
-			this.selector.batchFetchTaxonomies();
-			this.selector.isInitializing = false;
-		}, 0);
+		this.selector.isInitializing = false;
 
 		this.selector.subscribe((event, data) => {
 			if (event === 'selected-terms') this.handleTaxonomyChange(data);
 		});
 	}
 
+	addTaxonomyPreloadListeners(button, taxonomy) {
+		const preload = () => {
+			this.selector.preloadTaxonomy(taxonomy);
+		};
+
+		// Desktop hover
+		button.addEventListener('mouseenter', preload, { once: true });
+
+		// Touch/keyboard (fires before click)
+		button.addEventListener('pointerdown', preload, { once: true });
+
+		// Keyboard focus
+		button.addEventListener('focus', preload, { once: true });
+	}
+
 	handleTaxonomyChange(data) {
 		const { terms, taxonomy } = data;
 
@@ -177,10 +192,10 @@
 	}
 
 	initStore() {
-		this.addPlaceholders();
-		this.store = window.jvbStore.register(
+		const store = window.jvbStore.register(
 			'feed',
 			{
+				storeName: 'feed',
 				endpoint: 'feed',
 				keyPath: 'id',
 				indexes: [
@@ -197,6 +212,7 @@
 				delayFetch: true
 			}
 		);
+		this.store = store.feed;
 
 		this.store.subscribe((event, data) => {
 			switch (event) {
@@ -317,7 +333,6 @@
 		let items = this.store.getFiltered();
 		if (this.store.filters['page'] === 1) {
 			window.removeChildren(this.ui.grid);
-			this.addPlaceholders();
 		}
 
 		if (items.length === 0) {
@@ -436,8 +451,17 @@
 		return template;
 	}
 
+	removePlaceholders() {
+		const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+		if (placeholders.length > 0) {
+			placeholders.forEach(p => p.remove());
+		}
+	}
+
+
 	addPlaceholders() {
 		let total = this.contentTypes.length;
+		const fragment = document.createDocumentFragment();
 		for (let i = 0; i < 12; i++) {
 			let template = window.getTemplate('placeholderTemplate');
 
@@ -448,15 +472,10 @@
 			} else {
 				icon = window.getIcon(this.container.dataset.icon);
 			}
-			template.appendChild(icon);
-			this.ui.grid.appendChild(template);
+			template.append(icon);
+			fragment.append(template);
 		}
-	}
-
-	removePlaceholders() {
-		if (this.ui.grid.querySelector('.placeholder')) {
-			window.removeChildren(this.ui.grid);
-		}
+		this.ui.grid.append(fragment);
 	}
 
 
diff --git a/src/gmbreviews/render.php b/src/gmbreviews/render.php
index 7aa2dcb..43d203d 100644
--- a/src/gmbreviews/render.php
+++ b/src/gmbreviews/render.php
@@ -17,6 +17,8 @@
 	if ($inheritUser) {
 		global $post;
 		$inheritUser = $post->post_author;
+	}else {
+		$inheritUser = null;
 	}
 	try {
 		$gmb = JVB()->connect('gmb', $inheritUser);
diff --git a/src/gmbreviews/style.scss b/src/gmbreviews/style.scss
index 339f5dc..b7804c9 100644
--- a/src/gmbreviews/style.scss
+++ b/src/gmbreviews/style.scss
@@ -1,6 +1,7 @@
 .gmb-reviews {
-	max-width: var(--alignWide);
+	max-width: none;
 	> .row.btw {
+		max-width:var(--alignWide);
 		.button {
 			width: 100%;
 			height: max-content;
@@ -19,9 +20,12 @@
 		list-style: none;
 		margin: 0;
 		padding: 0;
+		max-width: var(--wider);
 		li {
 			margin: 2rem 0;
 			position:relative;
+			background-color: var(--base-100);
+			padding: 1rem;
 			@media (min-width: 768px) {
 				&:nth-of-type(odd) {
 					left: -2rem;

--
Gitblit v1.10.0