From e729f920139f0c65902be2d6b2c32466b08375e8 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 20 Oct 2025 17:54:52 +0000
Subject: [PATCH] =Form updates

---
 assets/js/dash/SquareCheckout.js             |  258 +
 assets/css/copy-hours.min.css                |    2 
 inc/helpers/renderFields.php                 |   22 
 assets/css/forms.min.css                     |    2 
 inc/managers/FormManager.php                 |    1 
 assets/js/dash/UtilityFunctions.js           |   15 
 jvb.php                                      |    5 
 inc/helpers/ui.php                           |   20 
 assets/js/min/square.min.js                  |    2 
 inc/managers/ReferralManager.php             |   80 
 assets/js/min/form.min.js                    |    2 
 build/forms/view.asset.php                   |    2 
 inc/rest/_setup.php                          |    4 
 inc/meta/MetaForm.php                        | 2981 +++++++---------------
 inc/meta/MetaFormOld.php                     | 2128 ++++++++++++++++
 assets/js/min/utility.min.js                 |    2 
 JVBase.php                                   |    8 
 inc/rest/routes/IntegrationsSquareRoutes.php |  239 +
 inc/integrations/Square.php                  |  444 +++
 inc/meta/MetaManager.php                     |    9 
 inc/integrations/Integrations.php            |   93 
 src/forms/view.js                            |  153 
 assets/js/concise/FormController.js          | 1152 ++++++++
 build/forms/view.js                          |    2 
 inc/blocks/FormBlock.php                     |   58 
 icons.php                                    |    4 
 inc/registry/PostTypeRegistrar.php           |    2 
 27 files changed, 5,426 insertions(+), 2,264 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 7191fff..e6fe838 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -13,6 +13,7 @@
 use JVBase\managers\UserTermsManager;
 use JVBase\rest\routes\FeedRoutes;
 use JVBase\rest\routes\FavouritesRoutes;
+use JVBase\rest\routes\IntegrationsSquareRoutes;
 use JVBase\rest\routes\NotificationsRoutes;
 use JVBase\rest\routes\ContentRoutes;
 use JVBase\rest\routes\TermRoutes;
@@ -84,7 +85,6 @@
 			'userTerms'		=> new UserTermsManager(),
         ];
 
-		add_action('wp_footer', [$this, 'additionalActions']);
 
 		if (Features::forSite()->has('magicLink')) {
 			$this->routes['magicLink'] = new MagicLinkRoutes();
@@ -98,6 +98,10 @@
 			$this->managers['dash'] = new DashboardManager();
 		}
 
+		if (Features::hasIntegration('square')){
+			$this->routes['square'] = new IntegrationsSquareRoutes();
+		}
+
         $this->routes = [
 			'integrations'	=> new IntegrationsRoutes(),
 		];
@@ -148,6 +152,8 @@
         }
 
 		$this->setupIntegrations();
+
+		add_action('wp_footer', [$this, 'additionalActions']);
 //        $this->managers['notifications'] = new NotificationManager();
         // Register activation hook
         register_activation_hook(JVB_DIR . '/jvb.php', [$this, 'activate']);
diff --git a/assets/css/copy-hours.min.css b/assets/css/copy-hours.min.css
index 16967a5..101a314 100644
--- a/assets/css/copy-hours.min.css
+++ b/assets/css/copy-hours.min.css
@@ -1 +1 @@
-.group-fields{position:relative}.hours-copy-btn:hover{background-color:var(--action-50);transform:scale(1.05)}.hours-copy-btn:active{transform:scale(.95)}.hours-copy-btn .icon{--w:0.875rem}.copy-hours-content h3{margin:0 0 1rem 0;color:var(--contrast);font-size:var(--large)}.copy-hours-source{background-color:var(--base-100);padding:1rem;border-radius:var(--innerRadius);margin-bottom:1.5rem;border:1px solid var(--base-200)}.copy-hours-source h4{margin:0 0 .5rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--small);font-weight:600}.source-info{--gap:.25rem}.source-day{font-weight:600;color:var(--contrast);text-transform:capitalize}.source-hours{--gap:1rem;font-weight:500;color:var(--contrast)}.source-hours.closed{color:var(--contrast-200);font-style:italic}.copy-hours-targets{margin-bottom:2rem}.copy-hours-targets h4{margin:0 0 1rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--small);font-weight:600}.day-checkboxes{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem}.feedback{position:fixed;top:2rem;right:2rem;background-color:var(--action-50);color:var(--action-contrast);padding:1rem 1.5rem;border-radius:var(--innerRadius);box-shadow:var(--shadow);z-index:10000;opacity:0;transform:translateX(100px);transition:all var(--transition-base);display:flex;align-items:center;gap:.5rem}.feedback.show{opacity:1;transform:translateX(0)}.feedback .icon{--w:1.25rem}@media (prefers-color-scheme:dark){.feedback{background-color:var(--success-dark,#1e7e34)}}
\ No newline at end of file
+.group-fields{position:relative}.hours-copy-btn:hover{background-color:var(--action-50);transform:scale(1.05)}.hours-copy-btn:active{transform:scale(.95)}.hours-copy-btn .icon{--w:0.875rem}.copy-hours-content h3{margin:0 0 1rem 0;color:var(--contrast);font-size:var(--large)}.copy-hours-source{background-color:var(--base-100);padding:1rem;border-radius:var(--innerRadius);margin-bottom:1.5rem;border:1px solid var(--base-200)}.copy-hours-source h4{margin:0 0 .5rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--small);font-weight:600}.source-info{--gap:.25rem}.source-day{font-weight:600;color:var(--contrast);text-transform:capitalize}.source-hours{--gap:1rem;font-weight:500;color:var(--contrast)}.source-hours.closed{color:var(--contrast-200);font-style:italic}.copy-hours-targets{margin-bottom:2rem}.copy-hours-targets h4{margin:0 0 1rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--small);font-weight:600}.day-checkboxes{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem}.feedback{position:fixed;top:2rem;right:2rem;background-color:var(--action-50);color:var(--action-contrast);padding:1rem 1.5rem;border-radius:var(--innerRadius);box-shadow:var(--shadow);z-index:10000;opacity:0;transform:translateX(100px);transition:all var(--transition-base);display:flex;align-items:center;gap:.5rem}.feedback.show{opacity:1;transform:translateX(0)}.feedback .icon{--w:1.25rem}
\ No newline at end of file
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index 4acd93b..188f3b8 100644
--- a/assets/css/forms.min.css
+++ b/assets/css/forms.min.css
@@ -1 +1 @@
-details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .star+.star,[type=radio].featured:checked+label .star{display:none}[type=radio].featured+label .star,[type=radio].featured:checked+label .star+.star{display:inline-block}.restore.item,.upload.item{border-radius:var(--innerRadius);overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:calc(100% - (var(--padding) * 2.5));height:calc(100% - (var(--padding) * 2))}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/
\ No newline at end of file
+details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .star+.star,[type=radio].featured:checked+label .star{display:none}[type=radio].featured+label .star,[type=radio].featured:checked+label .star+.star{display:inline-block}.restore.item,.upload.item{border-radius:var(--innerRadius);overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--innerRadius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--small)}form nav.tabs button h2{font-size:var(--small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}
\ No newline at end of file
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 6663484..cdae988 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -28,6 +28,10 @@
 		this.specialFields = new Map();
 		this.dependencies = new Map();
 
+		// Validation (YOU ARE GREAT!)
+		this.validators = this.initValidators();
+		this.touchedFields = new Set();
+
 		// Auto-save configuration
 		this.autoSaveDefaults = {
 			delay: 3000, // 3 seconds
@@ -71,15 +75,26 @@
 		switch(event) {
 			case 'item-saved':
 				if (data.item.status === 'autosave') {
-					this.showFormStatus(data.item.formId, 'autosave');
+					// this.showFormStatus(data.item.formId, 'autosave');
 				}
 				break;
 			case 'data-loaded':
-
+				this.checkPendingForms();
 				break;
 		}
 	}
 
+	async checkPendingForms() {
+		let items = await this.store.query('status', 'draft');
+		items.forEach(item => {
+			let form = this.forms.get(item.formId);
+			if (form && form.element) {
+				form.element.querySelector('.restore-form').hidden = false;
+				new this.populateForm(form.element, item.data);
+			}
+		});
+
+	}
 	/**
 	 * Check for pending operations from previous session
 	 */
@@ -160,11 +175,11 @@
 	initListeners() {
 		// Only add if not already added
 		if (!this.globalHandlersAdded) {
-			document.addEventListener('submit', this.submitHandler);
 			document.addEventListener('click', this.clickHandler);
 			document.addEventListener('change', this.changeHandler);
 			document.addEventListener('focus', this.focusHandler, true);
 			document.addEventListener('blur', this.blurHandler, true);
+			document.addEventListener('input', this.inputHandler);
 			this.globalHandlersAdded = true;
 		}
 	}
@@ -176,6 +191,8 @@
 		const formId = formElement.dataset.formId || `form_${Date.now()}`;
 		formElement.dataset.formId = formId;
 
+		formElement.addEventListener('submit', this.submitHandler);
+
 		const formConfig = {
 			element: formElement,
 			id: formId,
@@ -199,7 +216,7 @@
 
 		// Check for pending data
 		if (this.store && formConfig.options.cache) {
-			const cached = this.store.getForm(formId);
+			const cached = this.store.get(formId);
 			if (cached && cached.formData) {
 				this.showPendingNotification(cached);
 			}
@@ -231,7 +248,9 @@
 
 		// Initialize tabs if present
 		if (window.jvbTabs && form.querySelector('nav.tabs')) {
-			new window.jvbTabs(form);
+			formConfig.tabs = new window.jvbTabs(form);
+			this.forms.set(formConfig.formId, formConfig);
+			this.initSteppedForm(formConfig.formId);
 		}
 
 		// Scan for existing selector fields
@@ -241,6 +260,120 @@
 	}
 
 	/**
+	 * Initialize stepped form functionality
+	 */
+	initSteppedForm(formId) {
+		const formConfig = this.forms.get(formId);
+		const form = formConfig.element;
+		const tabsInstance = formConfig.tabs;
+
+		const sections = form.querySelectorAll('.tab-content');
+		const totalSteps = sections.length;
+		const progressBar = form.querySelector('.form-progress .fill');
+		const stepText = form.querySelector('.step-text .current');
+		const tabButtons = form.querySelectorAll('nav.tabs button');
+
+		// Update progress display
+		const updateProgress = (currentStep) => {
+			const progress = (currentStep / totalSteps) * 100;
+			if (progressBar) {
+				progressBar.style.width = progress + '%';
+			}
+			if (stepText) {
+				stepText.textContent = currentStep;
+			}
+
+			// Update tab states
+			tabButtons.forEach((btn, idx) => {
+				const stepNum = idx + 1;
+				btn.classList.remove('current', 'completed', 'pending');
+
+				if (stepNum < currentStep) {
+					btn.classList.add('completed');
+				} else if (stepNum === currentStep) {
+					btn.classList.add('current');
+				} else {
+					btn.classList.add('pending');
+				}
+			});
+		};
+
+		// Next/Previous button handling
+		form.addEventListener('click', (e) => {
+			const nextBtn = e.target.closest('[data-action="next-step"]');
+			const prevBtn = e.target.closest('[data-action="prev-step"]');
+
+			if (nextBtn) {
+				e.preventDefault();
+				const currentSection = nextBtn.closest('.tab-content');
+				const currentStep = parseInt(currentSection.dataset.step);
+				const nextSection = form.querySelector(`.tab-content[data-step="${currentStep + 1}"]`);
+
+				if (nextSection && this.validateStep(currentSection)) {
+					const nextTab = nextSection.dataset.tab;
+					tabsInstance.switchTab(nextTab, true);
+					updateProgress(currentStep + 1);
+
+					// Scroll to top of form
+					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+				}
+			}
+
+			if (prevBtn) {
+				e.preventDefault();
+				const currentSection = prevBtn.closest('.tab-content');
+				const currentStep = parseInt(currentSection.dataset.step);
+				const prevSection = form.querySelector(`.tab-content[data-step="${currentStep - 1}"]`);
+
+				if (prevSection) {
+					const prevTab = prevSection.dataset.tab;
+					tabsInstance.switchTab(prevTab, true);
+					updateProgress(currentStep - 1);
+
+					// Scroll to top of form
+					form.scrollIntoView({ behavior: 'smooth', block: 'start' });
+				}
+			}
+		});
+
+		// Update progress when tabs are clicked directly
+		const originalSwitchTab = tabsInstance.switchTab.bind(tabsInstance);
+		tabsInstance.switchTab = (tab, updateHistory) => {
+			originalSwitchTab(tab, updateHistory);
+			const activeSection = form.querySelector(`.tab-content[data-tab="${tab}"]`);
+			if (activeSection) {
+				const step = parseInt(activeSection.dataset.step);
+				updateProgress(step);
+			}
+		};
+
+		// Initialize progress
+		updateProgress(1);
+	}
+
+	/**
+	 * Validate current step before allowing progression
+	 * Can be enhanced with custom validation rules
+	 */
+	validateStep(section) {
+		const fields = section.querySelectorAll('.field');
+		let allValid = true;
+
+		fields.forEach(fieldWrapper => {
+			const input = fieldWrapper.querySelector('input, textarea, select');
+			if (input && !input.closest('[hidden]')) {
+				const isValid = this.validateField(input, fieldWrapper);
+				if (!isValid) {
+					allValid = false;
+				}
+			}
+		});
+
+		return allValid;
+	}
+
+
+	/**
 	 * Initialize Quill editors
 	 */
 	initQuillEditors(form) {
@@ -522,16 +655,12 @@
 		if (this.subscribers.size > 0 ){
 			const form = event.target;
 			if (!form.dataset.formId) return;
-			this.store.delete(form.dataset.formId);
-
 			event.preventDefault();
 
 			const formConfig = this.forms.get(form.dataset.formId);
 			if (!formConfig) return;
 
 			const formData = this.collectFormData(form);
-
-			event.preventDefault();
 			this.notify('form-submit', {
 				formId: formConfig.id,
 				data: formData,
@@ -544,9 +673,24 @@
 		if (window.targetCheck(e, 'div.quantity')) {
 			let container = window.targetCheck(e, 'div.quantity');
 			this.handleNumberClick(e, container.querySelector('input'));
+		} else if (window.targetCheck(e, '[data-action]')) {
+			let action = window.targetCheck(e, '[data-action]');
+			action = action.dataset.action;
+			switch (action) {
+				case 'clear-form':
+					let form = e.target.closest('form');
+					this.store.delete(form.dataset.formId);
+					form?.reset();
+					e.target.closest('.restore-form').hidden = true;
+					break;
+				case 'dismiss-restore':
+					e.target.closest('.restore-form').hidden = true;
+					break;
+			}
 		}
 	}
 
+
 	handleNumberClick(e, input) {
 		let change = 0;
 
@@ -635,23 +779,452 @@
 		}
 	}
 
-	handleBlur(event) {
-		const target = event.target;
+	handleBlur(e) {
+		const target = e.target;
 		const form = target.form || target.closest('form');
 
 		if (!form) return;
 
-		const formConfig = this.forms?.get(form.dataset.formId);
-		if (formConfig && formConfig.options.autoSave && !form.dataset.noautosave) {
-			// Shorter delay on blur
-			this.scheduleSave(formConfig, {
-				type: 'blur',
-				fieldName: target.name,
-				delay: 1500
-			});
+
+		const input = e.target.closest('input, textarea, select');
+		if (input) {
+			const fieldWrapper = this.findFieldWrapper(input);
+			if (fieldWrapper) {
+				// Mark as touched and validate
+				const fieldName = fieldWrapper.dataset.field;
+				if (fieldName) {
+					if (this.shouldDebounce(input)) {
+						window.debouncer.cancel(`validate_${fieldName}`);
+					}
+					this.touchedFields.add(fieldName);
+				}
+				this.validateField(input, fieldWrapper);
+			}
+			const formConfig = this.forms?.get(form.dataset.formId);
+			if (formConfig && formConfig.options.autoSave && !form.dataset.noautosave) {
+				// Shorter delay on blur
+				this.scheduleSave(formConfig, {
+					type: 'blur',
+					fieldName: target.name,
+					delay: 1500
+				});
+			}
 		}
 	}
 
+	handleInput(e) {
+		const input = e.target.closest('input, textarea, select');
+		if (!input) return;
+
+		let form = input.closest('form');
+		this.showFormStatus(form.dataset.formId, 'pending');
+
+		const fieldWrapper = this.findFieldWrapper(input);
+		if (!fieldWrapper) return;
+
+		const fieldName = fieldWrapper.dataset.field;
+		if (fieldName) {
+			this.touchedFields.add(fieldName);
+		}
+
+		if (this.shouldDebounce(input)){
+			window.debouncer.schedule(
+				`validate_${fieldName}`,
+				(input, fieldWrapper) => this.validateField.bind(this),
+				500
+			)
+		}
+	}
+
+	/***************************************************************
+	 FORM VALIDATION
+	***************************************************************/
+	/**
+	 * Initialize validation rules
+	 */
+	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: (value, fieldWrapper) => {
+					const num = parseFloat(value);
+					if (isNaN(num)) return 'Please enter a valid number';
+
+					const min = fieldWrapper.dataset.min;
+					const max = fieldWrapper.dataset.max;
+
+					if (min !== undefined && num < parseFloat(min)) {
+						return `Value must be at least ${min}`;
+					}
+					if (max !== undefined && num > parseFloat(max)) {
+						return `Value must be at most ${max}`;
+					}
+					return true;
+				}
+			},
+			text: {
+				test: (value, fieldWrapper) => {
+					const minLength = fieldWrapper.dataset.minlength;
+					const maxLength = fieldWrapper.dataset.maxlength;
+
+					if (minLength && value.length < parseInt(minLength)) {
+						return `Must be at least ${minLength} characters`;
+					}
+					if (maxLength && value.length > parseInt(maxLength)) {
+						return `Must be no more than ${maxLength} characters`;
+					}
+					return true;
+				}
+			}
+		};
+	}
+	/**
+	 * Find the field wrapper (handles both simple and complex fields)
+	 */
+	findFieldWrapper(input) {
+		// Try to find the closest .field wrapper
+		let wrapper = input.closest('.field');
+
+		// If we're in a repeater row, make sure we get the right field wrapper
+		if (!wrapper) {
+			wrapper = input.closest('[data-field]');
+		}
+
+		return wrapper;
+	}
+
+	/**
+	 * Check if input should be debounced
+	 */
+	shouldDebounce(input) {
+		const debounceTypes = ['text', 'email', 'url', 'tel', 'search'];
+		return debounceTypes.includes(input.type) || input.tagName === 'TEXTAREA';
+	}
+
+	/**
+	 * Validate a single field
+	 */
+	validateField(input, fieldWrapper) {
+		const value = this.getFieldValue(input);
+		const fieldName = fieldWrapper.dataset.field;
+
+		// Skip validation if field hasn't been touched yet (unless it's required)
+		if (!this.touchedFields.has(fieldName) && !input.required) {
+			return true;
+		}
+
+		// Skip validation if field is empty and not required
+		if (!value && !input.required) {
+			this.clearValidation(fieldWrapper);
+			return true;
+		}
+
+		// Check required
+		if (input.required && !value) {
+			this.showError(fieldWrapper, 'This field is required');
+			return false;
+		}
+
+		// Check HTML5 validity first
+		if (input.checkValidity && !input.checkValidity()) {
+			this.showError(fieldWrapper, input.validationMessage);
+			return false;
+		}
+
+		// Custom pattern validation from data attribute
+		const pattern = fieldWrapper.dataset.pattern;
+		if (pattern && value) {
+			const regex = new RegExp(pattern);
+			if (!regex.test(value)) {
+				const message = fieldWrapper.dataset.validationMessage || 'Invalid format';
+				this.showError(fieldWrapper, message);
+				return false;
+			}
+		}
+
+		// Type-specific validation
+		const validateType = fieldWrapper.dataset.validate || input.type;
+		if (validateType && this.validators[validateType]) {
+			const validator = this.validators[validateType];
+
+			if (validator.pattern && !validator.pattern.test(value)) {
+				this.showError(fieldWrapper, validator.message);
+				return false;
+			}
+
+			if (validator.test) {
+				const result = validator.test(value, fieldWrapper);
+				if (result !== true) {
+					this.showError(fieldWrapper, result);
+					return false;
+				}
+			}
+		}
+
+		// All validations passed
+		this.showSuccess(fieldWrapper);
+		return true;
+	}
+
+	/**
+	 * Get field value (handles different input types)
+	 */
+	getFieldValue(input) {
+		if (!input) return '';
+
+		if (input.type === 'checkbox') {
+			return input.checked ? input.value || '1' : '';
+		} else if (input.type === 'radio') {
+			const checked = input.form?.querySelector(`[name="${input.name}"]:checked`);
+			return checked ? checked.value : '';
+		} else if (input.type === 'select-multiple') {
+			return Array.from(input.selectedOptions).map(o => o.value);
+		}
+
+		return input.value?.trim() || '';
+	}
+
+	/**
+	 * Show success state (green checkmark)
+	 */
+	showSuccess(fieldWrapper) {
+		if (!fieldWrapper) return;
+
+		// Find validation elements (they might be in field-input-wrapper or field-content)
+		const success = fieldWrapper.querySelector('.validation-icon.success');
+		const error = fieldWrapper.querySelector('.validation-icon.error');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		// Remove error state
+		fieldWrapper.classList.remove('has-error');
+		input?.classList.remove('error');
+
+		// Add success state
+		fieldWrapper.classList.add('has-success');
+
+		// Show checkmark (if element exists)
+		if (success) {
+			success.hidden = false;
+		}
+		if (error) {
+			error.hidden = true;
+		}
+
+		// Hide error message
+		if (message) {
+			message.hidden = true;
+			message.textContent = '';
+		}
+	}
+
+	/**
+	 * Show error state (red message below field)
+	 */
+	showError(fieldWrapper, errorMessage) {
+		if (!fieldWrapper) return;
+
+		const success = fieldWrapper.querySelector('.validation-icon.success');
+		const error = fieldWrapper.querySelector('.validation-icon.error');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		// Remove success state
+		fieldWrapper.classList.remove('has-success');
+
+		// Add error state
+		fieldWrapper.classList.add('has-error');
+		input?.classList.add('error');
+
+		// Hide checkmark (if element exists)
+		if (success) {
+			success.hidden = true;
+		}
+		//show x
+		if (error) {
+			error.hidden = false;
+		}
+
+		// Show error message
+		if (message) {
+			message.hidden = false;
+			message.textContent = errorMessage;
+		}
+	}
+
+	/**
+	 * Clear validation state
+	 */
+	clearValidation(fieldWrapper) {
+		if (!fieldWrapper) return;
+
+		const icon = fieldWrapper.querySelector('.validation-icon');
+		const message = fieldWrapper.querySelector('.validation-message');
+		const input = fieldWrapper.querySelector('input, textarea, select');
+
+		fieldWrapper.classList.remove('has-error', 'has-success');
+		input?.classList.remove('error');
+
+		if (icon) {
+			icon.hidden = true;
+		}
+
+		if (message) {
+			message.hidden = true;
+			message.textContent = '';
+		}
+	}
+
+	/**
+	 * Validate all fields in a container (useful for step validation)
+	 */
+	validateAllFields(container) {
+		if (!container) return true;
+
+		const fields = container.querySelectorAll('.field:not([hidden])');
+		let allValid = true;
+
+		fields.forEach(fieldWrapper => {
+			// Skip complex parent wrappers (repeater, group) - validate their children
+			if (this.isComplexFieldWrapper(fieldWrapper)) {
+				return;
+			}
+
+			const input = fieldWrapper.querySelector('input:not([type="hidden"]), textarea, select');
+			if (input && !input.closest('[hidden]')) {
+				// Mark as touched so validation will run
+				const fieldName = fieldWrapper.dataset.field;
+				if (fieldName) {
+					this.touchedFields.add(fieldName);
+				}
+
+				const isValid = this.validateField(input, fieldWrapper);
+				if (!isValid) {
+					allValid = false;
+
+					// Scroll to first error
+					if (allValid === false) {
+						input.scrollIntoView({ behavior: 'smooth', block: 'center' });
+						input.focus();
+					}
+				}
+			}
+		});
+
+		return allValid;
+	}
+
+	/**
+	 * Check if field wrapper is a complex type (repeater, group, etc.)
+	 */
+	isComplexFieldWrapper(fieldWrapper) {
+		return fieldWrapper.classList.contains('repeater') ||
+			fieldWrapper.classList.contains('group') ||
+			fieldWrapper.classList.contains('upload');
+	}
+
+	/**
+	 * Special validation for repeater fields
+	 */
+	attachRepeaterValidation(form) {
+		// When a repeater row is added, attach validation to its fields
+		form.addEventListener('click', (e) => {
+			if (e.target.closest('.add-repeater-row')) {
+				// Wait for the DOM to update
+				setTimeout(() => {
+					const repeaterRows = form.querySelectorAll('.repeater-row');
+					repeaterRows.forEach(row => {
+						const inputs = row.querySelectorAll('input, textarea, select');
+						inputs.forEach(input => {
+							const fieldWrapper = this.findFieldWrapper(input);
+							if (fieldWrapper) {
+								// Validation listeners are already attached via event delegation
+								// Just clear any existing validation state for new rows
+								this.clearValidation(fieldWrapper);
+							}
+						});
+					});
+				}, 100);
+			}
+		});
+	}
+
+	/**
+	 * Special validation for group fields
+	 */
+	attachGroupValidation(form) {
+		// Group fields might have conditional fields
+		// Validate when conditions change
+		form.addEventListener('change', (e) => {
+			const changedInput = e.target.closest('input, select');
+			if (!changedInput) return;
+
+			// Check if this change affects conditional fields
+			const fieldName = changedInput.name;
+			if (!fieldName) return;
+
+			// Find any conditional fields that depend on this field
+			const conditionalFields = form.querySelectorAll(`[data-show-if*="${fieldName}"]`);
+			conditionalFields.forEach(conditionalField => {
+				// Clear validation for hidden fields
+				if (conditionalField.hidden) {
+					this.clearValidation(conditionalField);
+				}
+			});
+		});
+	}
+
+	/**
+	 * Reset validation state for a form
+	 */
+	resetForm(form) {
+		if (!form) return;
+
+		// Clear all touched fields
+		this.touchedFields.clear();
+
+		// Clear all validation states
+		const fields = form.querySelectorAll('.field');
+		fields.forEach(fieldWrapper => {
+			this.clearValidation(fieldWrapper);
+		});
+	}
+
+	/**
+	 * Get validation errors for a form
+	 */
+	getFormErrors(form) {
+		const errors = {};
+		const fields = form.querySelectorAll('.field.has-error');
+
+		fields.forEach(fieldWrapper => {
+			const fieldName = fieldWrapper.dataset.field;
+			const message = fieldWrapper.querySelector('.validation-message');
+			if (fieldName && message) {
+				errors[fieldName] = message.textContent;
+			}
+		});
+
+		return errors;
+	}
+
+	/**
+	 * Add custom validator
+	 */
+	addValidator(name, validator) {
+		this.validators[name] = validator;
+	}
 	/* ========== Auto-save functionality ========== */
 	/**
 	 * Get appropriate delay based on field type and context
@@ -671,7 +1244,7 @@
 		return this.autoSaveDefaults.delay;
 	}
 	scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
-		document.addEventListener('input', this.handleInput, {passive: true});
+		document.addEventListener('input', this.saveCheck, {passive: true});
 		const saveKey = `autosave_${formConfig.id}`;
 
 		this.debouncer.schedule(
@@ -682,24 +1255,27 @@
 	}
 
 	//Extend delay if user is currently typing
-	handleInput(e) {
+	saveCheck(e) {
 		let form = e.target.closest('form[data-id]');
 		if (!form) {
 			return;
 		}
+
 		this.scheduleSave(this.forms.get(form.dataset.id));
 	}
 
 	async autosave(formConfig) {
 		const formData = this.collectFormData(formConfig.element);
 
+		this.showFormStatus(formConfig.id, 'saving');
 		await this.store.save({
 			formId: formConfig.id,
 			data: formData,
 			status: 'draft',
 			timestamp: Date.now()
+		}).then(()=> {
+			this.showFormStatus(formConfig.id, 'autosaved');
 		});
-		this.showFormStatus(formConfig.id, 'saved');
 
 		// Get only changed fields
 		const changes = this.getChangedFields(formConfig.data, formData);
@@ -742,30 +1318,48 @@
 		return Object.keys(changes).length > 0;
 	}
 
-	showFormStatus(form, status) {
+	showFormStatus(formID, status) {
 		// Remove existing status
-		const existingStatus = form.querySelector('.form-status');
-		if (existingStatus) {
-			existingStatus.remove();
-		}
+		let form = this.forms.get(formID);
+
+		console.log('Setting status: ', status);
 
 		// Add new status
-		const statusElement = document.createElement('div');
-		statusElement.className = `form-status status-${status}`;
+		const statusWrap = form.element.querySelector('.fstatus');
+		statusWrap.hidden = false;
+		const statusElement = statusWrap.querySelector('.message');
+		statusElement.textContent = '';
+		statusWrap.querySelector('.icon')?.remove();
 
 		const messages = {
 			'saving': 'Saving changes...',
-			'saved': 'Changes saved',
-			'error': 'Failed to save 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'
 		};
+		const icons = {
+			'autosaved': 'check',
+			'submitted': 'check',
+			'error': 'close',
+			'offline': 'cloud-slash',
+			'pending': 'exclamation-mark'
+		}
 
+		let icon = window.getIcon(icons[status]);
+		if (icon) {
+			statusWrap.prepend(icon);
+		}
+		console.log(status, messages[status]);
+		console.log(status, icons[status]);
 		statusElement.textContent = messages[status] || status;
-		form.insertBefore(statusElement, form.firstChild);
+		statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
 
 		// Auto-hide success messages
-		if (status === 'saved') {
-			setTimeout(() => statusElement.remove(), 3000);
+		if (status === 'submitted') {
+			setTimeout(() => statusWrap.hidden = true, 3000);
 		}
 	}
 
@@ -810,7 +1404,7 @@
 		if (key.includes('|')) return this.processTableField;
 		if (key.includes('::')) return this.processGroupField;
 		if (key.includes(':')) return this.processRepeaterField;
-		if (key.includes('[')) return this.processLocationField;
+		if (/\[[^\]]+\]/.test(key)) return this.processLocationField;
 		return this.processRegularField;
 	}
 
@@ -944,6 +1538,7 @@
 
 	processRegularField(key, value, data, repeaterData, postData, form) {
 		//handle array values (like checkboxes/selects)
+		key = key.replace('[]','');
 		if (data[key]) {
 			if (!Array.isArray(data[key])) {
 				data[key] = [data[key]];
@@ -973,6 +1568,485 @@
 		return window.getDifferences?.map(original, current) || {};
 	}
 
+	/*******************************************************
+	 Field Summary
+	*******************************************************/
+	/**
+	 * Show a comprehensive summary of form submission
+	 */
+	showSummary(formId, clear = 'form') {
+		const formConfig = this.forms.get(formId);
+		if (!formConfig) return;
+
+		const form = formConfig.element || document.querySelector(`[data-form-id="${formId}"]`);
+		const summary = window.getTemplate('formSummary');
+
+		const [
+			title,
+			resultWrapper,
+			resultTemplate
+		] = [
+			summary.querySelector('h2'),
+			summary.querySelector('.summary'),
+			summary.querySelector('.result')
+		];
+
+		// Fields to skip in summary
+		const skipFields = ['sendAll', ...this.ignore];
+
+		// Process each field in the form data
+		for (const [key, value] of Object.entries(formConfig.data)) {
+			// Skip ignored fields and empty values
+			if (skipFields.includes(key) || this.isEmptyValue(value)) {
+				continue;
+			}
+
+			// Get field info from form
+			const fieldInfo = this.getFieldInfo(form, key);
+			if (!fieldInfo.label) continue; // Skip if no label found
+
+			// Create result element
+			const resultEl = this.createResultElement(
+				resultTemplate,
+				fieldInfo,
+				value,
+				form
+			);
+
+			if (resultEl) {
+				resultWrapper.appendChild(resultEl);
+			}
+		}
+
+		// Remove template
+		resultTemplate.remove();
+
+		// Insert summary and hide form
+		clear = (clear !== 'form') ? form.closest(clear)??form : form;
+
+		clear.after(summary);
+		window.fade(clear, false);
+	}
+
+	/**
+	 * Check if a value is empty (null, undefined, empty string, empty array, empty object)
+	 */
+	isEmptyValue(value) {
+		if (value === null || value === undefined || value === '') {
+			return true;
+		}
+		if (Array.isArray(value) && value.length === 0) {
+			return true;
+		}
+		if (typeof value === 'object' && Object.keys(value).length === 0) {
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Get field information (label, type, etc.) from the form
+	 * Handles special field name patterns ([], ::, :, etc.)
+	 */
+	getFieldInfo(form, fieldName) {
+		// Try to find label by 'for' attribute (exact match)
+		let label = form.querySelector(`label[for="${fieldName}"]`);
+		let input = null;
+		let fieldWrapper = null;
+
+		// Try to find the input field - check multiple patterns
+		if (!input) {
+			// Try exact match first
+			input = form.querySelector(`[name="${fieldName}"]`);
+		}
+
+		if (!input) {
+			// Try with [] suffix (for checkboxes, multi-selects)
+			input = form.querySelector(`[name="${fieldName}[]"]`);
+		}
+
+		if (!input) {
+			// Try as fieldset legend (for checkbox/radio groups)
+			const fieldset = form.querySelector(`fieldset[data-field="${fieldName}"]`);
+			if (fieldset) {
+				label = fieldset.querySelector('legend');
+				input = fieldset.querySelector('input, select, textarea');
+			}
+		}
+
+		// Get label from input if not found yet
+		if (!label && input) {
+			// Try closest field wrapper first
+			const field = input.closest('.field, fieldset');
+			if (field) {
+				label = field.querySelector('label, legend');
+			}
+		}
+
+		// Get field wrapper - always use base name (no special characters)
+		fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
+
+		// Determine field type
+		let fieldType = 'text';
+		if (fieldWrapper?.dataset.type) {
+			fieldType = fieldWrapper.dataset.type;
+		} else if (input) {
+			// Infer from input type
+			if (input.type === 'checkbox' && input.name.endsWith('[]')) {
+				fieldType = 'checkbox'; // checkbox group
+			} else if (input.type === 'checkbox') {
+				fieldType = 'true_false'; // single checkbox
+			} else if (input.tagName === 'SELECT' && input.multiple) {
+				fieldType = 'select'; // multi-select
+			} else {
+				fieldType = input.type || 'text';
+			}
+		}
+
+		return {
+			label: label?.textContent.replace('*', '').trim() || null,
+			type: fieldType,
+			wrapper: fieldWrapper,
+			input: input
+		};
+	}
+
+	/**
+	 * Create a result element for a field
+	 */
+	createResultElement(template, fieldInfo, value, form) {
+		const resultEl = template.cloneNode(true);
+		const titleEl = resultEl.querySelector('h4');
+		const valueEl = resultEl.querySelector('p');
+
+		// Set label
+		titleEl.textContent = fieldInfo.label;
+
+		// Format value based on field type
+		const formattedValue = this.formatFieldValue(value, fieldInfo.type, form);
+
+		// Determine how to set the value
+		if (this.isHtmlContent(formattedValue)) {
+			// HTML content - use innerHTML
+			valueEl.innerHTML = formattedValue;
+		} else {
+			// Plain text - use textContent for safety
+			valueEl.textContent = formattedValue;
+		}
+
+		return resultEl;
+	}
+
+	/**
+	 * Check if content should be treated as HTML
+	 */
+	isHtmlContent(content) {
+		return typeof content === 'string' && (
+			content.includes('<br>') ||
+			content.includes('<p>') ||
+			content.includes('<ul>') ||
+			content.includes('<ol>') ||
+			content.includes('<a ') ||
+			content.includes('<strong>') ||
+			content.includes('<em>') ||
+			content.includes('<div')
+		);
+	}
+
+	/**
+	 * Format field value based on type
+	 */
+	formatFieldValue(value, type, form) {
+		switch (type) {
+			case 'textarea':
+			case 'wysiwyg':
+				// Handle rich text - check if it's actual HTML content from Quill
+				return this.formatTextareaValue(value, type);
+
+			case 'true_false':
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
+			case 'checkbox':
+				// Handle both single checkbox and checkbox groups
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
+
+			case 'select':
+				// Handle both single and multi-select
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				// Get label from select option
+				return this.getSelectLabel(value, form, type);
+			case 'date':
+			case 'datetime':
+			case 'time':
+				return window.formatDate ? window.formatDate(value) : value;
+
+			case 'radio':
+				// Get label from select option or radio label
+				return this.getSelectLabel(value, form, type);
+
+			case 'repeater':
+				return this.formatRepeaterValue(value);
+
+			case 'group':
+				return this.formatGroupValue(value);
+
+			case 'location':
+				return this.formatLocationValue(value);
+
+			case 'file':
+			case 'image':
+				return this.formatFileValue(value);
+
+			case 'number':
+				return this.formatNumber(value);
+
+			case 'email':
+				return `<a href="mailto:${value}">${value}</a>`;
+
+			case 'url':
+				return `<a href="${value}" target="_blank" rel="noopener">${value}</a>`;
+
+			case 'phone':
+				return `<a href="tel:${value.replace(/\D/g, '')}">${value}</a>`;
+
+			default:
+				// Handle arrays (multi-select, checkbox group)
+				if (Array.isArray(value)) {
+					return this.formatArrayValue(value);
+				}
+				return value;
+		}
+	}
+
+	/**
+	 * Format repeater field value
+	 */
+	formatRepeaterValue(rows) {
+		if (!Array.isArray(rows) || rows.length === 0) {
+			return '<em>No entries</em>';
+		}
+
+		let html = '<div class="repeater-summary">';
+		rows.forEach((row, index) => {
+			html += `<div class="repeater-row">`;
+			html += `<strong>Entry ${index + 1}:</strong><ul>`;
+			for (const [key, value] of Object.entries(row)) {
+				if (!this.isEmptyValue(value)) {
+					const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+					html += `<li><strong>${label}:</strong> ${value}</li>`;
+				}
+			}
+			html += `</ul></div>`;
+		});
+		html += '</div>';
+		return html;
+	}
+
+	/**
+	 * Format group field value
+	 */
+	formatGroupValue(groupData) {
+		if (typeof groupData !== 'object' || Object.keys(groupData).length === 0) {
+			return '<em>No data</em>';
+		}
+
+		let html = '<div class="group-summary"><ul>';
+		for (const [key, value] of Object.entries(groupData)) {
+			if (!this.isEmptyValue(value)) {
+				const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+				// Handle nested groups
+				if (typeof value === 'object' && !Array.isArray(value)) {
+					html += `<li><strong>${label}:</strong> ${this.formatGroupValue(value)}</li>`;
+				} else {
+					html += `<li><strong>${label}:</strong> ${value}</li>`;
+				}
+			}
+		}
+		html += '</ul></div>';
+		return html;
+	}
+
+	/**
+	 * Format location field value
+	 */
+	formatLocationValue(location) {
+		if (typeof location !== 'object') return location;
+
+		const parts = [];
+		const fields = ['address', 'city', 'state', 'zip', 'country'];
+
+		fields.forEach(field => {
+			if (location[field]) {
+				parts.push(location[field]);
+			}
+		});
+
+		return parts.join(', ');
+	}
+
+	/**
+	 * Format file/image value
+	 */
+	formatFileValue(value) {
+		if (typeof value === 'string') {
+			// Single file - could be URL or filename
+			if (value.startsWith('http')) {
+				return `<a href="${value}" target="_blank">View file</a>`;
+			}
+			return value;
+		}
+
+		if (Array.isArray(value)) {
+			return value.map(file => {
+				if (typeof file === 'string') {
+					return `<a href="${file}" target="_blank">View file</a>`;
+				}
+				return file.name || 'File';
+			}).join(', ');
+		}
+
+		return 'File uploaded';
+	}
+
+	/**
+	 * Format number with proper locale formatting
+	 */
+	formatNumber(value) {
+		const num = parseFloat(value);
+		if (isNaN(num)) return value;
+
+		// Check if it's likely currency (has 2 decimal places)
+		if (value.toString().includes('.') && value.toString().split('.')[1].length === 2) {
+			return new Intl.NumberFormat('en-CA', {
+				style: 'currency',
+				currency: 'USD'
+			}).format(num);
+		}
+
+		return new Intl.NumberFormat('en-CA').format(num);
+	}
+
+	/**
+	 * Format array values (checkboxes, multi-select)
+	 */
+	/**
+	 * Format array values (checkboxes, multi-select)
+	 */
+	formatArrayValue(arr, form = null, fieldInfo = null) {
+		if (arr.length === 0) return '<em>None selected</em>';
+
+		// If we have field info, try to get proper labels
+		if (form && fieldInfo && fieldInfo.input) {
+			const labeled = arr.map(val => {
+				return this.getSelectLabel(val, form, fieldInfo.type);
+			});
+			return '<ul><li>' + labeled.join('</li><li>') + '</li></ul>';
+		}
+
+		// Fallback to raw values
+		return '<ul><li>' + arr.join('</li><li>') + '</li></ul>';
+	}
+
+	/**
+	 * Get label for select/radio option
+	 */
+	/**
+	 * Get label for select/radio/checkbox option
+	 */
+	getSelectLabel(value, form, type) {
+		if (type === 'select') {
+			const option = form.querySelector(`option[value="${value}"]`);
+			return option?.textContent || value;
+		}
+
+		if (type === 'radio') {
+			const radio = form.querySelector(`input[type="radio"][value="${value}"]`);
+			const label = radio?.nextElementSibling;
+			return label?.textContent || value;
+		}
+
+		if (type === 'checkbox') {
+			// Try to find the checkbox with this value
+			const checkbox = form.querySelector(`input[type="checkbox"][value="${value}"]`);
+			if (checkbox) {
+				// Look for associated label
+				const label = form.querySelector(`label[for="${checkbox.id}"]`);
+				if (label) {
+					return label.textContent.trim();
+				}
+				// Try next sibling
+				const nextLabel = checkbox.nextElementSibling;
+				if (nextLabel?.tagName === 'LABEL') {
+					return nextLabel.textContent.trim();
+				}
+			}
+		}
+
+		return value;
+	}
+
+	/**
+	 * Format textarea value - handles both rich text and plain text
+	 */
+	formatTextareaValue(value, type) {
+		if (!value) return '<em>Empty</em>';
+
+		// If it's explicitly a wysiwyg type or contains HTML tags, use as-is
+		if (type === 'wysiwyg' || this.containsHtml(value)) {
+			// Quill content already has proper HTML structure
+			return value;
+		}
+
+		// Plain textarea - preserve formatting
+		return this.formatPlainText(value);
+	}
+
+	/**
+	 * Check if string contains HTML content (more reliable than just checking for '<')
+	 */
+	containsHtml(str) {
+		// Check for common HTML tags that Quill uses
+		const htmlPattern = /<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i;
+		return htmlPattern.test(str);
+	}
+
+	/**
+	 * Format plain text content - preserves whitespace and converts newlines
+	 */
+	formatPlainText(text) {
+		if (!text) return '';
+
+		// First, escape any HTML entities that might be in the text
+		text = text
+			.replace(/&/g, '&amp;')
+			.replace(/</g, '&lt;')
+			.replace(/>/g, '&gt;');
+
+		// Convert double newlines to paragraphs for better readability
+		const paragraphs = text.split(/\n\n+/);
+
+		if (paragraphs.length > 1) {
+			// Multiple paragraphs
+			return paragraphs
+				.map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`)
+				.join('');
+		}
+
+		// Single paragraph - just convert newlines to breaks
+		return text.replace(/\n/g, '<br>');
+	}
+
+	/**
+	 * Convert newlines to <br> tags (kept for backwards compatibility)
+	 */
+	nl2br(text) {
+		return this.formatPlainText(text);
+	}
+
 	/**
 	 * Event system
 	 */
@@ -1010,11 +2084,17 @@
 	destroy() {
 		// Remove global handlers
 		if (this.globalHandlersAdded) {
-			document.removeEventListener('submit', this.submitHandler);
 			document.removeEventListener('change', this.changeHandler);
 			document.removeEventListener('focus', this.focusHandler, true);
 			document.removeEventListener('blur', this.blurHandler, true);
+			document.removeEventListener('input', this.inputHandler, true);
 		}
+		this.forms.forEach((formConfig) => {
+			let element = formConfig.element;
+			if (element) {
+				element.removeEventListener('submit', this.submitHandler);
+			}
+		});
 
 		// Clear maps
 		this.specialFields.clear();
diff --git a/assets/js/dash/SquareCheckout.js b/assets/js/dash/SquareCheckout.js
index 3fc9a3c..72d033e 100644
--- a/assets/js/dash/SquareCheckout.js
+++ b/assets/js/dash/SquareCheckout.js
@@ -1,50 +1,50 @@
 class SquareCheckout {
 	constructor(config = {}) {
+		this.config = {
+			...squareConfig,
+			...config
+		};
 
-		this.checkout = document.querySelector('aside#cart');
-		if (!this.checkout) {
-			return;
-		}
+		this.payments 		= null;
+		this.card 			= null;
+		this.isInitialized 	= false;
+		this.cartItems 		= new Map();
+		this.checkout 		= document.querySelector('aside#cart');
 
-		this.config = Object.assign({
-			application_id: squareConfig.application_id,
-			location_id: squareConfig.location_id,
-			api_url: squareConfig.api_url,
-			nonce: squareConfig.nonce,
-			currency: squareConfig.currency || 'CAD'
-		}, config);
-
-
-		this.stepMultiplier = 1;
+		this.isOpen = this.config.isOpen !== '1' || false;
+		//User Context
+		this.isLoggedIn 	= this.config.is_logged_in|| false;
+		this.userEmail 		= this.config.user_email || '';
+		this.savedCards		= [];
+		this.selectedCardId	= null;
+		this.cartId = null;
 
 		this.cache = new window.jvbCache('cart', {TTL: 8.64e+7});
 		this.a11y = window.jvbA11y;
-
 		this.initCart();
 
+		if (this.checkout) {
+			this.initElements();
+			this.init();
+			this.initListeners();
 
-		this.payments = null;
-		this.card = null;
-		this.isInitialized = false;
+			if (this.isLoggedIn) {
+				this.loadSavedCards();
+			}
+		}
 
+		this.stepMultiplier = 1;
 
-		this.clickHandler = this.handleClick.bind(this);
-		this.keyHandler = this.handleEscape.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-
-
-		this.initElements();
-		this.bindEvents();
-
+		//Handle the opening and closing of the checkout window
 		this.popup = new window.jvbPopup({
 			popup: this.checkout,
 			toggle: this.toggle,
 			name: 'Cart',
 			onOpen: this.maybeAddEmptyState.bind(this),
 		});
-		this.init();
 
-		this.toggle.hidden = false;
+		console.log(this.popup);
+		// this.toggle.hidden = false;
 	}
 
 	async initCart() {
@@ -54,6 +54,7 @@
 			this.notifyRestoredCart();
 		}
 	}
+
 	handleClick(e) {
 		if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) {
 			let quantity = window.targetCheck(e, 'div.quantity');
@@ -347,7 +348,7 @@
 	initElements() {
 		this.toggle = document.querySelector('.toggle-cart');
 
-		if (squareConfig.isOpen !== '1') {
+		if (!this.isOpen) {
 			this.toggle.disabled = true;
 			this.toggle.title = 'Currently closed for online ordering';
 		}
@@ -366,9 +367,12 @@
 		console.log('Initialized Checkout');
 	}
 
-	bindEvents() {
-		this.checkoutForm.addEventListener('submit', (e) => this.handleFormSubmit(e));
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.keyHandler = this.handleEscape.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
 
+		this.checkoutForm.addEventListener('submit', (e) => this.handleFormSubmit(e));
 		document.addEventListener('click', this.clickHandler);
 		document.addEventListener('change', this.changeHandler);
 	}
@@ -382,12 +386,18 @@
 				style: this.getCardStyle()
 			});
 			await this.card.attach('#square-card-container');
+			this.card.addEventListener('cardBrandChanged', (event) => {
+				console.log('Card brand:', event.detail.cardBrand);
+				// You could show card brand icon here
+			});
 		} catch (error) {
 			console.error('Failed to initialize card:', error);
 			throw error;
 		}
 	}
 
+
+
 	getCardStyle() {
 		return {
 			input: {
@@ -401,7 +411,10 @@
 				borderRadius: '4px'
 			},
 			'.input-container.is-focus': {
-				borderColor: '#007cba'
+				borderColor: '#006AFF',
+				borderWidth: '2px',
+				outline: '2px solid #006AFF',
+				outlineOffset: '2px'
 			},
 			'.input-container.is-error': {
 				borderColor: '#d63638'
@@ -410,7 +423,7 @@
 	}
 
 	async handleFormSubmit(event) {
-		if (squareConfig.isOpen !== '1') {
+		if (!this.isOpen) {
 			return;
 		}
 		event.preventDefault();
@@ -435,23 +448,23 @@
 	}
 
 	extractOrderData(form) {
-		// Convert cart items Map to array with proper structure
 		const items = Array.from(this.cartItems.values()).map(item => ({
-			post_id: item.post_id,
-			quantity: item.quantity,
+			catalog_object_id: item.square_catalog_id,
+			quantity: String(item.quantity),
 			price: item.price,
-			name: item.name
+			note: item.note || ''
 		}));
 
 		const total = items.reduce((sum, item) =>
 			sum + (item.price * item.quantity), 0
 		);
 
+		// Pre-fill customer info if logged in
 		return {
-			total: total * 100, // Square expects amount in cents
+			total: Math.round(total * 100),
 			items: items,
 			customer: {
-				email: form.querySelector('[name="email"]')?.value || '',
+				email: this.isLoggedIn ? this.userEmail : (form.querySelector('[name="email"]')?.value || ''),
 				name: form.querySelector('[name="name"]')?.value || '',
 				phone: form.querySelector('[name="phone"]')?.value || ''
 			},
@@ -462,52 +475,101 @@
 
 	async processPayment(orderData) {
 		try {
-			const result = await this.card.tokenize();
+			let sourceToken = null;
 
-			if (result.status === 'OK') {
-				return await this.submitToServer(result.token, orderData);
+			// Check if using saved card or new card
+			if (this.selectedCardId) {
+				// Use saved card
+				sourceToken = this.selectedCardId;
 			} else {
-				throw new Error('Card tokenization failed: ' + (result.errors?.join(', ') || 'Unknown error'));
+				// Tokenize new card
+				const tokenResult = await this.card.tokenize({
+					verificationDetails: {
+						amount: String(orderData.total),
+						currencyCode: this.config.currency || 'CAD',
+						intent: 'CHARGE',
+						customerInitiated: true,
+						billingContact: {
+							givenName: orderData.customer.name.split(' ')[0],
+							familyName: orderData.customer.name.split(' ').slice(1).join(' '),
+							email: orderData.customer.email,
+							phone: orderData.customer.phone,
+							addressLines: [form.querySelector('[name="address"]')?.value || ''],
+							city: form.querySelector('[name="city"]')?.value || '',
+							state: form.querySelector('[name="state"]')?.value || '',
+							postalCode: form.querySelector('[name="postal_code"]')?.value || '',
+							countryCode: 'CA' // or 'US'
+						}
+					}
+				});
+
+				if (tokenResult.status !== 'OK') {
+					const errors = tokenResult.errors?.map(e => e.message).join(', ') || 'Unknown error';
+					throw new Error(`Card tokenization failed: ${errors}`);
+				}
+
+				sourceToken = tokenResult.token;
+				if (tokenResult.details?.userChallenged) {
+					console.log('3D Secure verification completed');
+				}
 			}
+
+			// Send to server
+			return await this.submitToServer(sourceToken, orderData, !!this.selectedCardId);
+
 		} catch (error) {
 			console.error('Payment processing failed:', error);
 			throw error;
 		}
 	}
 
-	async submitToServer(token, orderData) {
-		if (squareConfig.isOpen !== '1') {
-			return;
+	async submitToServer(sourceToken, orderData, isSavedCard = false) {
+		if (!this.isOpen) {
+			throw new Error('Store is currently closed');
 		}
 
-		// Square Web Payments SDK handles EVERYTHING
-		// We just need to track the order for status updates
-		const response = await fetch(this.config.api_url + 'save-order', {
+		const response = await fetch(this.config.api_url + 'process-payment', {
 			method: 'POST',
 			headers: {
 				'Content-Type': 'application/json',
 				'X-WP-Nonce': this.config.nonce
 			},
 			body: JSON.stringify({
-				order_id: token.orderId,  // From Square SDK response
-				payment_id: token.paymentId,  // From Square SDK response
-				customer: orderData.customer,
+				source_id: sourceToken,
+				is_saved_card: isSavedCard,
+				cart_id: this.getCartId(),
+				amount: orderData.total,
 				items: orderData.items,
-				action: 'jvb_integration_action',
-				service: 'square',
-				integration_action: 'save_order'
+				customer: {
+					email: this.isLoggedIn ? this.userEmail : orderData.customer.email,
+					name: orderData.customer.name,
+					phone: orderData.customer.phone
+				},
+				note: orderData.note,
+				pickup_time: orderData.pickup_time
 			})
 		});
 
 		const result = await response.json();
 
 		if (!response.ok) {
-			throw new Error(result.message || 'Failed to save order');
+			throw new Error(result.message || 'Payment processing failed');
 		}
 
+		this.clearCart();
+
 		return result;
 	}
 
+	getCartId() {
+		// Generate once per cart session
+		if (!this.cartId) {
+			this.cartId = crypto.randomUUID();
+			this.cache.set('cart_id', this.cartId);
+		}
+		return this.cartId;
+	}
+
 	trackOrder(orderNum) {
 		this.orderId = orderNum;
 		this.scheduleOrderCheck();
@@ -545,43 +607,75 @@
 	/**************************************************************
 	 * Customer Data
 	**************************************************************/
-	async loadCustomerProfile(email) {
-		const response = await fetch('/wp-json/jvb/v1/square/customer', {
-			method: 'POST',
-			headers: {
-				'Content-Type': 'application/json',
-				'X-WP-Nonce': this.config.nonce
-			},
-			body: JSON.stringify({ email })
-		});
+	/**
+	 * Load saved cards for logged-in user
+	 */
+	async loadSavedCards() {
+		try {
+			const response = await fetch(this.config.api_url + 'saved-cards', {
+				method: 'GET',
+				headers: {
+					'X-WP-Nonce': this.config.nonce
+				}
+			});
 
-		const profile = await response.json();
+			const result = await response.json();
 
-		if (profile) {
-			this.displaySavedCards(profile.cards);
-			this.fillCustomerInfo(profile.customer);
+			if (result.success && result.cards) {
+				this.savedCards = result.cards;
+				this.renderSavedCards();
+			}
+		} catch (error) {
+			console.error('Failed to load saved cards:', error);
 		}
 	}
 
-	displaySavedCards(cards) {
+	/**
+	 * Render saved cards in the checkout form
+	 */
+	renderSavedCards() {
 		const container = document.getElementById('saved-cards');
-		if (!cards.length) return;
+		if (!container || this.savedCards.length === 0) {
+			return;
+		}
 
-		container.innerHTML = `
-            <h3>Saved Payment Methods</h3>
-            ${cards.map(card => `
-                <label>
-                    <input type="radio" name="payment_method" value="${card.id}">
-                    •••• ${card.last_4} (${card.card_brand})
+		const html = `
+            <div class="saved-cards-section">
+                <h4>Saved Payment Methods</h4>
+                ${this.savedCards.map(card => `
+                    <label class="saved-card">
+                        <input type="radio" name="payment-method" value="saved" data-card-id="${card.id}">
+                        <span class="card-info">
+                            <strong>${card.card_brand}</strong> ending in ${card.last_4}
+                            <small>Exp: ${card.exp_month}/${card.exp_year}</small>
+                        </span>
+                    </label>
+                `).join('')}
+                <label class="saved-card">
+                    <input type="radio" name="payment-method" value="new" checked>
+                    <span>Use a new card</span>
                 </label>
-            `).join('')}
-            <label>
-                <input type="radio" name="payment_method" value="new" checked>
-                Use new card
-            </label>
+            </div>
         `;
+
+		container.innerHTML = html;
+
+		// Listen for payment method selection
+		container.querySelectorAll('input[name="payment-method"]').forEach(radio => {
+			radio.addEventListener('change', (e) => {
+				const useNewCard = e.target.value === 'new';
+				const cardContainer = document.getElementById('square-card-container');
+
+				if (cardContainer) {
+					cardContainer.style.display = useNewCard ? 'block' : 'none';
+				}
+
+				this.selectedCardId = useNewCard ? null : e.target.dataset.cardId;
+			});
+		});
 	}
 
+
 	handleSuccess(result, form) {
 		// Trigger success event
 		document.dispatchEvent(new CustomEvent('squareCheckoutSuccess', {
diff --git a/assets/js/dash/UtilityFunctions.js b/assets/js/dash/UtilityFunctions.js
index ec64aac..b682acc 100644
--- a/assets/js/dash/UtilityFunctions.js
+++ b/assets/js/dash/UtilityFunctions.js
@@ -420,7 +420,22 @@
 window.showToast = function(message, type='success', actions={}){
 	window.jvbNotifications.showToast(message, type, actions);
 }
+window.dateFormatter = new Intl.DateTimeFormat('en-CA', {
+		year: 'numeric',
+		month: 'long',
+		day: 'numeric',
+		hour: '2-digit',
+		minute: '2-digit',
+		second: '2-digit',
+		timeZoneName: 'short'
+	});
 
+window.formatDate = function(date) {
+	if (!(date instanceof Date && !isNaN(date))) {
+		date = new Date(date);
+	}
+	return window.dateFormatter.format(date);
+}
 
 /**
  * Outputs the set text as if a typewriter were writing it
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index 59ffea1..ff139e4 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.store=new window.jvbStore({name:"forms",storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:6048e5}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.init()}async init(){await this.checkPendingOperations(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners()}handleStoreEvent(e,t){if("item-saved"===e)"autosave"===t.item.status&&this.showFormStatus(t.item.formId,"autosave")}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const s=document.createElement("div");s.className="pending-changes-notification",s.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(s,t.firstChild),s.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),s.remove()})),s.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),s.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("submit",this.submitHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),this.globalHandlersAdded=!0)}registerForm(e,t={}){const s=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=s;const a={element:e,id:s,options:{autoSave:!0,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e),isDirty:!1};if(this.initializeFormFields(e,a),this.forms.set(s,a),this.store&&a.options.cache){const e=this.store.getForm(s);e&&e.formData&&this.showPendingNotification(e)}return a}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")&&new window.jvbTabs(e),window.jvbSelector&&window.jvbSelector.scanExistingFields()}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const s=e.querySelector(".add-repeater-row"),a=e.querySelector(".repeater-items"),i=e.querySelector("template");s&&i&&a&&(window.Sortable&&new Sortable(a,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),s.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),a.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"),a=e.querySelector("template"),i=s.children.length,r=e.dataset.field,n=a.content.cloneNode(!0).firstElementChild;n.dataset.index=i,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${r}:${i}:${t}`,e.id=`${r}-${i}-${t}`;const s=e.nextElementSibling;s&&"LABEL"===s.tagName&&(s.htmlFor=e.id)})),s.appendChild(n),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:r,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const s=e.closest(".repeater"),a=s.dataset.field;e.remove(),this.updateRepeaterOrder(s,t),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:a,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const s=e.querySelector(".repeater-items"),a=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 i=s[2];e.name=`${a}:${t}:${i}`,e.id=`${a}-${t}-${i}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)}}))})),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:a,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((s=>{const a=s.dataset.dependsOn,i=s.dataset.dependsValue,r=s.dataset.dependsOperator||"==";t.dependencies.has(a)||t.dependencies.set(a,[]),t.dependencies.get(a).push({field:s,requiredValue:i,operator:r}),this.checkFieldDependency(e,s,a,i,r)}))}checkFieldDependency(e,t,s,a,i){const r=e.querySelector(`[name="${s}"]`);if(!r)return;const n=this.getFieldValue(r),o=this.evaluateCondition(n,a,i);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,s){const a=String(e||""),i=String(t||"");switch(s){case"==":default:return a==i;case"!=":return a!=i;case">":return parseFloat(a)>parseFloat(i);case"<":return parseFloat(a)<parseFloat(i);case">=":return parseFloat(a)>=parseFloat(i);case"<=":return parseFloat(a)<=parseFloat(i);case"contains":return a.includes(i);case"empty":return""===a;case"not_empty":return""!==a}}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 a=s?.querySelector(".char-count");!a&&s&&(a=document.createElement("div"),a.className="char-count",a.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,s.appendChild(a));const i=()=>{const s=e.value.length;a&&(a.querySelector(".current").textContent=s,a.classList.toggle("exceeded",s>t)),s>t&&(e.value=e.value.substring(0,t),a&&(a.querySelector(".current").textContent=t))};e.addEventListener("input",i),i()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}handleSubmit(e){if(this.subscribers.size>0){const t=e.target;if(!t.dataset.formId)return;this.store.delete(t.dataset.formId),e.preventDefault();const s=this.forms.get(t.dataset.formId);if(!s)return;const a=this.collectFormData(t);e.preventDefault(),this.notify("form-submit",{formId:s.id,data:a,config:s})}}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}}handleNumberClick(e,t){let s=0;if(e.target.closest(".increase")?s+=1:e.target.closest(".decrease")&&(s-=1),0!==s){let a=parseFloat(t.step);a=Math.max(a,1),e.ctrlKey&&e.shiftKey?a*=50:e.ctrlKey?a*=5:e.shiftKey&&(a*=10);let i=""===t.value?0:parseFloat(t.value);t.value=i+a*s,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,s,a,i]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],r=parseFloat(e.value);r<t?(e.value=t,i.disabled=!0):r>s?(e.value=s,a.disabled=!1):a.disabled?a.disabled=!1:i.disabled&&(i.disabled=!1)}handleChange(e){if(this.subscribers.size>0){const t=e.target,s=t.form||t.closest("form");if(!s)return;const a=this.forms?.get(s.dataset.formId);if(!a)return;const i=a.dependencies.get(t.name);if(i&&i.forEach((e=>{this.checkFieldDependency(s,e.field,t.name,e.requiredValue,e.operator)})),a.options.autoSave&&!s.dataset.noautosave){const e=this.getDelayForField(t);this.scheduleSave(a,e)}}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){const t=e.target,s=t.form||t.closest("form");if(!s)return;const a=this.forms?.get(s.dataset.formId);a&&a.options.autoSave&&!s.dataset.noautosave&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){document.addEventListener("input",this.handleInput,{passive:!0});const s=`autosave_${e.id}`;this.debouncer.schedule(s,(()=>this.autosave(e)),t)}handleInput(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);await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}),this.showFormStatus(e.id,"saved");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,a]of Object.entries(t))"object"==typeof a&&(s[e]=a);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),a=this.getChangedFields(t.lastSnapshot,s);return Object.keys(a).length>0}showFormStatus(e,t){const s=e.querySelector(".form-status");s&&s.remove();const a=document.createElement("div");a.className=`form-status status-${t}`;a.textContent={saving:"Saving changes...",saved:"Changes saved",error:"Failed to save changes",offline:"Changes will be saved when online"}[t]||t,e.insertBefore(a,e.firstChild),"saved"===t&&setTimeout((()=>a.remove()),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){const t=new FormData(e);let s={};const a={},i={};for(let[r,n]of t.entries()){if(this.ignore.includes(r)||r.endsWith("_temp"))continue;this.getFieldProcessor(r)(r,n,s,a,i,e)}return window.isEmptyObject(i)?this.mergeRepeaterData(s,a):(s=this.mergeRepeaterData(s,a),this.mergePostData(s,i))}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:e.includes("[")?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((s=>{const a={};Object.keys(t[s]).forEach((e=>{const i=t[s][e];Object.keys(i).length>0&&(a[e]=i)})),e[s]=Object.values(a)})),e}mergePostData(e,t){for(let[t,s]in Object.entries(s))e[t]=s;return e}processTableField(e,t,s,a,i,r){let[n,o]=e.split("|");!n in i&&(i[n]={});this.getFieldProcessor(o)(o,t,i,a,i,r)}processRepeaterField(e,t,s,a,i,r){let[n,o,d]=e.split(":");const l=d.endsWith("[]");d=d.replace("[]",""),a[n]||(a[n]={}),a[n][o]||(a[n][o]={}),l||a[n][o][d]?(a[n][o][d]?Array.isArray(a[n][o][d])||(a[n][o][d]=[a[n][o][d]]):a[n][o][d]=[],a[n][o][d].push(t)):a[n][o][d]=t}processGroupField(e,t,s,a,i,r){const n=e.split("::"),o=n[0];s[o]||(s[o]={});let d=s[o];for(let e=1;e<n.length-1;e++){const t=n[e];d[t]||(d[t]={}),d=d[t]}const l=n[n.length-1];void 0!==d[l]?(Array.isArray(d[l])||(d[l]=[d[l]]),d[l].push(t)):d[l]=t}processLocationField(e,t,s,a,i,r){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,a,i,r){s[e]?(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)||{}}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("submit",this.submitHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0)),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(){this.store=new window.jvbStore({name:"forms",storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:6048e5}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.init()}async init(){await this.checkPendingOperations(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners()}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){(await this.store.query("status","draft")).forEach((e=>{let t=this.forms.get(e.formId);t&&t.element&&(t.element.querySelector(".restore-form").hidden=!1,new this.populateForm(t.element,e.data))}))}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(r,t.firstChild),r.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),r.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,options:{autoSave:!0,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e),isDirty:!1};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.formData&&this.showPendingNotification(e)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields()}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=s.switchTab.bind(s);s.switchTab=(e,t)=>{c(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s==a;case"!=":return s!=a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}handleSubmit(e){if(this.subscribers.size>0){const t=e.target;if(!t.dataset.formId)return;e.preventDefault();const r=this.forms.get(t.dataset.formId);if(!r)return;const s=this.collectFormData(t);this.notify("form-submit",{formId:r.id,data:s,config:r})}}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]");switch(t=t.dataset.action,t){case"clear-form":let t=e.target.closest("form");this.store.delete(t.dataset.formId),t?.reset(),e.target.closest(".restore-form").hidden=!0;break;case"dismiss-restore":e.target.closest(".restore-form").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(this.subscribers.size>0){const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(!s)return;const a=s.dependencies.get(t.name);if(a&&a.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)})),s.options.autoSave&&!r.dataset.noautosave){const e=this.getDelayForField(t);this.scheduleSave(s,e)}}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&a.options.autoSave&&!r.dataset.noautosave&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,((e,t)=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with http:// or https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),!0}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}showSuccess(e){if(!e)return;const t=e.querySelector(".validation-icon.success"),r=e.querySelector(".validation-icon.error"),s=e.querySelector(".validation-message"),a=e.querySelector("input, textarea, select");e.classList.remove("has-error"),a?.classList.remove("error"),e.classList.add("has-success"),t&&(t.hidden=!1),r&&(r.hidden=!0),s&&(s.hidden=!0,s.textContent="")}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let r=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const s=e.dataset.field;s&&this.touchedFields.add(s);this.validateField(t,e)||(r=!1,!1===r&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),r}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const r=t.target.closest("input, select");if(!r)return;const s=r.name;if(!s)return;e.querySelectorAll(`[data-show-if*="${s}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const r=e.dataset.field,s=e.querySelector(".validation-message");r&&s&&(t[r]=s.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")}));const r=this.getChangedFields(e.data,t);if(0!==Object.keys(r).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.lastSnapshot,r);return Object.keys(s).length>0}showFormStatus(e,t){let r=this.forms.get(e);console.log("Setting status: ",t);const s=r.element.querySelector(".fstatus");s.hidden=!1;const a=s.querySelector(".message");a.textContent="",s.querySelector(".icon")?.remove();const i={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"},n={autosaved:"check",submitted:"check",error:"close",offline:"cloud-slash",pending:"exclamation-mark"};let o=window.getIcon(n[t]);o&&s.prepend(o),console.log(t,i[t]),console.log(t,n[t]),a.textContent=i[t]||t,s.classList.toggle("loading",["uploading","saving"].includes(t)),"submitted"===t&&setTimeout((()=>s.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){const t=new FormData(e);let r={};const s={},a={};for(let[i,n]of t.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;this.getFieldProcessor(i)(i,n,r,s,a,e)}return window.isEmptyObject(a)?this.mergeRepeaterData(r,s):(r=this.mergeRepeaterData(r,s),this.mergePostData(r,a))}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[t,r]in Object.entries(r))e[t]=r;return e}processTableField(e,t,r,s,a,i){let[n,o]=e.split("|");!n in a&&(a[n]={});this.getFieldProcessor(o)(o,t,a,s,a,i)}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),c||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary"),[i,n,o]=[a.querySelector("h2"),a.querySelector(".summary"),a.querySelector(".result")],l=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;const a=this.createResultElement(o,r,t,s);a&&n.appendChild(a)}o.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=null,a=null;if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}createResultElement(e,t,r,s){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(r,t.type,s);return this.isHtmlContent(o)?n.innerHTML=o:n.textContent=o,a}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
\ No newline at end of file
diff --git a/assets/js/min/square.min.js b/assets/js/min/square.min.js
index 8ea9e23..98ae4bc 100644
--- a/assets/js/min/square.min.js
+++ b/assets/js/min/square.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(t={}){this.checkout=document.querySelector("aside#cart"),this.checkout&&(this.config=Object.assign({application_id:squareConfig.application_id,location_id:squareConfig.location_id,api_url:squareConfig.api_url,nonce:squareConfig.nonce,currency:squareConfig.currency||"CAD"},t),this.stepMultiplier=1,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.payments=null,this.card=null,this.isInitialized=!1,this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),this.changeHandler=this.handleChange.bind(this),this.initElements(),this.bindEvents(),this.popup=new window.jvbPopup({popup:this.checkout,toggle:this.toggle,name:"Cart",onOpen:this.maybeAddEmptyState.bind(this)}),this.init(),this.toggle.hidden=!1)}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,console.log("cart",this.cartItems),this.cartItems.size>0&&this.notifyRestoredCart()}handleClick(t){if(window.targetCheck(t,"button")&&window.targetCheck(t,"div.quantity")){let e=window.targetCheck(t,"div.quantity");this.handleNumberClick(t,e)}else if(window.targetCheck(t,"[data-add-to-cart]")){let e=window.targetCheck(t,"[data-add-to-cart]");this.handleAddToCart(e)}else if(window.targetCheck(t,"[data-remove-from-cart]")){let e=window.targetCheck(t,"[data-remove-from-cart]");this.handleRemoveFromCart(e)}else window.targetCheck(t,"[data-clear-cart]")&&this.clearCart()}handleChange(t,e){console.log("Checkout change");let a=window.targetCheck(t,".quantity-input");if(a){let e=t.target.closest(".quantity"),i=a.value;if(window.targetCheck(t,".cart-items")){let t=document.querySelector(`.menu-section [data-id="${e.dataset.id}"] input`);t&&(t.value=a.value)}i>0?this.handleAddToCart(e):this.handleRemoveFromCart(e)}}handleNumberClick(t,e){console.log(e),t.preventDefault();let a=0;if(t.target.closest(".increase")?a+=1:t.target.closest(".decrease")&&(a-=1),0!==a){let[t,i]=[parseInt(e.dataset.step),e.querySelector("input")],r=""===i.value?0:parseInt(i.value);i.value=r+t*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(e)}}handleNumberLimits(t){let[e,a,i,r,s]=[t.dataset.min,t.dataset.max,t.querySelector("input"),t.querySelector(".increase"),t.querySelector(".decrease")],o=parseInt(i.value);o<e?(i.value=e,s.disabled=!0):o>a?(i.value=a,r.disabled=!1):r.disabled?r.disabled=!1:s.disabled&&(s.disabled=!1)}maybeAddEmptyState(){let t=this.itemsList.querySelector(".empty");if(t&&t.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let t=window.getTemplate("emptyCart");this.itemsList.append(t),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}handleEscape(t){"Escape"===t.key?this.stepMultiplier=1:t.ctrlKey&&t.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):t.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(t){let e=t.dataset.id;this.createItemElement(t);let a=parseFloat(t.dataset.price),i=parseInt(t.querySelector(".quantity-input")?.value)??1,r=parseFloat(a*i);this.cartItems.set(e,{post_id:e,name:t.dataset.name,price:a,quantity:i,total:r,square_catalog_id:t.dataset.squareCatalogId}),this.saveCart()}notifyRestoredCart(){let t=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(t,this.itemsList),this.cartItems.forEach((t=>{console.log(t);let e=window.getTemplate("cartItem"),a=e.querySelector(".quantity"),i=t.price,r=t.quantity;[a.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,a.dataset.price,a.dataset.squareCatalogId,e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[t.post_id,t.name,window.formatPrice(i),i,t.square_catalog_id,r,window.formatPrice(r*i)],this.table.append(e)})),this.updateTotal()}handleRemoveFromCart(t){if(confirm("This will remove this item from the cart. Continue?")){t.querySelector("[data-id]")||(t=t.closest(".item")?.querySelector(".quantity.field"));let e=t.dataset.id;this.cartItems.delete(e),this.table.querySelector(`[data-id="${e}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${e}"] input`);a&&(a.value=0),this.maybeAddEmptyState(),this.saveCart()}}clearCart(){this.cartItems.clear(),window.removeChildren(this.table),this.saveCart()}saveCart(){this.updateTotal(),this.cache.set("cart",this.cartItems)}updateTotal(){let t=0;this.cartItems.forEach((e=>{console.log(e),t+=e.total}));let e=.05*t;t=window.formatPrice(t+e),e=window.formatPrice(e),window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,e),window.typeText(this.grandTotal,t),this.totalTax.classList.remove("typeText")}createItemElement(t){let e=this.itemsList.querySelector(`[data-id="${t.dataset.id}"]`),a=!1,i=t.dataset.price,r=t.querySelector('[name="quantity"]')?.value??1;if(e)e=e.closest("tr");else{a=!0,e=window.getTemplate("cartItem");let r=e.querySelector(".quantity");[r.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,r.dataset.price,r.dataset.squareCatalogId]=[t.dataset.id,t.dataset.name,window.formatPrice(i),i,t.dataset.squareCatalogId]}[e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[r,window.formatPrice(r*i)],a&&(e.classList.add("adding"),this.table.append(e),setTimeout((()=>{e.classList.remove("adding")}),500))}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("squareCheckoutReady",{detail:{checkout:this}}))}catch(t){console.error("Failed to initialize Square payments:",t),this.handleError(t)}else console.error("Square Web Payments SDK not loaded")}initElements(){this.toggle=document.querySelector(".toggle-cart"),"1"!==squareConfig.isOpen&&(this.toggle.disabled=!0,this.toggle.title="Currently closed for online ordering"),this.checkoutPanel=this.checkout.querySelector('button[data-tab="checkout"]'),this.itemsList=this.checkout.querySelector(".cart-items"),this.table=this.checkout.querySelector(".cart-items tbody"),this.total=this.checkout.querySelector(".cart-total"),this.totalTax=this.total.querySelector(".tax span"),this.grandTotal=this.total.querySelector(".total span"),this.checkoutForm=this.checkout.querySelector("form"),this.tabs=new window.jvbTabs(this.checkoutForm,{updateURL:!1}),console.log("Initialized Checkout")}bindEvents(){this.checkoutForm.addEventListener("submit",(t=>this.handleFormSubmit(t))),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}async initializePaymentMethods(){if(document.getElementById("square-card-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#square-card-container")}catch(t){throw console.error("Failed to initialize card:",t),t}}getCardStyle(){return{input:{fontSize:"16px",fontFamily:"inherit",color:"#333",backgroundColor:"#fff"},".input-container":{borderColor:"#ccc",borderRadius:"4px"},".input-container.is-focus":{borderColor:"#007cba"},".input-container.is-error":{borderColor:"#d63638"}}}async handleFormSubmit(t){if("1"!==squareConfig.isOpen)return;if(t.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const e=t.target,a=this.extractOrderData(e);try{window.jvbLoading.showLoading("Processing payment...");const t=await this.processPayment(a);this.handleSuccess(t,e)}catch(t){this.handleError(t)}finally{window.jvbLoading.hideLoading()}}extractOrderData(t){const e=Array.from(this.cartItems.values()).map((t=>({post_id:t.post_id,quantity:t.quantity,price:t.price,name:t.name})));return{total:100*e.reduce(((t,e)=>t+e.price*e.quantity),0),items:e,customer:{email:t.querySelector('[name="email"]')?.value||"",name:t.querySelector('[name="name"]')?.value||"",phone:t.querySelector('[name="phone"]')?.value||""},note:t.querySelector('[name="special_instructions"]')?.value||"",pickup_time:t.querySelector('[name="pickup_time"]')?.value||""}}async processPayment(t){try{const e=await this.card.tokenize();if("OK"===e.status)return await this.submitToServer(e.token,t);throw new Error("Card tokenization failed: "+(e.errors?.join(", ")||"Unknown error"))}catch(t){throw console.error("Payment processing failed:",t),t}}async submitToServer(t,e){if("1"!==squareConfig.isOpen)return;const a=await fetch(this.config.api_url+"save-order",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({order_id:t.orderId,payment_id:t.paymentId,customer:e.customer,items:e.items,action:"jvb_integration_action",service:"square",integration_action:"save_order"})}),i=await a.json();if(!a.ok)throw new Error(i.message||"Failed to save order");return i}trackOrder(t){this.orderId=t,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>{this.checkOrderStatus()}),3e4)}async checkOrderStatus(){const t=await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`),e=await t.json();"ready"!==e.status&&this.scheduleOrderCheck(),this.updateOrderStatus(e)}updateOrderStatus(t){this.checkout.querySelectorAll(".status-item").forEach((e=>{e.dataset.status===t.status&&e.classList.add("active")})),this.checkout.querySelector("#eta").textContent=t.eta||"In progress"}async loadCustomerProfile(t){const e=await fetch("/wp-json/jvb/v1/square/customer",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({email:t})}),a=await e.json();a&&(this.displaySavedCards(a.cards),this.fillCustomerInfo(a.customer))}displaySavedCards(t){const e=document.getElementById("saved-cards");t.length&&(e.innerHTML=`\n            <h3>Saved Payment Methods</h3>\n            ${t.map((t=>`\n                <label>\n                    <input type="radio" name="payment_method" value="${t.id}">\n                    •••• ${t.last_4} (${t.card_brand})\n                </label>\n            `)).join("")}\n            <label>\n                <input type="radio" name="payment_method" value="new" checked>\n                Use new card\n            </label>\n        `)}handleSuccess(t,e){document.dispatchEvent(new CustomEvent("squareCheckoutSuccess",{detail:{result:t,form:e}}));const a=e.dataset.successUrl||`/order-confirmation/?order=${t.wp_order_id}`;window.location.href=a}handleError(t){console.error("Square checkout error:",t),document.dispatchEvent(new CustomEvent("squareCheckoutError",{detail:{error:t}})),window.jvbNotifications?.show?.(t.message||"Payment failed","error")}}document.addEventListener("DOMContentLoaded",(()=>{window.squareCheckout=new t}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){this.config={...squareConfig,...e},this.payments=null,this.card=null,this.isInitialized=!1,this.cartItems=new Map,this.checkout=document.querySelector("aside#cart"),this.isOpen="1"!==this.config.isOpen||!1,this.isOpen=!0,this.isLoggedIn=this.config.is_logged_in||!1,this.userEmail=this.config.user_email||"",this.savedCards=[],this.selectedCardId=null,this.cartId=null,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.checkout&&(this.initElements(),this.init(),this.initListeners(),this.isLoggedIn&&this.loadSavedCards()),this.stepMultiplier=1,this.popup=new window.jvbPopup({popup:this.checkout,toggle:this.toggle,name:"Cart",onOpen:this.maybeAddEmptyState.bind(this)}),console.log(this.popup)}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,console.log("cart",this.cartItems),this.cartItems.size>0&&this.notifyRestoredCart()}handleClick(e){if(window.targetCheck(e,"button")&&window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t)}else if(window.targetCheck(e,"[data-add-to-cart]")){let t=window.targetCheck(e,"[data-add-to-cart]");this.handleAddToCart(t)}else if(window.targetCheck(e,"[data-remove-from-cart]")){let t=window.targetCheck(e,"[data-remove-from-cart]");this.handleRemoveFromCart(t)}else window.targetCheck(e,"[data-clear-cart]")&&this.clearCart()}handleChange(e,t){console.log("Checkout change");let a=window.targetCheck(e,".quantity-input");if(a){let t=e.target.closest(".quantity"),i=a.value;if(window.targetCheck(e,".cart-items")){let e=document.querySelector(`.menu-section [data-id="${t.dataset.id}"] input`);e&&(e.value=a.value)}i>0?this.handleAddToCart(t):this.handleRemoveFromCart(t)}}handleNumberClick(e,t){console.log(t),e.preventDefault();let a=0;if(e.target.closest(".increase")?a+=1:e.target.closest(".decrease")&&(a-=1),0!==a){let[e,i]=[parseInt(t.dataset.step),t.querySelector("input")],s=""===i.value?0:parseInt(i.value);i.value=s+e*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,a,i,s,r]=[e.dataset.min,e.dataset.max,e.querySelector("input"),e.querySelector(".increase"),e.querySelector(".decrease")],n=parseInt(i.value);n<t?(i.value=t,r.disabled=!0):n>a?(i.value=a,s.disabled=!1):s.disabled?s.disabled=!1:r.disabled&&(r.disabled=!1)}maybeAddEmptyState(){let e=this.itemsList.querySelector(".empty");if(e&&e.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let e=window.getTemplate("emptyCart");this.itemsList.append(e),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}handleEscape(e){"Escape"===e.key?this.stepMultiplier=1:e.ctrlKey&&e.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):e.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(e){let t=e.dataset.id;this.createItemElement(e);let a=parseFloat(e.dataset.price),i=parseInt(e.querySelector(".quantity-input")?.value)??1,s=parseFloat(a*i);this.cartItems.set(t,{post_id:t,name:e.dataset.name,price:a,quantity:i,total:s,square_catalog_id:e.dataset.squareCatalogId}),this.saveCart()}notifyRestoredCart(){let e=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(e,this.itemsList),this.cartItems.forEach((e=>{console.log(e);let t=window.getTemplate("cartItem"),a=t.querySelector(".quantity"),i=e.price,s=e.quantity;[a.dataset.id,t.querySelector("label").textContent,t.querySelector(".price").textContent,a.dataset.price,a.dataset.squareCatalogId,t.querySelector('[name="quantity"]').value,t.querySelector(".total").textContent]=[e.post_id,e.name,window.formatPrice(i),i,e.square_catalog_id,s,window.formatPrice(s*i)],this.table.append(t)})),this.updateTotal()}handleRemoveFromCart(e){if(confirm("This will remove this item from the cart. Continue?")){e.querySelector("[data-id]")||(e=e.closest(".item")?.querySelector(".quantity.field"));let t=e.dataset.id;this.cartItems.delete(t),this.table.querySelector(`[data-id="${t}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${t}"] input`);a&&(a.value=0),this.maybeAddEmptyState(),this.saveCart()}}clearCart(){this.cartItems.clear(),window.removeChildren(this.table),this.saveCart()}saveCart(){this.updateTotal(),this.cache.set("cart",this.cartItems)}updateTotal(){let e=0;this.cartItems.forEach((t=>{console.log(t),e+=t.total}));let t=.05*e;e=window.formatPrice(e+t),t=window.formatPrice(t),window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,t),window.typeText(this.grandTotal,e),this.totalTax.classList.remove("typeText")}createItemElement(e){let t=this.itemsList.querySelector(`[data-id="${e.dataset.id}"]`),a=!1,i=e.dataset.price,s=e.querySelector('[name="quantity"]')?.value??1;if(t)t=t.closest("tr");else{a=!0,t=window.getTemplate("cartItem");let s=t.querySelector(".quantity");[s.dataset.id,t.querySelector("label").textContent,t.querySelector(".price").textContent,s.dataset.price,s.dataset.squareCatalogId]=[e.dataset.id,e.dataset.name,window.formatPrice(i),i,e.dataset.squareCatalogId]}[t.querySelector('[name="quantity"]').value,t.querySelector(".total").textContent]=[s,window.formatPrice(s*i)],a&&(t.classList.add("adding"),this.table.append(t),setTimeout((()=>{t.classList.remove("adding")}),500))}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("squareCheckoutReady",{detail:{checkout:this}}))}catch(e){console.error("Failed to initialize Square payments:",e),this.handleError(e)}else console.error("Square Web Payments SDK not loaded")}initElements(){this.toggle=document.querySelector(".toggle-cart"),this.isOpen||(this.toggle.disabled=!0,this.toggle.title="Currently closed for online ordering"),this.checkoutPanel=this.checkout.querySelector('button[data-tab="checkout"]'),this.itemsList=this.checkout.querySelector(".cart-items"),this.table=this.checkout.querySelector(".cart-items tbody"),this.total=this.checkout.querySelector(".cart-total"),this.totalTax=this.total.querySelector(".tax span"),this.grandTotal=this.total.querySelector(".total span"),this.checkoutForm=this.checkout.querySelector("form"),this.tabs=new window.jvbTabs(this.checkoutForm,{updateURL:!1}),console.log("Initialized Checkout")}initListeners(){this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),this.changeHandler=this.handleChange.bind(this),this.checkoutForm.addEventListener("submit",(e=>this.handleFormSubmit(e))),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}async initializePaymentMethods(){if(document.getElementById("square-card-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#square-card-container"),this.card.addEventListener("cardBrandChanged",(e=>{console.log("Card brand:",e.detail.cardBrand)}))}catch(e){throw console.error("Failed to initialize card:",e),e}}getCardStyle(){return{input:{fontSize:"16px",fontFamily:"inherit",color:"#333",backgroundColor:"#fff"},".input-container":{borderColor:"#ccc",borderRadius:"4px"},".input-container.is-focus":{borderColor:"#006AFF",borderWidth:"2px",outline:"2px solid #006AFF",outlineOffset:"2px"},".input-container.is-error":{borderColor:"#d63638"}}}async handleFormSubmit(e){if(!this.isOpen)return;if(e.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const t=e.target,a=this.extractOrderData(t);try{window.jvbLoading.showLoading("Processing payment...");const e=await this.processPayment(a);this.handleSuccess(e,t)}catch(e){this.handleError(e)}finally{window.jvbLoading.hideLoading()}}extractOrderData(e){const t=Array.from(this.cartItems.values()).map((e=>({catalog_object_id:e.square_catalog_id,quantity:String(e.quantity),price:e.price,note:e.note||""}))),a=t.reduce(((e,t)=>e+t.price*t.quantity),0);return{total:Math.round(100*a),items:t,customer:{email:this.isLoggedIn?this.userEmail:e.querySelector('[name="email"]')?.value||"",name:e.querySelector('[name="name"]')?.value||"",phone:e.querySelector('[name="phone"]')?.value||""},note:e.querySelector('[name="special_instructions"]')?.value||"",pickup_time:e.querySelector('[name="pickup_time"]')?.value||""}}async processPayment(e){try{let t=null;if(this.selectedCardId)t=this.selectedCardId;else{const a=await this.card.tokenize({verificationDetails:{amount:String(e.total),currencyCode:this.config.currency||"CAD",intent:"CHARGE",customerInitiated:!0,billingContact:{givenName:e.customer.name.split(" ")[0],familyName:e.customer.name.split(" ").slice(1).join(" "),email:e.customer.email,phone:e.customer.phone,addressLines:[form.querySelector('[name="address"]')?.value||""],city:form.querySelector('[name="city"]')?.value||"",state:form.querySelector('[name="state"]')?.value||"",postalCode:form.querySelector('[name="postal_code"]')?.value||"",countryCode:"CA"}}});if("OK"!==a.status){const e=a.errors?.map((e=>e.message)).join(", ")||"Unknown error";throw new Error(`Card tokenization failed: ${e}`)}t=a.token,a.details?.userChallenged&&console.log("3D Secure verification completed")}return await this.submitToServer(t,e,!!this.selectedCardId)}catch(e){throw console.error("Payment processing failed:",e),e}}async submitToServer(e,t,a=!1){if(!this.isOpen)throw new Error("Store is currently closed");const i=await fetch(this.config.api_url+"process-payment",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({source_id:e,is_saved_card:a,cart_id:this.getCartId(),amount:t.total,items:t.items,customer:{email:this.isLoggedIn?this.userEmail:t.customer.email,name:t.customer.name,phone:t.customer.phone},note:t.note,pickup_time:t.pickup_time})}),s=await i.json();if(!i.ok)throw new Error(s.message||"Payment processing failed");return this.clearCart(),s}getCartId(){return this.cartId||(this.cartId=crypto.randomUUID(),this.cache.set("cart_id",this.cartId)),this.cartId}trackOrder(e){this.orderId=e,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>{this.checkOrderStatus()}),3e4)}async checkOrderStatus(){const e=await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`),t=await e.json();"ready"!==t.status&&this.scheduleOrderCheck(),this.updateOrderStatus(t)}updateOrderStatus(e){this.checkout.querySelectorAll(".status-item").forEach((t=>{t.dataset.status===e.status&&t.classList.add("active")})),this.checkout.querySelector("#eta").textContent=e.eta||"In progress"}async loadSavedCards(){try{const e=await fetch(this.config.api_url+"saved-cards",{method:"GET",headers:{"X-WP-Nonce":this.config.nonce}}),t=await e.json();t.success&&t.cards&&(this.savedCards=t.cards,this.renderSavedCards())}catch(e){console.error("Failed to load saved cards:",e)}}renderSavedCards(){const e=document.getElementById("saved-cards");if(!e||0===this.savedCards.length)return;const t=`\n            <div class="saved-cards-section">\n                <h4>Saved Payment Methods</h4>\n                ${this.savedCards.map((e=>`\n                    <label class="saved-card">\n                        <input type="radio" name="payment-method" value="saved" data-card-id="${e.id}">\n                        <span class="card-info">\n                            <strong>${e.card_brand}</strong> ending in ${e.last_4}\n                            <small>Exp: ${e.exp_month}/${e.exp_year}</small>\n                        </span>\n                    </label>\n                `)).join("")}\n                <label class="saved-card">\n                    <input type="radio" name="payment-method" value="new" checked>\n                    <span>Use a new card</span>\n                </label>\n            </div>\n        `;e.innerHTML=t,e.querySelectorAll('input[name="payment-method"]').forEach((e=>{e.addEventListener("change",(e=>{const t="new"===e.target.value,a=document.getElementById("square-card-container");a&&(a.style.display=t?"block":"none"),this.selectedCardId=t?null:e.target.dataset.cardId}))}))}handleSuccess(e,t){document.dispatchEvent(new CustomEvent("squareCheckoutSuccess",{detail:{result:e,form:t}}));const a=t.dataset.successUrl||`/order-confirmation/?order=${e.wp_order_id}`;window.location.href=a}handleError(e){console.error("Square checkout error:",e),document.dispatchEvent(new CustomEvent("squareCheckoutError",{detail:{error:e}})),window.jvbNotifications?.show?.(e.message||"Payment failed","error")}}document.addEventListener("DOMContentLoaded",(()=>{window.squareCheckout=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/utility.min.js b/assets/js/min/utility.min.js
index 978b9cc..8bdeaee 100644
--- a/assets/js/min/utility.min.js
+++ b/assets/js/min/utility.min.js
@@ -1 +1 @@
-(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),s=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:s<7?`${s} ${1===s?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.getIcon=function(t){if(void 0===t)return"";if(window.jvbIcons||(window.jvbIcons=new Map),!window.jvbIcons.has(t)&&jvbSettings.icons[t]){let e=document.createElement("div");e.innerHTML=jvbSettings.icons[t],window.jvbIcons.set(t,e.firstElementChild.cloneNode(!0)),e.remove()}return window.jvbIcons.get(t)?.cloneNode(!0)},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let s=!0;return async function(){for(;s;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){s=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const s=this.map(t[o],r);null!==s&&(s.hasOwnProperty("type")&&s.hasOwnProperty("data")?n[o]=s.data:n[o]=s,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),console.log("Scheduling action: ",t),console.log("With callback",e),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(console.log("Cancelling ",t),clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())console.log("clearing timeout: ",t),clearTimeout(t);this.timeouts.clear()}}})();
\ No newline at end of file
+(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),a=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:a<7?`${a} ${1===a?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.getIcon=function(t){if(void 0===t)return"";if(window.jvbIcons||(window.jvbIcons=new Map),!window.jvbIcons.has(t)&&jvbSettings.icons[t]){let e=document.createElement("div");e.innerHTML=jvbSettings.icons[t],window.jvbIcons.set(t,e.firstElementChild.cloneNode(!0)),e.remove()}return window.jvbIcons.get(t)?.cloneNode(!0)},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let a=!0;return async function(){for(;a;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){a=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),console.log("Scheduling action: ",t),console.log("With callback",e),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(console.log("Cancelling ",t),clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())console.log("clearing timeout: ",t),clearTimeout(t);this.timeouts.clear()}}})();
\ No newline at end of file
diff --git a/build/forms/view.asset.php b/build/forms/view.asset.php
index 4f95fff..8bb1b2c 100644
--- a/build/forms/view.asset.php
+++ b/build/forms/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '489e47cea4ff0c15a5f0');
+<?php return array('dependencies' => array(), 'version' => '6af2556d0306f0da3d78');
diff --git a/build/forms/view.js b/build/forms/view.js
index eb7164f..73415e6 100644
--- a/build/forms/view.js
+++ b/build/forms/view.js
@@ -1 +1 @@
-document.addEventListener("DOMContentLoaded",(function(){let e=new window.jvbForm;document.querySelectorAll(".jvb-form-block").forEach((t=>{e.registerForm(t.querySelector("form"))})),e.subscribe(((e,t)=>{"form-submit"===e&&async function(e,t,o){let r={"X-WP-Nonce":jvbSettings.nonce,"Content-Type":"application/json"};t.form_type=o.dataset.formId,t.form_id=o.id;let n=o.closest(".jvb-form-block"),i=window.getTemplate("spinner");o.hidden=!0,n.classList.add("loading"),n.prepend(i);try{const e=await fetch(`${jvbSettings.api}forms`,{method:"POST",headers:r,body:JSON.stringify(t)});if(!e.ok){const t=await e.json().catch((()=>({})));throw new Error(t.message||`Request failed with status ${e.status}`)}!function(e,t){if(!1===e.success){let o=t.querySelector("form");return o.removeAttribute("hidden"),void("required"in e&&e.required.forEach((e=>{let t=o.querySelector(`[name=${e}]`);t.classList.add("required","error"),t.scrollIntoView()})))}let o=window.getTemplate("formSummary");o.querySelector("h2").textContent="Success!",console.log("Form Response: ",e);for(let[t,r]of Object.entries(e)){let e=o.querySelector(`#${t}`),n=e.querySelector("h4");n.innerText.includes("%s")?n.innerHTML=n.replace("%s","<b>"+r+"</b>"):e.querySelector("div").innerHTML=r}t.append(o)}(await e.json(),n)}catch(e){throw e}finally{n.classList.remove("loading")}}(0,t.data,t.config.element)}))}));
\ No newline at end of file
+(()=>{class e{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((e=>{this.controller.registerForm(e)})),this.controller.subscribe(((e,o)=>{"form-submit"===e&&this.handleFormSubmission(o)}))}async handleFormSubmission(e){let[o,t,r]=[e.formId,e.config,e.data],s=t.element,n={"X-WP-Nonce":jvbSettings.nonce,"Content-Type":"application/json"};e.form_type=o,e.form_id=s.id,s.closest(".jvb-form-block"),this.controller.showFormStatus(o,"uploading");try{const e=await fetch(`${jvbSettings.api}forms`,{method:"POST",headers:n,body:JSON.stringify(r)});if(!e.ok){this.controller.showFormStatus(o,"error");const t=await e.json().catch((()=>({})));throw new Error(t.message||`Request failed with status ${e.status}`)}this.controller.showFormStatus(o,"submitted"),this.controller.showSummary(o,".jvb-form-block"),this.controller.store.delete(o)}catch(e){throw e}}updateUI(e,o){let t=window.getTemplate("formSummary");t.querySelector("h2").textContent="Success!",console.log("Form Response: ",e),console.log(t);for(let[o,r]of Object.entries(e)){let e=t.querySelector(`#${o}`);if(e){let o=e.querySelector("h4");o.innerText.includes("%s")?o.innerHTML=o.replace("%s","<b>"+r+"</b>"):e.querySelector("div").innerHTML=r}}o.append(t)}}document.addEventListener("DOMContentLoaded",(function(){new e}))})();
\ No newline at end of file
diff --git a/icons.php b/icons.php
index be6007c..10df568 100644
--- a/icons.php
+++ b/icons.php
@@ -378,6 +378,7 @@
 //		$this->cache->invalidateGroup('icons');
 		$this->style = JVB_SITE['icons']??'regular';
 
+
 		$this->used = get_option(BASE.'used_icons', [
 			$this->style => [
 				'heart',
@@ -475,7 +476,8 @@
 				'location',
 				'hours',
 				'star',
-				'star-half'
+				'star-half',
+				'exclamation-mark'
 			],
 			'fill'	=> [
 				'heart',
diff --git a/inc/blocks/FormBlock.php b/inc/blocks/FormBlock.php
index 6cec6de..369ce77 100644
--- a/inc/blocks/FormBlock.php
+++ b/inc/blocks/FormBlock.php
@@ -46,7 +46,6 @@
 
 		// Register forms data for the block editor
 		add_action('enqueue_block_editor_assets', [$this, 'localizeFormsData']);
-
 		add_action('init', [$this, 'registerBlock']);
 	}
 
@@ -112,7 +111,7 @@
 
 		$cache_key = $this->cache->generateKey($block);
 		$cached = $this->cache->get($cache_key);
-
+		$cached = false;
 		if ($cached) {
 			return $cached;
 		}
@@ -285,6 +284,7 @@
 		if (!empty($form_config['sections'])) {
 			$this->renderSections($type, $meta);
 		} else {
+			echo jvbFormStatus();
 			// Render fields directly
 			foreach ($form_config['fields'] as $field_name => $field_config) {
 				$meta->render('form', $field_name, $field_config);
@@ -300,8 +300,14 @@
 		$form_config = $this->forms[$type];
 		$sections = $form_config['sections'];
 		$fields = $form_config['fields'];
+		$total = count($sections);
 
 		echo '<div class="container">';
+		if ($total > 1) {
+			echo '<div class="form-progress">';
+			jvbRenderProgressBar('<span class="step-text">Step <span class="current">1</span> of ' . $total . '</span>');
+			echo '</div>';
+		}
 
 		// Render navigation if multiple sections
 		if (count($sections) > 1) {
@@ -311,21 +317,24 @@
 				$active_class = $i === 1 ? ' active' : '';
 				$aria_selected = $i === 1 ? 'true' : 'false';
 
-				echo '<button type="button" class="tab' . $active_class . '" data-tab="' . esc_attr($slug) . '" role="tab" aria-selected="' . $aria_selected . '">';
+				echo '<button type="button" class="tab' . $active_class . '" data-tab="' . esc_attr($slug) . '" data-step="'.$i.'" role="tab" aria-selected="' . $aria_selected . '">';
+				echo '<span class="step-number">' . $i . '</span>';
 				echo '<h2>' . esc_html($section['label'] ?? $section) . '</h2>';
 				echo '</button>';
 				$i++;
 			}
 			echo '</nav>';
 		}
+		echo jvbFormStatus();
 
 		// Render section content
 		$i = 0;
 		$total = count($sections);
 		foreach ($sections as $slug => $section) {
 			$active_class = $i === 0 ? ' active' : '';
+			$is_last = $i === ($total -1);
 
-			echo '<section id="' . esc_attr($slug) . '" class="tab-content' . $active_class . '" data-tab="' . esc_attr($slug) . '" role="tabpanel">';
+			echo '<section id="' . esc_attr($slug) . '" class="tab-content' . $active_class . '" data-tab="' . esc_attr($slug) . '" data-step="'.($i + 1).'" role="tabpanel">';
 
 			if (is_array($section) && !empty($section['title'])) {
 				echo '<h2>' . esc_html($section['title']) . '</h2>';
@@ -344,14 +353,30 @@
 				$meta->render('form', $field_name, $field_config);
 			}
 
-			if ($i === ($total - 1)){
-				$submit_text = $form_config['submit'] ?? 'Submit';
-				// Submit button
-				echo '<div class="form-actions">';
-				echo '<button type="submit">' . esc_html($submit_text) . '</button>';
-				echo '</div>';
+			// Add step navigation buttons
+			echo '<div class="step-navigation row btw">';
+
+			if ($i > 0) {
+				echo '<button type="button" class="button secondary prev-step" data-action="prev-step">';
+				echo jvbIcon('caret-left');
+				echo '<span>Previous</span>';
+				echo '</button>';
+			} else {
+				echo '<div></div>'; // Spacer for flex layout
 			}
 
+			if ($is_last) {
+				$submit_text = $form_config['submit'] ?? 'Submit';
+				echo '<button type="submit" class="button primary">' . esc_html($submit_text) . '</button>';
+			} else {
+				echo '<button type="button" class="button primary next-step" data-action="next-step">';
+				echo '<span>Next Step</span>';
+				echo jvbIcon('caret-right');
+				echo '</button>';
+			}
+
+			echo '</div>'; // .step-navigation
+
 			echo '</section>';
 			$i++;
 		}
@@ -389,19 +414,6 @@
 		echo '<input type="hidden" name="timestamp" value="' . time() . '">';
 
 		echo '</form>';
-
-		echo '<template class="formSummary">
-		<section class="form-summary">
-		<h2></h2>
-		<div class="message"><p>You\'ll get an email with this information, too. If you need to make any changes, respond to that email.</p></div>
-		<h3>Summary:</h3>';
-
-		foreach($form_config['fields'] as $field=>$config) {
-			$label = $config['summaryTitle'] ?? $config['label'];
-
-			echo '<div id="'.$field.'"><h4>'.$label.'</h4><div></div></div>';
-		}
-		echo '</section></template>';
 	}
 
 	/**
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 5c806d0..2e31338 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -324,6 +324,28 @@
 add_action('wp_footer', 'jvbOutputImageTemplates');
 
 function jvbOutputImageTemplates() {
+	if (wp_script_is('jvb-form')) {
+		jvbInlineStyles('forms');
+		?>
+		<template class="formSummary">
+			<div class="form-summary">
+				<h2>Success!</h2>
+				<div class="message">
+					<p>We're picking up what you're laying down.</p>
+					<p>You'll get an email with your response. If you can't find it, check your spam.</p>
+					<p>If you need to make any changes, respond to that email.</p>
+					<p>You can see a summary of what you wrote below:</p>
+				</div>
+				<div class="summary">
+					<div class="result">
+						<h4></h4>
+						<p></p>
+					</div>
+				</div>
+			</div>
+		</template>
+		<?php
+	}
 	if (wp_script_is('jvb-uploader')) {
 		?>
 		<template class="emptyGroup">
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index 1831a33..b9bdfd4 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -434,3 +434,23 @@
 	<?php
 }
 
+function jvbFormStatus(string $message = '') {
+	return '<div class="restore-form col" hidden>
+			<h3>Looks like we left things hanging...</h3>
+			<p>We\'ve filled in the fields with what you put last time.</p>
+			<div class="actions">
+				<button type="button" data-action="clear-form">
+					'.jvbIcon('arrows-clockwise').'
+					<span>Start Over</span>
+				</button>
+				<button type="button" data-action="dismiss-restore">
+					'.jvbIcon('close').'
+					<span>Dismiss</span>
+				</button>
+			</div>
+		</div>
+		<div class="fstatus row" hidden>
+			<div class="spinner"></div>
+			<p class="message">'.$message.'</p>
+		</div>';
+}
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 76f0c56..5f47ff6 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -35,6 +35,7 @@
 	protected array $apiEndpoints = [];   // Valid endpoint paths for this service
 	protected string $apiVersion = '';     // API version string (e.g., 'v2', '2024-01-01')
 
+	protected int $refresh_interval = 0; //seconds before expiry to refresh tokens. 0 to disable
 
 	/**
 	 * OAuth Configuration
@@ -806,7 +807,9 @@
 
 				// Retry with backoff for server errors
 				if ($attempt < $this->maxRetries && !$this->isClientError($e)) {
-					sleep($this->retryDelays[$attempt - 1] ?? 5);
+					$delay = pow(2, $attempt) * 1000000; // 2^attempt seconds in microseconds
+					$jitter = rand(0, $delay * 0.3); // Add 0-30% jitter
+					usleep($delay + $jitter);
 				} else {
 					break;
 				}
@@ -1341,10 +1344,21 @@
 		}
 
 		if (!empty($this->credentials)) {
-			if ($this->isOAuthService && $this->hasOAuthCredentials() && !$this->isOAuthValid()) {
-				$this->logDebug('OAuth token expired, attempting refresh');
-				if (!$this->refreshOAuthToken()) {
-					$this->logError('Failed to refresh OAuth token');
+			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');
+					}
+				}
+				// Check if we should proactively refresh (before expiry)
+				elseif ($this->shouldRefreshToken()) {
+					$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
+					}
 				}
 			}
 			$this->initialize();
@@ -1786,6 +1800,75 @@
 	}
 
 	/**
+	 * Check if token should be proactively refreshed
+	 * Different from isOAuthValid() which checks if token is actually expired
+	 */
+	protected function shouldRefreshToken(): bool
+	{
+		if (!$this->isOAuthService || $this->refresh_interval === 0) {
+			return false;
+		}
+
+		// If no expiry info, we can't proactively refresh
+		if (empty($this->credentials['expires_at'])) {
+			return false;
+		}
+
+		$expires_at = intval($this->credentials['expires_at']);
+		$time_until_expiry = $expires_at - time();
+
+		// Refresh if we're within the refresh interval window
+		return $time_until_expiry > 0 && $time_until_expiry <= $this->refresh_interval;
+	}
+	/**
+	 * Get time until token refresh is recommended
+	 * Useful for displaying in admin UI
+	 */
+	public function getTimeUntilRefresh(): ?int
+	{
+		if ($this->refresh_interval === 0 || empty($this->credentials['expires_at'])) {
+			return null;
+		}
+
+		$expires_at = intval($this->credentials['expires_at']);
+		$refresh_at = $expires_at - $this->refresh_interval;
+		$time_until_refresh = $refresh_at - time();
+
+		return max(0, $time_until_refresh);
+	}
+
+	/**
+	 * Get token freshness status
+	 * Returns: 'fresh', 'should_refresh', 'expired', or 'no_expiry_info'
+	 */
+	public function getTokenStatus(): string
+	{
+		if (!$this->isOAuthService) {
+			return 'not_oauth';
+		}
+
+		if (empty($this->credentials['access_token'])) {
+			return 'no_token';
+		}
+
+		if (empty($this->credentials['expires_at'])) {
+			return 'no_expiry_info';
+		}
+
+		$expires_at = intval($this->credentials['expires_at']);
+		$now = time();
+
+		if ($expires_at <= $now) {
+			return 'expired';
+		}
+
+		if ($this->shouldRefreshToken()) {
+			return 'should_refresh';
+		}
+
+		return 'fresh';
+	}
+	/**
 	 * Refresh OAuth token
 	 */
 	protected function refreshOAuthToken(): bool
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index 4eb7b1c..ca3a7aa 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -1,8 +1,10 @@
 <?php
 namespace JVBase\integrations;
 
+use JVBase\meta\MetaForm;
 use JVBase\meta\MetaManager;
 use Exception;
+use JVBase\registry\PostTypeRegistrar;
 use WP_Error;
 
 if (!defined('ABSPATH')) {
@@ -71,6 +73,8 @@
 		$this->title = 'Square';
 		$this->icon = 'square-logo';
 
+		$this->refresh_interval = 7 * DAY_IN_SECONDS;
+
 		// Define credential fields
 		$this->fields = [
 			'environment'	=> [
@@ -171,6 +175,8 @@
 				'sync_to_square' => 'Sync Site to Square',
 			]
 		);
+
+		add_action('init', [$this, 'registerSquarePostTypes']);
 	}
 
 	/**
@@ -207,6 +213,134 @@
 
 	}
 
+	public function getSquarePostConfig(string $post = 'all'):array
+	{
+		$posts = [
+			'_sq_orders' => [
+				'singular'	=> 'Square Order',
+				'plural'	=> 'Square Orders',
+				'public'	=> false,
+				'fields' => [
+					'post_title' => [
+						'type' => 'text',
+						'label' => 'Order Number'
+					],
+					'square_order_id' => [
+						'type' => 'text',
+						'label' => 'Square Order ID',
+						'readonly' => true
+					],
+					'square_payment_id' => [
+						'type' => 'text',
+						'label' => 'Square Payment ID',
+						'readonly' => true
+					],
+					'square_customer_id' => [
+						'type' => 'text',
+						'label' => 'Square Customer ID',
+						'readonly' => true
+					],
+					'amount' => [
+						'type' => 'number',
+						'label' => 'Total Amount (cents)',
+						'readonly' => true
+					],
+					'status' => [
+						'type' => 'select',
+						'label' => 'Order Status',
+						'options' => [
+							'PROPOSED' => 'Proposed',
+							'RESERVED' => 'Reserved',
+							'PREPARED' => 'Prepared (Ready for Pickup)',
+							'COMPLETED' => 'Completed',
+							'CANCELED' => 'Canceled'
+						],
+						'readonly' => true
+					],
+					'fulfillment_status' => [
+						'type' => 'select',
+						'label' => 'Fulfillment Status',
+						'options' => [
+							'PROPOSED' => 'Proposed',
+							'RESERVED' => 'Reserved',
+							'PREPARED' => 'Prepared',
+							'COMPLETED' => 'Completed',
+							'CANCELED' => 'Canceled',
+							'FAILED' => 'Failed'
+						],
+						'readonly' => true
+					],
+					'pickup_time' => [
+						'type' => 'datetime',
+						'label' => 'Pickup Time'
+					],
+					'customer_email' => [
+						'type' => 'email',
+						'label' => 'Customer Email',
+						'readonly' => true
+					],
+					'customer_name' => [
+						'type' => 'text',
+						'label' => 'Customer Name',
+						'readonly' => true
+					],
+					'customer_phone' => [
+						'type' => 'tel',
+						'label' => 'Customer Phone',
+						'readonly' => true
+					],
+					'special_instructions' => [
+						'type' => 'textarea',
+						'label' => 'Special Instructions',
+						'readonly' => true
+					],
+					'items' => [
+						'type' => 'repeater',
+						'label' => 'Order Items',
+						'readonly' => true,
+						'fields' => [
+							'name' => ['type' => 'text', 'label' => 'Item Name'],
+							'quantity' => ['type' => 'number', 'label' => 'Quantity'],
+							'price' => ['type' => 'number', 'label' => 'Price'],
+							'note' => ['type' => 'text', 'label' => 'Note']
+						]
+					],
+					'receipt_url' => [
+						'type' => 'url',
+						'label' => 'Receipt URL',
+						'readonly' => true
+					],
+					'created_at' => [
+						'type' => 'datetime',
+						'label' => 'Created At',
+						'readonly' => true
+					],
+					'updated_at' => [
+						'type' => 'datetime',
+						'label' => 'Last Updated',
+						'readonly' => true
+					]
+				]
+			]
+		];
+
+		if ($post === 'all'){
+			return $posts;
+		}elseif(array_key_exists($post, $posts)) {
+			return $posts[$post];
+		}
+		return [];
+	}
+
+	public function registerSquarePostTypes():void
+	{
+		$squarePostTypes = $this->getSquarePostConfig();
+		foreach ($squarePostTypes as $slug => $config) {
+			$registrar = new PostTypeRegistrar($slug, $config);
+			$registrar->register();
+		}
+	}
+
 	/**
 	 * Get request headers for API calls
 	 */
@@ -717,10 +851,10 @@
 		// User login tracking for security
 		add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
 
-		add_action('jvbAdditionalActions', [$this, 'outputCheckout']);
-
 		// Enqueue checkout scripts
 		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
+
+		add_filter('jvbAdditionalActions', [$this, 'outputCheckout']);
 	}
 
 
@@ -728,7 +862,7 @@
 		if (is_singular(BASE.'dash') || is_post_type_archive(BASE.'dash')) {
 			return $actions;
 		}
-
+		$meta = new MetaForm();
 		$form = '<aside id="cart" class="right">
 			<form id="checkout" data-form-id="checkout" data-save="checkout">';
 
@@ -745,12 +879,37 @@
 			'description' => 'Securely checkout with your name, email, and payments processed by Square.',
 			'content'	=> '<div class="checkout-section">
 								<h3>Customer Information</h3>
-
-								<input type="text" name="name" placeholder="Full Name" required autocomplete="name">
-								<input type="email" name="email" placeholder="Email" required autocomplete="email">
-								<input type="tel" name="phone" placeholder="Phone" required autocomplete="tel"">
-								<h3>Pickup Details</h3>
-								<input type="time" name="pickup_time" min="11:00" max="20:00" required>
+								'.$meta->return('cart_name', null, [
+									'type'		=> 'text',
+									'label'		=> 'Your Name',
+									'required'	=> true,
+									'autocomplete' => 'name'
+								]).
+								$meta->return('cart_email', null, [
+									'type'		=> 'email',
+									'label'		=> 'Your Email',
+									'required'	=> true,
+									'autocomplete'=> 'email',
+								]).
+								$meta->return('cart_phone', null, [
+									'type'		=> 'tel',
+									'label'		=> 'Your Phone',
+									'required'	=> true,
+									'autocomplete'=> 'phone'
+								]).'
+								<h3>Pickup Details</h3>'.
+								$meta->return('pickup_time', null, [
+									'type'		=> 'datetime',
+									'label'		=> 'Pickup Type',
+									'min'		=> '11:00',
+									'max'		=> '20:00',
+									'required'	=> true,
+								]).
+								$meta->return('special_instructions', null, [
+									'type'		=> 'textarea',
+									'label'		=> 'Special Instructions',
+									'quill'		=> true,
+								]).'
 								<textarea name="special_instructions" placeholder="Special instructions or dietary notes"></textarea>
 							</div>
 
@@ -768,7 +927,7 @@
 			'content'	=> $this->renderOrderStatus()
 		]
 				];
-		$form .= jvbRenderTabs($tabs);
+		$form .= jvbRenderTabs($tabs, true);
 
 		$form .= '<div class="cart-total row end"><p class="tax">Tax: <span></span></p><p class="total">GRAND TOTAL: <span></span></p></div>
 		</form>
@@ -817,7 +976,7 @@
 
 
 		$actions[] = [
-			'button' => 	'<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden>
+			'button' => 	'<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false">
 					'.jvbIcon('cart').'<span class="abs"></span><span class="abs count"></span>
 				</button>',
 			'content' =>	$form
@@ -1836,16 +1995,49 @@
 	/**
 	 * Handle order status webhook
 	 */
+	/**
+	 * Handle order status webhook - NOW UPDATES POST TYPE
+	 */
 	private function handleOrderWebhook(array $data): bool
 	{
 		$order_id = $data['object']['order']['id'] ?? '';
 		$state = $data['object']['order']['state'] ?? '';
+		$fulfillments = $data['object']['order']['fulfillments'] ?? [];
 
 		if (!$order_id) {
 			return false;
 		}
 
-		// Update cached order status
+		// Find the WP post for this order
+		$wp_order_id = get_option(BASE . 'square_order_map_' . $order_id);
+
+		if ($wp_order_id) {
+			// Update the post meta
+			$meta = new MetaManager($wp_order_id, 'post');
+			$updates = [
+				'status' => $state,
+				'updated_at' => current_time('mysql')
+			];
+
+			// Extract fulfillment status and pickup time
+			if (!empty($fulfillments[0])) {
+				$fulfillment = $fulfillments[0];
+				$updates['fulfillment_status'] = $fulfillment['state'] ?? $state;
+
+				if (!empty($fulfillment['pickup_details']['pickup_at'])) {
+					$updates['pickup_time'] = $fulfillment['pickup_details']['pickup_at'];
+				}
+			}
+
+			$meta->setAll($updates);
+
+			// Trigger notification to customer if order is ready
+			if ($state === 'PREPARED') {
+				do_action(BASE . 'square_order_ready', $wp_order_id, $order_id);
+			}
+		}
+
+		// Also update transient cache for quick status checks
 		set_transient(BASE . 'square_order_' . $order_id, $state, HOUR_IN_SECONDS);
 
 		// Trigger action for other integrations
@@ -1940,7 +2132,6 @@
 				'jvb-a11y',
 				'jvb-cache',
 				'jvb-tabs',
-				'jvb-modal',
 				'jvb-popup'
 			],
 			'1.0.0',
@@ -1957,13 +2148,15 @@
 			'jvb-square-checkout',
 			'squareConfig',
 			[
-				'isOpen'		=> jvbIsOpen(),
+				'isOpen' => jvbIsOpen(),
 				'application_id' => $this->credentials['client_id'] ?? '',
 				'location_id' => $this->locationId,
 				'environment' => $this->environment,
 				'api_url' => rest_url('jvb/v1/square/'),
 				'nonce' => wp_create_nonce('wp_rest'),
-				'currency' => get_option(BASE . 'currency', 'CAD')
+				'currency' => get_option(BASE . 'currency', 'CAD'),
+				'is_logged_in' => is_user_logged_in(),
+				'user_email' => is_user_logged_in() ? wp_get_current_user()->user_email : '' // NEW
 			]
 		);
 	}
@@ -2950,7 +3143,8 @@
 					'name' => $image_title ?: 'Image',
 					'caption' => $alt_text ?: ''
 				]
-			]
+			],
+			'object_id'	=> $supported_image_id
 		];
 
 		$body = $this->buildMultipartBody($file_path, $request_json, $boundary);
@@ -3279,4 +3473,222 @@
 			$this->deleteCredentials();
 		}
 	}
+
+	private function createSquareOrder(array $items, ?string $customer_id, array $data): array|WP_Error
+	{
+		// Build line items for Square
+		$line_items = [];
+		foreach ($items as $item) {
+			$line_item = [
+				'quantity' => (string)$item['quantity'], // MUST be string!
+			];
+
+			// Use catalog_object_id if available (recommended)
+			if (!empty($item['catalog_object_id'])) {
+				$line_item['catalog_object_id'] = $item['catalog_object_id'];
+				$line_item['catalog_version'] = $item['catalog_version'] ?? null;
+			} else {
+				// Ad-hoc line item (not recommended - no tax/inventory automation)
+				$line_item['name'] = $item['name'];
+				$line_item['base_price_money'] = [
+					'amount' => (int)$item['price'],
+					'currency' => $this->getCurrency()
+				];
+			}
+
+			if (!empty($item['note'])) {
+				$line_item['note'] = $item['note'];
+			}
+
+			$line_items[] = $line_item;
+		}
+
+		$order_data = [
+			'idempotency_key' => wp_generate_uuid4(), // Different from payment idempotency key
+			'order' => [
+				'location_id' => $this->locationId,
+				'line_items' => $line_items,
+				'state' => 'OPEN'
+			]
+		];
+
+		// Add customer if available
+		if ($customer_id) {
+			$order_data['order']['customer_id'] = $customer_id;
+		}
+
+		// Add metadata
+		if (!empty($data['note'])) {
+			$order_data['order']['metadata'] = [
+				'special_instructions' => $data['note']
+			];
+		}
+
+		if (!empty($data['pickup_time'])) {
+			$order_data['order']['metadata']['pickup_time'] = $data['pickup_time'];
+		}
+
+		return $this->postRequest('orders', $order_data);
+	}
+
+	private function createSquarePayment(
+		string $source_id,
+		string $idempotency_key,
+		int $amount_cents,
+		string $order_id,
+		?string $customer_id
+	): array|WP_Error
+	{
+		$payment_data = [
+			'idempotency_key' => $idempotency_key,
+			'source_id' => $source_id,
+			'amount_money' => [
+				'amount' => $amount_cents, // Already in cents!
+				'currency' => $this->getCurrency()
+			],
+			'order_id' => $order_id,
+			'location_id' => $this->locationId,
+			'autocomplete' => true, // Capture immediately
+		];
+
+		// Add customer if available
+		if ($customer_id) {
+			$payment_data['customer_id'] = $customer_id;
+		}
+
+		// Add reference ID for tracking
+		$payment_data['reference_id'] = 'WP_' . time();
+
+		return $this->postRequest('payments', $payment_data);
+	}
+
+	private function saveOrderToWordPress(array $order_data): int
+	{
+		// Extract customer info
+		$customer_email = $order_data['customer']['email'] ?? '';
+		$customer_name = $order_data['customer']['name'] ?? '';
+
+		// Find or create WP user for logged-in association
+		$user_id = 0;
+		if ($customer_email) {
+			$user = get_user_by('email', $customer_email);
+			if ($user) {
+				$user_id = $user->ID;
+				// Store Square customer ID on user
+				if (!empty($order_data['square_customer_id'])) {
+					update_user_meta($user_id, BASE . '_square_customer_id', $order_data['square_customer_id']);
+				}
+			}
+		}
+
+		// Create order post
+		$order_post_id = wp_insert_post([
+			'post_type' => BASE . '_sq_orders',
+			'post_title' => 'Order #' . $order_data['square_order_id'],
+			'post_status' => 'publish',
+			'post_author' => $user_id // Associate with user if logged in
+		]);
+
+		if (!$order_post_id || is_wp_error($order_post_id)) {
+			$this->logError('Failed to create order post', ['order_data' => $order_data]);
+			return 0;
+		}
+
+		// Save all order meta
+		$meta = new MetaManager($order_post_id, 'post');
+		$fields = $this->getSquarePostConfig('_sq_orders')['fields'];
+		unset($fields['post_title']);
+		$meta->setFieldConfig($fields);
+
+		$meta->setAll([
+			'square_order_id' => $order_data['square_order_id'],
+			'square_payment_id' => $order_data['square_payment_id'] ?? '',
+			'square_customer_id' => $order_data['square_customer_id'] ?? '',
+			'amount' => $order_data['amount'],
+			'status' => $order_data['status'],
+			'fulfillment_status' => $order_data['fulfillment_status'] ?? 'PROPOSED',
+			'pickup_time' => $order_data['pickup_time'] ?? '',
+			'customer_email' => $customer_email,
+			'customer_name' => $customer_name,
+			'customer_phone' => $order_data['customer']['phone'] ?? '',
+			'special_instructions' => $order_data['note'] ?? '',
+			'items' => $order_data['items'],
+			'receipt_url' => $order_data['receipt_url'] ?? '',
+			'created_at' => current_time('mysql'),
+			'updated_at' => current_time('mysql')
+		]);
+
+		// Index by Square order ID for quick webhook lookups
+		update_option(BASE . 'square_order_map_' . $order_data['square_order_id'], $order_post_id);
+
+		return $order_post_id;
+	}
+
+	/**
+	 * Get currency code
+	 */
+	private function getCurrency(): string
+	{
+		return get_option(BASE . 'currency', 'CAD');
+	}
+
+	/**
+	 * Get customer with saved cards (2025-compliant)
+	 */
+	public function getUserCards(string $customer_id): array
+	{
+		$response = $this->getRequest('cards?customer_id=' . $customer_id);
+		return $response['cards'] ?? [];
+	}
+
+
+	public function getUserOrders(string $customer_email): array
+	{
+		// First get Square customer ID
+		$customer_response = $this->postRequest('customers/search', [
+			'filter' => [
+				'email_address' => ['exact' => $customer_email]
+			]
+		]);
+
+		$customer_id = $customer_response['customers'][0]['id'] ?? null;
+		if (!$customer_id) {
+			return [];
+		}
+
+		// Get their orders
+		$orders_response = $this->postRequest('orders/search', [
+			'filter' => [
+				'customer_filter' => [
+					'customer_ids' => [$customer_id]
+				]
+			],
+			'sort' => [
+				'sort_field' => 'CREATED_AT',
+				'sort_order' => 'DESC'
+			],
+			'limit' => 50
+		]);
+
+		return $orders_response['orders'] ?? [];
+	}
+
+	public function checkOrderStatus(string $order_id): ?string
+	{
+		// Check transient cache first
+		$cached = get_transient(BASE . 'square_order_' . $order_id);
+		if ($cached) {
+			return $cached;
+		}
+
+		// Fetch from Square
+		$response = $this->getRequest('orders/' . $order_id);
+		if (!is_wp_error($response)) {
+			$state = $response['order']['state'] ?? null;
+			set_transient(BASE . 'square_order_' . $order_id, $state, HOUR_IN_SECONDS);
+			return $state;
+		}
+
+		return null;
+	}
 }
diff --git a/inc/managers/FormManager.php b/inc/managers/FormManager.php
index fe90ee0..28288b8 100644
--- a/inc/managers/FormManager.php
+++ b/inc/managers/FormManager.php
@@ -146,6 +146,7 @@
         <form id="<?= $id ?>" class="jvb-form" action="<?=esc_url(admin_url('admin-post.php'))?>" method="post">
         <?php
         wp_nonce_field('jvb_form_' . $id);
+		echo jvbFormStatus();
     }
 
 
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 429ea99..9777fa4 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -3,6 +3,7 @@
 
 use JVBase\managers\MagicLinkManager;
 use JVBase\integrations\Cloudflare;
+use JVBase\meta\MetaForm;
 use JVBase\utility\Features;
 use WP_User;
 use WP_Error;
@@ -58,7 +59,7 @@
 		add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2);
 
 
-		add_action('jvbAdditionalActions', [$this, 'outputShareWidget']);
+		add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']);
 
 		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
 		// Schedule cron jobs for reports
@@ -877,14 +878,13 @@
 	{
 
 		$user_id = get_current_user_id();
-		jvbDump($user_id);
 		$content = '<aside class="jvb-referral right">';
 		if (!$user_id) {
 			$content .= $this->getUnloggedInReferral();
 		} else {
 			$content .= $this->getLoggedInReferral($user_id);
 		}
-		$content .= '<button type="button" class="close">'.jvbIcon('close').'</button></aside>';
+		$content .= '</aside>';
 
 		$actions[] =[
 			'button' => '<button type="button" class="toggle-referral row" title="Your Referrals" data-action="toggle-referral" aria-label="Open Referral Sidebar" aria-controls="referral" aria-expanded="false">
@@ -901,44 +901,30 @@
 		ob_start();
 		JVB()->connect('cloudflare')->renderTurnstile();
 		$turnstile = ob_get_clean();
+		$meta = new MetaForm();
 		$codeForm = '<form id="referral-code-form">
-					<div class="status" hidden>
-						<div class="spinner"></div>
-						<p class="message"></p>
-					</div>
-					<div class="field text">
-						<label for="referral-name">Your Name</label>
-						<input type="text"
-							   id="referral-name"
-							   name="name"
-							   placeholder="Mister Meeseeks"
-							   autocomplete="name"
-							   required>
-					</div>
-
-					<div class="field email">
-						<label for="referral-email">Your Email</label>
-						<input type="email"
-							   id="referral-email"
-							   name="email"
-							   placeholder="look@me.com"
-							   autocomplete="email"
-							   required>
-					</div>
-
-					<div class="field text">
-						<label for="referral-code-input">Referral Code</label>
-						<input type="text"
-							   id="referral-code-input"
-							   name="referral_code"
-							   placeholder="e.g., THISISFAKE1234"
-							   required
-							   pattern="[A-Za-z0-9]+"
-							   maxlength="20"
-							   autocomplete="off"
-							   style="text-transform: uppercase;">
-					</div>
-
+					'.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>
@@ -954,14 +940,12 @@
 				</div>';
 
 		$loginForm = '<form id ="login-form">
-		<div class="status" hidden>
-			<div class="spinner"></div>
-			<p class="message"></p>
-		</div>
-		<div class="field email">
-			<label for="login-email">Your Email</label>
-			<input id="login-email" name="login-email" type="email" autocomplete="email">
-		</div>
+		'.jvbFormStatus().$meta->return('login_email', null, [
+				'required'	=> true,
+				'type'		=> 'email',
+				'label'		=> 'Your Email',
+				'autocomplete'=>'email'
+			]).'
 		'.$turnstile.'
 		<button type="submit">Login With Magic Link</button>
 </form>
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index d6846c4..91d9980 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -6,7 +6,7 @@
 use DateTime;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit;
 }
 
 /**
@@ -14,25 +14,28 @@
  */
 class MetaForm
 {
-    protected int $max_file_size = 5242880;
+	protected int $max_file_size = 5242880;
 
-    //Rendering fields
-    public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false):mixed
-    {
+	/* ========== MAIN RENDER METHOD ========== */
+	public function return(string $name, mixed $value, array $config, bool $showHidden = false)
+	{
+		return $this->render($name, $value, $config, $showHidden, true);
+	}
+	public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false): mixed
+	{
 		$out = '';
-        if (jvbCheck('hidden', $config) && !$showHidden) {
-            return $out;
-        }
-        // Get conditional attributes if they exist
-        $conditional = array_key_exists('condition', $config) ?
-            $this->handleConditionalField($config) : '';
 
-        if (!array_key_exists('type', $config)) {
-            return $out;
-        }
+		if (jvbCheck('hidden', $config) && !$showHidden) {
+			return $out;
+		}
 
-		if (array_key_exists('display', $config) && $config['display'] === 'hidden'){
-			$out = '<input type="hidden" name="'.$name.'" value="'.$value.'" />';
+		if (!array_key_exists('type', $config)) {
+			return $out;
+		}
+
+		// Handle hidden display type
+		if (array_key_exists('display', $config) && $config['display'] === 'hidden') {
+			$out = '<input type="hidden" name="' . $name . '" value="' . $value . '" />';
 			if (!$return) {
 				echo $out;
 			}
@@ -40,647 +43,961 @@
 		}
 
 		ob_start();
-        $type = array_map( 'ucfirst', explode('_', $config['type']));
-        $type = implode('', $type);
-        $method = 'render' . $type . 'Field';
+
+		// Try custom function overrides first
+		$type = array_map('ucfirst', explode('_', $config['type']));
+		$type = implode('', $type);
+		$method = 'render' . $type . 'Field';
 
 		$nameTemp = implode('', array_map('ucfirst', explode('_', $name)));
-		$nameMethod = 'render'.$nameTemp.'Field';
-		if(function_exists($nameMethod)) {
+		$nameMethod = 'render' . $nameTemp . 'Field';
+
+		if (function_exists($nameMethod)) {
 			call_user_func($nameMethod, $value, $config);
 		} elseif (function_exists($method)) {
 			call_user_func($method, $value, $config);
 		} elseif (method_exists($this, $method)) {
-            $this->$method($name, $value, $config);
-        }
-
-
+			$this->$method($name, $value, $config);
+		}
 
 		$out = ob_get_clean();
 
 		do_action('jvbRenderFormField', $name, $config, $value);
 		$out = apply_filters('jvbFilterRenderFormField', $out, $name, $config, $value);
+
 		if (!$return) {
 			echo $out;
 		}
 		return $out;
-    }
+	}
 
-    public function renderTextField(string $name, mixed $value, array $field):void
-    {
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="<?= esc_attr($field['subtype']??'text'); ?>"
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $autocomplete ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-				<?= $describedBy ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-            <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
+	/* ========== HELPER METHODS ========== */
 
-    private function renderTelField(string $name, mixed $value, array $field):void
-    	{$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-			$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="tel"
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $autocomplete?>
-				<?= $describedBy ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
-    private function renderEmailField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="email"
-			 <?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $autocomplete ?>
-				<?= $describedBy ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
+	/**
+	 * Prepare common field data
+	 */
+	protected function prepareFieldData(string $name, mixed $value, array $field): array
+	{
+		return [
+			'name' => array_key_exists('group', $field) ? $field['group'] . '::' . $name : $name,
+			'value' => isset($field['value']) ? $field['value'] : $value,
+			'id' => (array_key_exists('base', $field) ? esc_attr($field['base']) : '') . esc_attr($name),
+		];
+	}
 
-    private function renderUrlField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Use field-specific value if provided, otherwise use the meta value
-        $display_value = isset($field['value']) ? $field['value'] : $value;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <input
-                type="url"
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                value="<?= esc_attr($display_value); ?>"
-				<?= $placeholder ?>
-				<?= $describedBy ?>
-				<?= $autocomplete ?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-        if (array_key_exists('limit', $field)) {
-            $this->outputCharacterCountJS();
-        }
-    }
+	/**
+	 * Build common HTML attributes for inputs
+	 */
+	protected function buildInputAttributes(string $name, array $field): string
+	{
+		$attrs = [];
 
-    private function renderNumberField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		$description = '<ul class="list-none"><li>Tip: hold Ctrl/Command to increase 5x</li><li>Shift to increase 10x,</li><li>Or Ctrl/Command + Shift to increase 50x</li></ul>';
-		$description .= $field['description']??'';
-        $conditional = $this->handleConditionalField($field);
+		// Conditional rendering
+		if (array_key_exists('condition', $field)) {
+			$attrs['conditional'] = $this->handleConditionalField($field);
+		}
+
+		// Accessibility
+		if (!empty($field['description'])) {
+			$attrs['aria-describedby'] = $name . '-help';
+		}
+
+		// Common attributes
+		$common = ['placeholder', 'autocomplete', 'pattern', 'minlength', 'maxlength', 'min', 'max', 'step'];
+		foreach ($common as $attr) {
+			if (array_key_exists($attr, $field)) {
+				$attrs[$attr] = $field[$attr];
+			}
+		}
+
+		// Required
+		if (!empty($field['required'])) {
+			$attrs['required'] = true;
+		}
+
+		// Build attribute string
+		$attrString = '';
+		foreach ($attrs as $key => $val) {
+			if ($key === 'conditional') {
+				$attrString .= ' ' . $val; // Already formatted
+			} elseif ($val === true) {
+				$attrString .= ' ' . $key;
+			} else {
+				$attrString .= ' ' . $key . '="' . esc_attr($val) . '"';
+			}
+		}
+
+		return $attrString;
+	}
+
+	/**
+	 * Build validation data attributes
+	 */
+	protected function buildValidationAttributes(array $field): string
+	{
+		$attrs = [];
+
+		if (!empty($field['pattern'])) {
+			$attrs['data-pattern'] = $field['pattern'];
+		}
+
+		if (!empty($field['validate'])) {
+			$attrs['data-validate'] = $field['validate'];
+		}
+
+		if (isset($field['min'])) {
+			$attrs['data-min'] = $field['min'];
+		}
+
+		if (isset($field['max'])) {
+			$attrs['data-max'] = $field['max'];
+		}
+
+		if (isset($field['minlength'])) {
+			$attrs['data-minlength'] = $field['minlength'];
+		}
+
+		if (isset($field['maxlength'])) {
+			$attrs['data-maxlength'] = $field['maxlength'];
+		}
+
+		if (!empty($field['validation_message'])) {
+			$attrs['data-validation-message'] = $field['validation_message'];
+		}
+		$attrs['data-type'] = $field['type'];
+
+		$attrString = '';
+		foreach ($attrs as $key => $val) {
+			$attrString .= ' ' . $key . '="' . esc_attr($val) . '"';
+		}
+
+		return $attrString;
+	}
+
+	/* ========== GENERIC FIELD WRAPPER ========== */
+
+	/**
+	 * Render a standard input field with validation wrapper
+	 */
+	protected function renderStandardInput(string $name, mixed $value, array $field, string $inputType = 'text'): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$inputAttrs = $this->buildInputAttributes($name, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<?php $this->renderLabel($name, $field); ?>
+
+			<div class="field-input-wrapper">
+				<input
+					type="<?= esc_attr($inputType) ?>"
+					id="<?= esc_attr($data['id']) ?>"
+					name="<?= esc_attr($data['name']) ?>"
+					value="<?= esc_attr($data['value']) ?>"
+					<?= $inputAttrs ?>
+				>
+				<span class="validation-icon success" hidden aria-hidden="true">
+                    <?= jvbIcon('check-circle') ?>
+                </span>
+				<span class="validation-icon error" hidden aria-hidden="true">
+                    <?= jvbIcon('x-circle') ?>
+                </span>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	protected function renderComplexFieldWrapper(string $name, array $field, callable $renderContent): void
+	{
+		$data = $this->prepareFieldData($name, $field['value'] ?? '', $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
+		// Additional data attributes for complex fields
+		$dataAttrs = '';
+		if (array_key_exists('data', $field) && !empty($field['data'])) {
+			foreach ($field['data'] as $key => $val) {
+				$dataAttrs .= ($val === '') ? ' data-' . $key : ' data-' . $key . '="' . esc_attr($val) . '"';
+			}
+		}
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>
+			<?= $dataAttrs ?>
+			<?= $describedBy ?>>
+
+			<?php if (!empty($field['label']) && (!isset($field['show_label']) || $field['show_label'])) : ?>
+				<h3 class="field-label"><?= esc_html($field['label']) ?></h3>
+			<?php endif; ?>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+
+			<div class="field-content">
+				<?php $renderContent($name, $data, $field); ?>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Render field label with optional character count
+	 */
+	protected function renderLabel(string $name, array $field): void
+	{
+		?>
+		<label for="<?= esc_attr($name) ?>">
+			<?= esc_html($field['label']) ?>
+			<?php if (!empty($field['required'])) : ?>
+				<span class="required" aria-label="required">*</span>
+			<?php endif; ?>
+			<?php if (!empty($field['limit'])) : ?>
+				<span class="char-count" data-limit="<?= esc_attr($field['limit']) ?>">
+                    <span class="current">0</span>/<?= esc_attr($field['limit']) ?>
+                </span>
+			<?php endif; ?>
+		</label>
+		<?php
+	}
+
+	/**
+	 * Render hint and description
+	 */
+	protected function renderHintAndDescription(array $field, string $name): void
+	{
+		if (array_key_exists('hint', $field)) {
+			$this->renderHint($field['hint']);
+		}
+
+		if (array_key_exists('description', $field)) {
+			$this->renderDescription($field['description'], $name);
+		}
+	}
+
+	/* ========== SIMPLE INPUT FIELD TYPES ========== */
+
+	public function renderTextField(string $name, mixed $value, array $field): void
+	{
+		$this->renderStandardInput($name, $value, $field, $field['input_type'] ?? 'text');
+	}
+
+	public function renderEmailField(string $name, mixed $value, array $field): void
+	{
+		$field['validate'] = 'email'; // Auto-add email validation
+		$this->renderStandardInput($name, $value, $field, 'email');
+	}
+
+	private function renderUrlField(string $name, mixed $value, array $field): void
+	{
+		$field['validate'] = 'url'; // Auto-add URL validation
+		$this->renderStandardInput($name, $value, $field, 'url');
+	}
+
+	private function renderTelField(string $name, mixed $value, array $field): void
+	{
+		$field['validate'] = 'phone'; // Auto-add phone validation
+		$this->renderStandardInput($name, $value, $field, 'tel');
+	}
+
+	private function renderDateField(string $name, mixed $value, array $field): void
+	{
+		$format = !empty($field['format']) ? $field['format'] : 'Y-m-d';
+
+		// Format the date if we have a value
+		if (!empty($value)) {
+			$date = DateTime::createFromFormat($format, $value);
+			if ($date) {
+				$value = $date->format('Y-m-d'); // HTML date input requires Y-m-d format
+			}
+		}
+
+		$this->renderStandardInput($name, $value, $field, 'date');
+	}
+
+	private function renderTimeField(string $name, mixed $value, array $field): void
+	{
+		$this->renderStandardInput($name, $value, $field, 'time');
+	}
+
+	private function renderDatetimeField(string $name, mixed $value, array $field): void
+	{
+		$this->renderStandardInput($name, $value, $field, 'datetime-local');
+	}
+
+	/* ========== TEXTAREA FIELD ========== */
+
+	public function renderTextareaField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$inputAttrs = $this->buildInputAttributes($name, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		$rows = isset($field['rows']) ? (int)$field['rows'] : 4;
+		$quill = (array_key_exists('quill', $field) && $field['quill'] == true) ? ' data-editor="true"' : '';
+
+		if ($quill !== '') {
+			$allowImages = array_key_exists('allowImage', $field);
+			$quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"';
+		}
+
+		// Handle array values
+		if (is_array($value)) {
+			$value = implode(', ', $value);
+		}
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<?php $this->renderLabel($name, $field); ?>
+
+			<div class="field-input-wrapper">
+                <textarea
+					id="<?= esc_attr($data['id']) ?>"
+					name="<?= esc_attr($data['name']) ?>"
+					rows="<?= esc_attr($rows) ?>"
+                    <?= $quill ?>
+					<?= $inputAttrs ?>
+                ><?= esc_textarea($data['value']) ?></textarea>
+				<span class="validation-icon success" hidden aria-hidden="true">
+                    <?= jvbIcon('check-circle') ?>
+                </span>
+				<span class="validation-icon error" hidden aria-hidden="true">
+                    <?= jvbIcon('x-circle') ?>
+                </span>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	/* ========== NUMBER FIELD ========== */
+
+	private function renderNumberField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
 
 		$min = isset($field['min']) ? (float)$field['min'] : 0;
 		$max = isset($field['max']) ? (float)$field['max'] : 100;
 		$step = isset($field['step']) ? (float)$field['step'] : 1;
 
-		$data = '';
+		// Handle custom data attributes
+		$customData = '';
 		if (array_key_exists('data', $field) && !empty($field['data'])) {
-			foreach($field['data'] as $key => $v) {
-				if ($v === '') {
-					$data .= ' data-'.$key;
-				} else {
-					$data .= ' data-'.$key.'="'.$v.'"';
-				}
+			foreach ($field['data'] as $key => $v) {
+				$customData .= ($v === '') ? ' data-' . $key : ' data-' . $key . '="' . $v . '"';
 			}
 		}
 
 		if (empty($value)) {
-			$value = $field['default']??0;
+			$value = $field['default'] ?? 0;
 		}
 
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?> row" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-            </label>
+		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="' . $field['autocomplete'] . '"' : '';
 
-			 <div class="quantity"
-				  <?=$data?>>
-
-				 <button type="button"
-						 class="decrease"
-						 title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
-						 aria-label="Decrease <?= esc_attr($field['label']); ?>">
-					 <?= jvbIcon('minus')?>
-				 </button>
-
-				 <input type="number"
-						id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-						name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-						value="<?= esc_attr($value); ?>"
-						min="<?= esc_attr($min); ?>"
-						max="<?= esc_attr($max); ?>"
-						step="<?= esc_attr($step); ?>"
-						class="quantity-input"
-						<?= $describedBy ?>
-						<?= $autocomplete ?>
-					 <?= !empty($field['required']) ? 'required' : ''; ?>>
-
-				 <button type="button"
-						 class="increase"
-						 title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
-						 aria-label="Increase <?= esc_attr($field['label']); ?>">
-					 <?= jvbIcon('add')?>
-				 </button>
-			 </div>
-			 <?php $this->renderDescription($description, $name);  ?>
-        </div>
-        <?php
-    }
-
-    public function renderTextareaField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $rows = isset($field['rows']) ? (int)$field['rows'] : 4;
-        $conditional = $this->handleConditionalField($field);
-        $quill = (array_key_exists('quill', $field) && $field['quill'] == true) ? ' data-editor="true"' : '';
-        if ($quill !== '') {
-            $allowImages = array_key_exists('allowImage', $field);
-            $quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"';
-        }
-        // Handle array values
-        if (is_array($value)) {
-            $value = implode(', ', $value);
-        }
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name ?? ''); ?>">
-                <?= esc_html($field['label']); ?>
-                <?php if (!empty($field['limit'])) : ?>
-                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
-                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
-                </span>
-                <?php endif; ?>
-            </label>
-            <textarea
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
-                    <?= $quill ?>
-                    rows="<?= esc_attr($rows); ?>"
-				<?= $placeholder ?>
-				<?= $describedBy ?>
-				<?= !empty($field['required']) ? 'required' : ''; ?>
-            <?= !empty($field['limit']) ? 'data-limit="' . esc_attr($field['limit']) . '"' : ''; ?>
-        ><?= esc_textarea($value); ?></textarea>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-
-    private function renderSetField(string $name, mixed $value, array $field):void
-    {
-        $this->renderCheckboxField($name, $value, $field);
-    }
-
-    private function renderCheckboxField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $value = !is_array($value) ? explode(',', $value) : $value;
-        $limit = isset($field['limit']) ? (int)$field['limit'] : 0;
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-        <div class="field checkbox-group" <?= $limit ? 'data-limit="' . esc_attr($limit) . '"' : ''; ?> <?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
-            <span class="label"><?= esc_html($field['label']); ?></span>
-            <div class="checkbox-options flex">
-            <?php foreach ($field['options'] as $key => $label) : ?>
-                <input
-                    type="checkbox"
-                    id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key)?>"
-					<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name)?>[]"
-                    value="<?= esc_attr($key); ?>"
-                    <?= (in_array($key, $value)) ? 'checked' : ''; ?>
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-                >
-                <label class="checkbox-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
-                    <?= esc_html($label); ?>
-                </label>
-            <?php endforeach; ?>
-        </div>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-    private function renderRadioField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $value = (array)$value;
-        $conditional = $this->handleConditionalField($field);
-
-        if (!array_key_exists('label', $field)) {
-            error_log('No label for: '.print_r($name, true));
-        }
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-        <div class="field radio-group"<?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
-            <label><?= esc_html($field['label']); ?></label>
-
-            <div class="radio-options row">
-                <?php foreach ($field['options'] as $key => $label) : ?>
-                    <input
-                        type="radio"
-                        id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key) ?>"
-                        name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                        value="<?= esc_attr($key); ?>"
-						<?= !empty($field['required']) ? 'required' : ''; ?>
-                        <?= (in_array($key, $value)) ? 'checked' : ''; ?>
-                    >
-                    <label class="radio-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
-                        <?= esc_html($label); ?>
-                    </label>
-                <?php endforeach; ?>
-            </div>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-
-        </div>
-        <?php
-    }
-
-    private function renderRepeaterField(string $name, mixed $value, array $field):void
-    {
-		error_log('Rendering Repeater Field!');
-        $values = is_array($value) ? $value : array();
-
-        $conditional = $this->handleConditionalField($field);
-        $row_label = isset($field['row_label']) ? $field['row_label'] : '';
-		$rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item';
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        ?>
-        <div class="field repeater <?=$name?>"
-             data-field="<?= esc_attr($name); ?>"
-			 <?= $describedBy ?>
-            <?= $row_label ? 'data-label="' . esc_attr($row_label) . '"' : ''; ?>
-            <?=$conditional?>>
-            <?php
-            if (!array_key_exists('label', $field)) {
-                error_log('No label for: '.print_r($name, true));
-            }
-            ?>
-            <h3><?= esc_html($field['label']); ?></h3>
-
-
-            <div class="repeater-items">
-                <?php
-                if (!empty($values)) {
-                    foreach ($values as $index => $row) {
-                        $this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle);
-                    }
-                }
-                ?>
-            </div>
-
-            <template class="<?=uniqid('repeaterTemplate')?>">
-                <?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
-            </template>
-
-            <button type="button" class="add-repeater-row">
-                <?= jvbIcon('plus', ['title'=> 'Add']); ?> <?= (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?>
-            </button>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-
-    private function renderRepeaterRow(array $fields, array $values, int|string $index, string $base_name, string $rowTitle = 'New Item'):void
-    {
-        $display_number = (is_string($index)) ? $index : ($index + 1);
-        ?>
-        <div class="repeater-row" data-index="<?= esc_attr($index); ?>">
-            <details <?= (is_string($index)) ? 'open' : ''; ?>>
-                <summary class="repeater-row-header row btw">
-                    <span class="drag-handle"><?= jvbIcon('grab'); ?></span>
-                    <span class="row-number">#<?= esc_html($display_number); ?></span>
-                    <span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span>
-                    <button type="button" class="remove-row" title="Remove">
-                        <?= jvbIcon('delete', ['title'=>'Remove']); ?>
-                    </button>
-                </summary>
-                <div class="repeater-row-content">
-                    <?php
-                    foreach ($fields as $slug => $field) :
-                        if ($base_name === '') {
-                            $field_name = $slug;
-                        } else {
-                            $field_name = sprintf('%s:%s:%s', $base_name, $index, $slug);
-                        }
-                        $field_value = isset($values[$slug]) ? $values[$slug] : '';
-                        $name = $field_name;
-                        $this->render($name, $field_value, $field);
-                    endforeach;
-                    ?>
-                </div>
-            </details>
-        </div>
-        <?php
-    }
-
-    private function getRowTitle(array $fields, array $values, string $rowTitle):string
-    {
-        // Try to find the first text field or textarea value to use as title
-        foreach ($fields as $slug => $field) {
-            if (in_array($field['type'], ['text', 'textarea']) &&
-                isset($values[$slug]) &&
-                !empty($values[$slug])) {
-                return $values[$slug];
-            }
-        }
-        return $rowTitle;
-    }
-
-    private function renderTaxonomyField(string $name, mixed $value, array $field):void
-    {
-		$conditional = $this->handleConditionalField($field);
-		$taxonomy = $field['taxonomy'];
-
-		// Get currently selected terms
-		$selected_terms = ($value === '') ? [] : explode(',', $value);
-
-
-		// Convert selected term IDs to the format expected by single modal
-		$processedSelected = [];
-		if (!empty($selected_terms)) {
-			foreach ($selected_terms as $termId) {
-				if (is_numeric($termId)) {
-					$term = get_term($termId, $taxonomy);
-					if ($term && !is_wp_error($term)) {
-						$processedSelected[$term->term_id] = [
-							'name' => $term->name,
-							'path' => TaxonomySelector::getTermPath($term)
-						];
-					}
-				}
-			}
-		}
-
-		// Create configuration for single modal system
-		$config = [
-			'taxonomy' => $taxonomy,
-			'max' => $field['limit'] ?? 0,
-			'search' => $field['search'] ?? true,
-			'createNew' => $field['createNew'] ?? false,
-			'selected' => $processedSelected,
-			'base' => $field['base'] ?? '',
-		];
-
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
 		?>
-		<div class="field taxonomy <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
-			<div class="field-group-header">
-				<label class="toggle">
-					<?= jvbIcon(str_replace(BASE, '', $taxonomy)) ?>
-					<?= esc_html($field['label']) ?>
-				</label>
-			</div>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?> row"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
 
-			<?php
-			$tax = new TaxonomySelector($name, $taxonomy, $config);
-			$extra = '<input type="hidden"
-				   name="'.esc_attr($name).'"
-				   id="'.esc_attr($name).'"'.$describedBy.'
-				   data-taxonomy="'.esc_attr($taxonomy).'"
-				   value="'.esc_attr(is_array($selected_terms) ? implode(',', $selected_terms) : $selected_terms).'">';
-			echo $tax->render([], $extra);
-			?>
+			<?php $this->renderLabel($name, $field); ?>
 
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-		</div>
-		<?php
-    }
+			<div class="quantity" <?= $customData ?>>
+				<button type="button"
+						class="decrease"
+						title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
+						aria-label="Decrease <?= esc_attr($field['label']) ?>">
+					<?= jvbIcon('minus') ?>
+				</button>
 
-    protected function renderPostSelectorField(string $name, mixed $value, array $field):void
-    {
-		$conditional = $this->handleConditionalField($field);
+				<input type="number"
+					   id="<?= esc_attr($data['id']) ?>"
+					   name="<?= esc_attr($data['name']) ?>"
+					   value="<?= esc_attr($value) ?>"
+					   min="<?= esc_attr($min) ?>"
+					   max="<?= esc_attr($max) ?>"
+					   step="<?= esc_attr($step) ?>"
+					   class="quantity-input"
+					<?= $describedBy ?>
+					<?= $autocomplete ?>
+					<?= !empty($field['required']) ? 'required' : '' ?>>
 
-		// Process selected posts
-		$selected_posts = $value;
-		if (is_string($selected_posts)) {
-			$selected_posts = !empty($selected_posts) ? explode(',', $selected_posts) : [];
-		} elseif (!is_array($selected_posts)) {
-			$selected_posts = [];
-		}
-
-		// Configure the post selector
-		$config = [
-			'multiple' => $field['multiple'] ?? true,
-			'maxSelections' => $field['limit'] ?? 0,
-			'search' => true,
-			'placeholder' => $field['placeholder'] ?? 'Search posts...',
-			'noResults' => 'No posts found',
-			'shop_id' => $field['shop_id'] ?? null,
-			'onClose' => 'updateMetaFormPost'
-		];
-
-		$postSelector = new PostSelector($field['post_type'], $config);
-		$containerId = $name . '-post-selector';
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		?>
-		<div class="field post-selector <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
-			<div class="field-group-header">
-				<label class="toggle">
-					<?= jvbIcon($field['post_type'] . '-selector') ?>
-					<?= esc_html($field['label'] ?? ucfirst($field['post_type'])) ?>
-				</label>
-				<button title="Add <?= esc_attr(ucfirst($field['post_type'])) ?>"
-						class="add-item-btn"
-						type="button">
-					<?= jvbIcon('add', ['title' => "Add " . ucfirst($field['post_type'])]) ?>
+				<button type="button"
+						class="increase"
+						title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
+						aria-label="Increase <?= esc_attr($field['label']) ?>">
+					<?= jvbIcon('add') ?>
 				</button>
 			</div>
 
-			<?= $postSelector->render($selected_posts, $containerId) ?>
-
-			<!-- Hidden input for form submission -->
-			<input type="hidden"
-				   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name) ?>"
-				   class="post-selector-input"
-				   <?= $describedBy ?>
-				   data-post-type="<?= esc_attr($field['post_type']) ?>"
-				   value="<?= esc_attr(is_array($selected_posts) ? implode(',', $selected_posts) : $selected_posts) ?>">
-
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+			<?php $this->renderHintAndDescription($field, $name); ?>
 		</div>
 		<?php
-    }
+	}
 
-	protected function renderGroupField(string $name, mixed $value, array $field):void
+	/* ========== SELECT, RADIO, CHECKBOX FIELDS ========== */
+
+	private function renderSelectField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$inputAttrs = $this->buildInputAttributes($name, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<?php $this->renderLabel($name, $field); ?>
+
+			<div class="field-input-wrapper">
+				<select
+					id="<?= esc_attr($data['id']) ?>"
+					name="<?= esc_attr($data['name']) ?>"
+					<?= $inputAttrs ?>
+				>
+					<?php foreach ($field['options'] as $key => $label) : ?>
+						<option value="<?= esc_attr($key) ?>" <?php selected($value, $key); ?>>
+							<?= esc_html($label) ?>
+						</option>
+					<?php endforeach; ?>
+				</select>
+				<span class="validation-icon success" hidden aria-hidden="true">
+                    <?= jvbIcon('check-circle') ?>
+                </span>
+				<span class="validation-icon error" hidden aria-hidden="true">
+                    <?= jvbIcon('x-circle') ?>
+                </span>
+			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	private function renderRadioField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<fieldset>
+				<legend><?= esc_html($field['label']) ?>
+					<?php if (!empty($field['required'])) : ?>
+						<span class="required" aria-label="required">*</span>
+					<?php endif; ?>
+				</legend>
+
+				<?php foreach ($field['options'] as $key => $label) : ?>
+					<label class="radio-option">
+						<input
+							type="radio"
+							name="<?= esc_attr($data['name']) ?>"
+							value="<?= esc_attr($key) ?>"
+							<?php checked($value, $key); ?>
+							<?= !empty($field['required']) ? 'required' : '' ?>
+						>
+						<span><?= esc_html($label) ?></span>
+					</label>
+				<?php endforeach; ?>
+			</fieldset>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	private function renderCheckboxField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+
+		if (!is_array($value)) {
+			$value = !empty($value) ? [$value] : [];
+		}
+
+		?>
+		<div class="field <?= esc_attr($field['type']) ?> <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<fieldset>
+				<legend><?= esc_html($field['label']) ?>
+					<?php if (!empty($field['required'])) : ?>
+						<span class="required" aria-label="required">*</span>
+					<?php endif; ?>
+				</legend>
+
+				<?php foreach ($field['options'] as $key => $label) : ?>
+					<input
+						type="checkbox"
+						id="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>"
+						name="<?= esc_attr($data['name']) ?>[]"
+						value="<?= esc_attr($key) ?>"
+						<?php checked(in_array($key, $value)); ?>
+					>
+					<label class="checkbox-option" for="<?= esc_attr($data['name']) ?>-<?= esc_attr($key)?>">
+						<span><?= esc_html($label) ?></span>
+					</label>
+				<?php endforeach; ?>
+			</fieldset>
+
+			<span class="validation-message" hidden role="alert"></span>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+	private function renderTrueFalseField(string $name, mixed $value, array $field): void
+	{
+		$data = $this->prepareFieldData($name, $value, $field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$conditional = array_key_exists('condition', $field) ? $this->handleConditionalField($field) : '';
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
+		?>
+		<div class="field true-false <?= esc_attr($name) ?> row btw"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>>
+
+			<label class="toggle-switch row" <?= $describedBy ?>>
+				<input
+					type="checkbox"
+					name="<?= esc_attr($data['name']) ?>"
+					value="1"
+					<?= ($value) ? ' checked' : '' ?>
+					<?= !empty($field['required']) ? 'required' : '' ?>
+				>
+				<div class="slider"></div>
+				<span class="toggle-label">
+					<?php if (!empty($field['required'])) : ?>
+						<span class="required" aria-label="required">*</span>
+					<?php endif; ?>
+
+					<?= esc_html($field['label']) ?></span>
+			</label>
+			<span class="validation-message" hidden role="alert"></span>
+			<?php $this->renderHintAndDescription($field, $name); ?>
+		</div>
+		<?php
+	}
+
+
+
+	/* ========== REPEATER FIELD ========== */
+
+	private function renderRepeaterField(string $name, mixed $value, array $field): void
+	{
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use ($value) {
+			$values = is_array($value) ? $value : [];
+			$rowLabel = $field['row_label'] ?? '';
+			$rowTitle = $field['new_row'] ?? 'New Item';
+			$addLabel = $field['add_label'] ?? 'Add Item';
+
+			?>
+			<div class="repeater-items" data-label="<?= esc_attr($rowLabel) ?>">
+				<?php
+				if (!empty($values)) {
+					foreach ($values as $index => $row) {
+						$this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle);
+					}
+				}
+				?>
+			</div>
+
+			<template class="<?= uniqid('repeaterTemplate') ?>">
+				<?php $this->renderRepeaterRow($field['fields'], [], '', $name, $rowTitle); ?>
+			</template>
+
+			<button type="button" class="add-repeater-row button secondary">
+				<?= jvbIcon('plus', ['title' => 'Add']) ?>
+				<span><?= esc_html($addLabel) ?></span>
+			</button>
+			<?php
+		});
+	}
+
+	private function renderRepeaterRow(array $fields, array $values, int|string $index, string $base_name, string $rowTitle): void
+	{
+		$display_number = is_string($index) ? $index : ($index + 1);
+		?>
+		<div class="repeater-row" data-index="<?= esc_attr($index) ?>">
+			<details <?= is_string($index) ? 'open' : '' ?>>
+				<summary class="repeater-row-header row btw">
+					<span class="drag-handle"><?= jvbIcon('grab') ?></span>
+					<span class="row-number">#<?= esc_html($display_number) ?></span>
+					<span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)) ?></span>
+					<button type="button" class="remove-row" title="Remove">
+						<?= jvbIcon('delete', ['title' => 'Remove']) ?>
+					</button>
+				</summary>
+				<div class="repeater-row-content">
+					<?php
+					foreach ($fields as $slug => $field) {
+						$field_name = ($base_name === '') ? $slug : sprintf('%s:%s:%s', $base_name, $index, $slug);
+						$field_value = $values[$slug] ?? '';
+						$this->render($field_name, $field_value, $field);
+					}
+					?>
+				</div>
+			</details>
+		</div>
+		<?php
+	}
+
+	private function getRowTitle(array $fields, array $values, string $rowTitle): string
+	{
+		// Try to find the first text field or textarea value to use as title
+		foreach ($fields as $slug => $field) {
+			if (in_array($field['type'], ['text', 'textarea']) &&
+				isset($values[$slug]) &&
+				!empty($values[$slug])) {
+				return $values[$slug];
+			}
+		}
+		return $rowTitle;
+	}
+
+	/* ========== GROUP FIELD ========== */
+
+	protected function renderGroupField(string $name, mixed $value, array $field): void
 	{
 		if (!array_key_exists('fields', $field) || empty($field['fields'])) {
 			return;
 		}
 
-		// Handle conditional fields
-		$conditional = $this->handleConditionalField($field);
-
-		// Ensure value is an array
 		$values = is_array($value) ? $value : [];
 		$original = $name;
+
 		if (array_key_exists('group', $field)) {
-			$name = $field['group'].'::'.$name;
+			$name = $field['group'] . '::' . $name;
 		}
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+
 		$hidden = (array_key_exists('mode', $field) && $field['mode'] === 'hidden');
-		if (!$hidden) {
-			?>
-			<fieldset class="field group <?= esc_attr($name) ?>" <?= $conditional ?> data-field="<?=$name?>"<?= $describedBy?>>
-				<legend><?= esc_html($field['label']) ?></legend>
-			<?php
+
+		if ($hidden) {
+			// Simplified render for hidden groups
+			$this->renderGroupFields($name, $values, $field);
+			return;
 		}
+
+		// Standard fieldset render
+		$conditional = $this->handleConditionalField($field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
 		?>
+		<fieldset class="field group <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+				  data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>
+			<?= $describedBy ?>>
+			<legend><?= esc_html($field['label']) ?></legend>
 
-			<div class="group-fields <?=$original?>"<?= ($hidden) ? ' data-field="'.$name.'"' : ''?>"<?= $describedBy ?>>
-		<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-		<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-				<?php
-				foreach ($field['fields'] as $field_name => $config) {
-					// Set the group context for proper field naming
-					$config['group'] = $name;
+			<?php $this->renderHintAndDescription($field, $name); ?>
 
-					// Get the value for this specific field
-					$field_value = $values[$field_name] ?? '';
-
-					// Handle conditional fields within the group
-					if (isset($config['condition'])) {
-						// Convert condition field reference to group context
-						$condition_field = $config['condition']['field'];
-						if (!str_contains($condition_field, '::')) {
-							$config['condition']['field'] = $name . '::' . $condition_field;
-						}
-					}
-
-					$this->render($field_name, $field_value, $config);
-				}
-				?>
+			<div class="group-fields <?= esc_attr($original) ?>">
+				<?php $this->renderGroupFields($name, $values, $field); ?>
 			</div>
+
+			<span class="validation-message" hidden role="alert"></span>
+		</fieldset>
 		<?php
-		if (!$hidden) {
-			?>
-			</fieldset>
-			<?php
+	}
+
+	/**
+	 * Render individual fields within a group
+	 * Reusable for both standard and hidden group modes
+	 */
+	private function renderGroupFields(string $groupName, array $values, array $field): void
+	{
+		foreach ($field['fields'] as $field_name => $config) {
+			// Set the group context for proper field naming
+			$config['group'] = $groupName;
+
+			// Get the value for this specific field
+			$field_value = $values[$field_name] ?? '';
+
+			// Handle conditional fields within the group
+			if (isset($config['condition'])) {
+				$condition_field = $config['condition']['field'];
+				if (!str_contains($condition_field, '::')) {
+					$config['condition']['field'] = $groupName . '::' . $condition_field;
+				}
+			}
+
+			$this->render($field_name, $field_value, $config);
 		}
 	}
+
+	/* ========== UPLOAD FIELD ========== */
+
+	private function renderUploadField(string $name, mixed $value, array $field): void
+	{
+		// Merge with defaults
+		$config = array_merge([
+			'subtype' => 'image',
+			'accepted_types' => null,
+			'multiple' => false,
+			'limit' => 0,
+			'mode' => 'direct',
+			'destination' => 'meta',
+			'max_size' => null,
+			'convert' => 'webp',
+			'quality' => 80,
+			'create_thumbnails' => true,
+		], $field);
+
+		// Validate destination config
+		if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
+			error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined");
+			return;
+		}
+
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		// Prepare upload configuration
+		$acceptedTypes = $this->getAllowedTypes($config);
+		$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
+		$acceptAttr = implode(',', $acceptExtensions);
+		$attachmentIds = $this->parseAttachmentIds($value);
+		$fieldType = $config['multiple'] ? 'gallery' : 'image';
+
+		// Build data attributes for uploader.js
+		$uploadData = [
+			'data-subtype' => $config['subtype'],
+			'data-mode' => $config['mode'],
+			'data-destination' => $config['destination'],
+			'data-multiple' => $config['multiple'] ? 'true' : 'false',
+			'data-limit' => $config['limit'],
+			'data-convert' => $config['convert'],
+			'data-quality' => $config['quality'],
+		];
+
+		if (!empty($config['content'])) {
+			$uploadData['data-content'] = $config['content'];
+		}
+
+		$this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use (
+			$config, $acceptAttr, $attachmentIds, $fieldType, $uploadData, $value
+		) {
+			?>
+			<div class="upload-field-wrapper <?= esc_attr($fieldType) ?>"
+			<?php foreach ($uploadData as $attr => $val) : ?>
+				<?= $attr ?>="<?= esc_attr($val) ?>"
+			<?php endforeach; ?>>
+
+			<!-- Preview Area -->
+			<div class="upload-preview-area">
+				<?php $this->renderUploadPreviews($attachmentIds, $config); ?>
+			</div>
+
+			<!-- Upload Area -->
+			<div class="file-upload-container">
+				<div class="file-upload-wrapper">
+					<input type="file"
+						   name="<?= esc_attr($data['name']) ?>_temp"
+						   accept="<?= esc_attr($acceptAttr) ?>"
+						<?= $config['multiple'] ? 'multiple' : '' ?>>
+					<p class="file-upload-text">
+						<strong>Click to upload</strong> or drag and drop<br>
+						<?= esc_html($this->getUploadInstructions($config)) ?>
+					</p>
+				</div>
+				<div class="file-error"></div>
+			</div>
+
+			<!-- Hidden input for storing the IDs -->
+			<input type="hidden"
+				   name="<?= esc_attr($data['name']) ?>"
+				   value="<?= esc_attr($value) ?>"
+				<?= !empty($field['required']) ? 'required' : '' ?>>
+			</div>
+			<?php
+		});
+	}
+
+	/**
+	 * Render upload preview items
+	 */
+	private function renderUploadPreviews(array $attachmentIds, array $config): void
+	{
+		if (empty($attachmentIds)) {
+			return;
+		}
+
+		foreach ($attachmentIds as $id) {
+			if ($config['subtype'] === 'image') {
+				$url = wp_get_attachment_image_url($id, 'thumbnail');
+				if ($url) {
+					echo '<div class="upload-item" data-id="' . esc_attr($id) . '">';
+					echo '<img src="' . esc_url($url) . '" alt="">';
+					echo '<button type="button" class="remove-preview">' .
+						jvbIcon('trash', ['title' => 'Remove']) . '</button>';
+					echo '</div>';
+				}
+			}
+			// Add other subtypes (video, document) as needed
+		}
+	}
+
+	/**
+	 * Get upload instruction text based on config
+	 */
+	private function getUploadInstructions(array $config): string
+	{
+		$extensions = $this->getMimeExtensions($this->getAllowedTypes($config));
+		$extList = implode(', ', array_map('strtoupper', $extensions));
+		$maxSize = $config['max_size'] ?? $this->max_file_size;
+		$maxSizeMB = round($maxSize / 1048576, 1);
+
+		return "{$extList} (max. {$maxSizeMB}MB)";
+	}
+
+	/* ========== TAXONOMY/USER SELECTOR FIELDS ========== */
+
+	private function renderTaxonomyField(string $name, mixed $value, array $field): void
+	{
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$this->renderSelectorField($name, $value, $field, 'taxonomy');
+	}
+
+	private function renderUserField(string $name, mixed $value, array $field): void
+	{
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		$this->renderSelectorField($name, $value, $field, 'post');
+	}
+
+	/**
+	 * Generic selector field renderer
+	 * Handles both taxonomy and post selectors with consistent structure
+	 */
+	private function renderSelectorField(string $name, mixed $value, array $field, string $type): void
+	{
+		$conditional = $this->handleConditionalField($field);
+		$validationAttrs = $this->buildValidationAttributes($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="' . $name . '-help"' : '';
+
+		// Parse selected values
+		$selected = ($value === '') ? [] : (is_array($value) ? $value : explode(',', $value));
+
+		// Create selector instance
+		if ($type === 'taxonomy') {
+			$taxonomy = $field['taxonomy'];
+			$selectorConfig = [
+				'multiple' => $field['multiple'] ?? true,
+				'placeholder' => $field['placeholder'] ?? 'Search terms...',
+				'noResults' => 'No terms found',
+				'onClose' => 'updateMetaFormTaxonomy'
+			];
+			$selector = new TaxonomySelector($taxonomy, $selectorConfig);
+			$icon = $taxonomy;
+		} else {
+			$postType = $field['post_type'];
+			$selectorConfig = [
+				'multiple' => $field['multiple'] ?? true,
+				'placeholder' => $field['placeholder'] ?? 'Search posts...',
+				'noResults' => 'No posts found',
+				'shop_id' => $field['shop_id'] ?? null,
+				'onClose' => 'updateMetaFormPost'
+			];
+			$selector = new PostSelector($postType, $selectorConfig);
+			$icon = $postType;
+		}
+
+		$containerId = $name . '-' . $type . '-selector';
+
+		?>
+		<div class="field <?= esc_attr($type) ?>-selector <?= esc_attr($name) ?>"
+			<?= $conditional ?>
+			 data-field="<?= esc_attr($name) ?>"
+			<?= $validationAttrs ?>
+			<?= $describedBy ?>>
+
+			<div class="field-group-header row btw">
+				<label class="toggle row">
+					<?= jvbIcon($icon) ?>
+					<span><?= esc_html($field['label'] ?? ucfirst($type)) ?></span>
+				</label>
+				<button type="button"
+						class="add-item-btn button secondary"
+						title="Add <?= esc_attr(ucfirst($type)) ?>">
+					<?= jvbIcon('add', ['title' => 'Add ' . ucfirst($type)]) ?>
+				</button>
+			</div>
+
+			<?= $selector->render($selected, $containerId) ?>
+
+			<!-- Hidden input for form submission -->
+			<input type="hidden"
+				   class="<?= esc_attr($type) ?>-selector-input"
+				   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>"
+				   data-<?= esc_attr($type) ?>="<?= esc_attr($field[$type === 'taxonomy' ? 'taxonomy' : 'post_type']) ?>"
+				   value="<?= esc_attr(is_array($selected) ? implode(',', $selected) : $value) ?>"
+				<?= !empty($field['required']) ? 'required' : '' ?>>
+
+			<?php $this->renderHintAndDescription($field, $name); ?>
+
+			<span class="validation-message" hidden role="alert"></span>
+		</div>
+		<?php
+	}
+
+	/* ========== LOCATION FIELD ========== */
+
 	protected function renderLocationField(string $name, mixed $value, array $field): void
 	{
 		$googleMaps = JVB()->connect('maps');
@@ -689,7 +1006,7 @@
 			return;
 		}
 
-		// Extract stored values
+		// Parse stored data
 		if (is_string($value)) {
 			$value = maybe_unserialize($value);
 		}
@@ -698,18 +1015,15 @@
 		$address = $stored_data['address'] ?? '';
 		$lat = $stored_data['lat'] ?? '';
 		$lng = $stored_data['lng'] ?? '';
+		$street = $stored_data['street'] ?? '';
 
-		// Generate unique field ID
-		$field_id = esc_attr($name);
-		$map_id = $field_id . '_map';
-
-		// Handle grouped fields
 		if (array_key_exists('group', $field)) {
 			$name = $field['group'] . '::' . $name;
 		}
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
 
-		// Prepare configuration for JavaScript initialization
+		// Prepare JavaScript configuration
+		$field_id = esc_attr($name);
+		$map_id = $field_id . '_map';
 		$js_config = [
 			'fieldId' => $field_id,
 			'initialCoords' => (!empty($lat) && !empty($lng)) ? [
@@ -718,1411 +1032,144 @@
 			] : null
 		];
 
-		// IMPORTANT: Properly escape the JSON for use in HTML attribute
-		$json_config = htmlspecialchars(json_encode($js_config), ENT_QUOTES, 'UTF-8');
-		?>
-
-		<div class="field location <?= esc_attr($field_id) ?>"
-			 data-field="<?= esc_attr($field_id) ?>"
-			 data-location-field-init="<?= $json_config ?>"<?=$describedBy?>>
-
-			<?php
-			if (!empty($stored_data['street'])) {
-				echo '<p><b>Current location:</b> '.esc_html($stored_data['street']).'</p>';
-				echo '<p class="hint"><b>Search below to change:</b></p>';
-			}
+		$this->renderComplexFieldWrapper($name, $field, function($name, $data, $field) use (
+			$stored_data, $street, $address, $lat, $lng, $map_id, $js_config
+		) {
 			?>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+			<div class="location-field-wrapper"
+				 data-location-field-init="<?= esc_attr(json_encode($js_config)) ?>">
 
-			<div class="location-search-wrapper">
-				<div class="autocomplete-wrapper"></div>
+				<?php if (!empty($street)) : ?>
+					<p class="current-location">
+						<strong>Current location:</strong> <?= esc_html($street) ?>
+					</p>
+				<?php endif; ?>
 
-				<!-- Map container -->
-				<div class="location-preview">
-					<div id="<?= esc_attr($map_id); ?>"
-						 class="location-map">
-					</div>
+				<label for="<?= esc_attr($data['id']) ?>">Address</label>
+				<input type="text"
+					   id="<?= esc_attr($data['id']) ?>"
+					   name="<?= esc_attr($data['name']) ?>[address]"
+					   value="<?= esc_attr($address) ?>"
+					   placeholder="Enter an address"
+					   class="location-search-input"
+					   autocomplete="off"
+					<?= !empty($field['required']) ? 'required' : '' ?>>
 
-					<?php if (!empty($stored_data)):
-						jvbLocationLinks($stored_data);
-					endif; ?>
-				</div>
+				<div id="<?= esc_attr($map_id) ?>" class="location-map" style="height: 300px;"></div>
 
-				<!-- Hidden inputs for data storage -->
-				<input type="hidden"
-					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[address]"
-					   value="<?= esc_attr($address); ?>"
-					   data-location-field="address">
-
-				<input type="hidden"
-					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lat]"
-					   value="<?= esc_attr($lat); ?>"
-					   data-location-field="lat">
-
-				<input type="hidden"
-					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lng]"
-					   value="<?= esc_attr($lng); ?>"
-					   data-location-field="lng">
-
-				<?php
-				// Component fields
-				$components = ['street', 'city', 'province', 'postal_code', 'country'];
-				foreach ($components as $component):
-					?>
-					<input type="hidden"
-						   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[<?= $component; ?>]"
-						   value="<?= esc_attr($stored_data[$component] ?? ''); ?>"
-						   data-location-field="<?= esc_attr($component); ?>">
-				<?php endforeach; ?>
-
+				<!-- Hidden fields for lat/lng -->
+				<input type="hidden" name="<?= esc_attr($data['name']) ?>[lat]" value="<?= esc_attr($lat) ?>" class="location-lat">
+				<input type="hidden" name="<?= esc_attr($data['name']) ?>[lng]" value="<?= esc_attr($lng) ?>" class="location-lng">
+				<input type="hidden" name="<?= esc_attr($data['name']) ?>[street]" value="<?= esc_attr($stored_data['street'] ?? '') ?>" class="location-street">
+				<input type="hidden" name="<?= esc_attr($data['name']) ?>[city]" value="<?= esc_attr($stored_data['city'] ?? '') ?>" class="location-city">
+				<input type="hidden" name="<?= esc_attr($data['name']) ?>[province]" value="<?= esc_attr($stored_data['province'] ?? '') ?>" class="location-province">
+				<input type="hidden" name="<?= esc_attr($data['name']) ?>[postal]" value="<?= esc_attr($stored_data['postal'] ?? '') ?>" class="location-postal">
+				<input type="hidden" name="<?= esc_attr($data['name']) ?>[country]" value="<?= esc_attr($stored_data['country'] ?? '') ?>" class="location-country">
 			</div>
-		</div>
-		<?php
+			<?php
+		});
 	}
-    //TODO: This is more or less handled by PostSelector/TaxonomySelector, no?
-    private function renderAssociationField(string $name, mixed $value, array $field):void
-    {
-        // Ensure value is an array
-        if (!is_array($value)) {
-            $value = empty($value) ? [] : [$value];
-        }
 
-        // Get field configuration
-        $limit = isset($field['limit']) ? (int)$field['limit'] : 0;
-        $object_types = isset($field['object_types']) ? $field['object_types'] : ['post'];
-        $post_types = isset($field['post_types']) ? $field['post_types'] : ['post'];
-        $taxonomies = isset($field['taxonomies']) ? $field['taxonomies'] : [];
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        // Create unique ID for this field
-        $field_id = 'association-' . esc_attr($name);
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-        <div class="field association <?=$name?>" data-field="<?= esc_attr($name); ?>" <?= $conditional; ?>>
-            <label><?= esc_html($field['label']); ?></label>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+	/* ========== HTML FIELD ========== */
 
-            <div class="association-container"<?=$describedBy?>>
-                <div class="association-search">
-                    <input type="text"
-                           id="<?= esc_attr($field_id); ?>-search"
-                           class="association-search-input"
-                           placeholder="Search items...">
-
-                    <div class="association-filter">
-                        <?php if (count($object_types) > 1 || count($post_types) > 1 || count($taxonomies) > 0) : ?>
-                            <select class="association-filter-select">
-                                <?php if (in_array('post', $object_types)) : ?>
-                                    <?php foreach ($post_types as $post_type) : ?>
-                                        <?php
-                                        $post_type_obj = get_post_type_object($post_type);
-                                        $label = $post_type_obj ? $post_type_obj->labels->singular_name : ucfirst($post_type);
-                                        ?>
-                                        <option value="post:<?= esc_attr($post_type); ?>">
-                                            <?= esc_html($label); ?>
-                                        </option>
-                                    <?php endforeach; ?>
-                                <?php endif; ?>
-
-                                <?php if (in_array('term', $object_types)) : ?>
-                                    <?php foreach ($taxonomies as $taxonomy) : ?>
-                                        <?php
-                                        $tax_obj = get_taxonomy($taxonomy);
-                                        $label = $tax_obj ? $tax_obj->labels->singular_name : ucfirst($taxonomy);
-                                        ?>
-                                        <option value="term:<?= esc_attr($taxonomy); ?>">
-                                            <?= esc_html($label); ?>
-                                        </option>
-                                    <?php endforeach; ?>
-                                <?php endif; ?>
-                            </select>
-                        <?php endif; ?>
-
-                        <button type="button" class="search-button">
-                            <?= jvbIcon('search', ['title' => 'Search']); ?>
-                        </button>
-                    </div>
-                </div>
-
-                <div class="association-results">
-                    <div class="association-available">
-                        <h4>Available Items</h4>
-                        <ul class="available-items"></ul>
-                        <div class="association-loading" hidden>
-                            Loading...
-                        </div>
-                        <div class="association-no-results" hidden>
-                            No items found
-                        </div>
-                        <div class="association-pagination">
-                            <button type="button" class="prev-page" disabled>
-                                <?= jvbIcon('arrow-left', ['title' => 'Previous']); ?>
-                            </button>
-                            <span class="page-info">Page <span class="current-page">1</span></span>
-                            <button type="button" class="next-page" disabled>
-                                <?= jvbIcon('arrow-right', ['title' => 'Next']); ?>
-                            </button>
-                        </div>
-                    </div>
-
-                    <div class="association-actions">
-                        <button type="button" class="add-selected" disabled>
-                            <?= jvbIcon('arrow-right', ['title' => 'Add selected']); ?>
-                        </button>
-                        <button type="button" class="remove-selected" disabled>
-                            <?= jvbIcon('arrow-left', ['title' => 'Remove selected']); ?>
-                        </button>
-                    </div>
-
-                    <div class="association-selected">
-                        <h4>Selected Items
-                            <?php if ($limit) : ?>
-                                <span class="limit-info">(<?= esc_html($limit); ?> max)</span>
-                            <?php endif; ?>
-                        </h4>
-                        <ul class="selected-items row">
-                            <?php
-                            // Display currently selected items
-                            foreach ($value as $item_id) {
-                                // Try to determine the type and get details
-                                $item_type = '';
-                                $item_title = '';
-                                $item_object = '';
-
-                                // Check if it's a post
-                                if (in_array('post', $object_types)) {
-                                    $post = get_post($item_id);
-                                    if ($post && in_array($post->post_type, $post_types)) {
-                                        $item_type = 'post';
-                                        $item_title = $post->post_title;
-                                        $item_object = $post->post_type;
-                                    }
-                                }
-
-                                // Check if it's a term
-                                if (empty($item_type) && in_array('term', $object_types)) {
-                                    foreach ($taxonomies as $taxonomy) {
-                                        $term = get_term($item_id, $taxonomy);
-                                        if (!is_wp_error($term) && $term) {
-                                            $item_type = 'term';
-                                            $item_title = $term->name;
-                                            $item_object = $term->taxonomy;
-                                            break;
-                                        }
-                                    }
-                                }
-
-                                // Only output if we found the item
-                                if (!empty($item_type) && !empty($item_title)) {
-                                    ?>
-                                    <li data-id="<?= esc_attr($item_id); ?>"
-                                        data-type="<?= esc_attr($item_type); ?>"
-                                        data-object="<?= esc_attr($item_object); ?>">
-                                        <span class="item-title"><?= esc_html($item_title); ?></span>
-                                        <span class="item-type"><?= esc_html(ucfirst($item_object)); ?></span>
-                                        <button type="button" class="remove-item row">
-                                            <?= jvbIcon('close', ['title' => 'Remove']); ?>
-                                        </button>
-                                    </li>
-                                    <?php
-                                }
-                            }
-                            ?>
-                        </ul>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Hidden input to store selected values -->
-            <input type="hidden" name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>" value="<?= esc_attr(implode(',', $value)); ?>">
-        </div>
-
-        <script>
-            (function() {
-                // Initialize association field
-                const container = document.querySelector('[data-field="<?= esc_attr($name); ?>"]');
-                if (!container) return;
-
-                const searchInput = container.querySelector('.association-search-input');
-                const filterSelect = container.querySelector('.association-filter-select');
-                const searchButton = container.querySelector('.search-button');
-                const availableList = container.querySelector('.available-items');
-                const selectedList = container.querySelector('.selected-items');
-                const hiddenInput = container.querySelector('input[name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"]');
-                const addButton = container.querySelector('.add-selected');
-                const removeButton = container.querySelector('.remove-selected');
-                const loadingIndicator = container.querySelector('.association-loading');
-                const noResultsMessage = container.querySelector('.association-no-results');
-                const prevPageButton = container.querySelector('.prev-page');
-                const nextPageButton = container.querySelector('.next-page');
-                const currentPageSpan = container.querySelector('.current-page');
-
-                // Configuration
-                const config = {
-                    limit: <?= $limit ?: 0; ?>,
-                    objectTypes: <?= json_encode($object_types); ?>,
-                    postTypes: <?= json_encode($post_types); ?>,
-                    taxonomies: <?= json_encode($taxonomies); ?>,
-                    perPage: 10,
-                    currentPage: 1
-                };
-
-                // Current state
-                let currentSearch = '';
-                let currentFilter = filterSelect ? filterSelect.value : (config.objectTypes.includes('post') ? 'post:' + config.postTypes[0] : 'term:' + config.taxonomies[0]);
-                let availableItems = [];
-                let selectedItems = [];
-
-                // Parse initial selected items
-                const initialValue = hiddenInput.value;
-                if (initialValue) {
-                    selectedItems = initialValue.split(',').map(id => parseInt(id, 10));
-                }
-
-                // Event Listeners
-                if (searchButton) {
-                    searchButton.addEventListener('click', performSearch);
-                }
-
-                if (searchInput) {
-                    searchInput.addEventListener('keypress', function(e) {
-                        if (e.key === 'Enter') {
-                            e.preventDefault();
-                            performSearch();
-                        }
-                    });
-                }
-
-                if (filterSelect) {
-                    filterSelect.addEventListener('change', function() {
-                        currentFilter = this.value;
-                        config.currentPage = 1;
-                        performSearch();
-                    });
-                }
-
-                if (prevPageButton) {
-                    prevPageButton.addEventListener('click', function() {
-                        if (config.currentPage > 1) {
-                            config.currentPage--;
-                            performSearch();
-                        }
-                    });
-                }
-
-                if (nextPageButton) {
-                    nextPageButton.addEventListener('click', function() {
-                        config.currentPage++;
-                        performSearch();
-                    });
-                }
-
-                // Add items
-                addButton.addEventListener('click', function() {
-                    const selectedAvailableItems = availableList.querySelectorAll('li.selected');
-                    selectedAvailableItems.forEach(item => {
-                        const id = parseInt(item.dataset.id, 10);
-                        // Check limit
-                        if (config.limit && selectedItems.length >= config.limit) {
-                            return;
-                        }
-
-                        // Skip if already selected
-                        if (selectedItems.includes(id)) {
-                            return;
-                        }
-
-                        // Add to selection
-                        selectedItems.push(id);
-
-                        // Clone and modify for selected list
-                        const clone = item.cloneNode(true);
-                        clone.classList.remove('selected');
-
-                        // Replace checkbox with remove button
-                        const checkbox = clone.querySelector('input[type="checkbox"]');
-                        if (checkbox) {
-                            const removeBtn = document.createElement('button');
-                            removeBtn.type = 'button';
-                            removeBtn.className = 'remove-item';
-                            removeBtn.innerHTML = '<?= jvbIcon('close', ['title' => 'Remove']); ?>';
-                            removeBtn.addEventListener('click', function() {
-                                removeItem(id, clone);
-                            });
-
-                            checkbox.parentNode.replaceChild(removeBtn, checkbox);
-                        }
-
-                        selectedList.appendChild(clone);
-                    });
-
-                    // Update hidden input
-                    updateHiddenInput();
-
-                    // Update UI state
-                    updateButtonStates();
-                });
-
-                // Remove items
-                removeButton.addEventListener('click', function() {
-                    const selectedSelectedItems = selectedList.querySelectorAll('li.selected');
-                    selectedSelectedItems.forEach(item => {
-                        const id = parseInt(item.dataset.id, 10);
-                        removeItem(id, item);
-                    });
-                });
-
-                // Listen for clicks on items in both lists
-                availableList.addEventListener('click', function(e) {
-                    const item = e.target.closest('li');
-                    if (!item) return;
-
-                    // If clicking checkbox, handle separately
-                    if (e.target.type === 'checkbox') {
-                        updateButtonStates();
-                        return;
-                    }
-
-                    // Toggle selection
-                    if (item.classList.contains('selected')) {
-                        item.classList.remove('selected');
-                        item.querySelector('input[type="checkbox"]').checked = false;
-                    } else {
-                        item.classList.add('selected');
-                        item.querySelector('input[type="checkbox"]').checked = true;
-                    }
-
-                    updateButtonStates();
-                });
-
-                selectedList.addEventListener('click', function(e) {
-                    const item = e.target.closest('li');
-                    if (!item) return;
-
-                    // If clicking remove button, handle it
-                    if (e.target.closest('.remove-item')) {
-                        const id = parseInt(item.dataset.id, 10);
-                        removeItem(id, item);
-                        return;
-                    }
-
-                    // Toggle selection
-                    item.classList.toggle('selected');
-                    updateButtonStates();
-                });
-
-                // Helper Functions
-                function performSearch() {
-                    currentSearch = searchInput.value.trim();
-
-                    // Show loading
-                    loadingIndicator.hidden = false;
-                    noResultsMessage.hidden = true;
-                    availableList.innerHTML = '';
-
-                    // Get filter parts
-                    const [type, object] = currentFilter.split(':');
-
-                    // Prepare data for AJAX
-                    const data = {
-                        action: 'jvb_association_search',
-                        nonce: jvbSettings.nonce,
-                        type: type,
-                        object: object,
-                        search: currentSearch,
-                        page: config.currentPage,
-                        per_page: config.perPage,
-                        selected: selectedItems
-                    };
-
-                    // Make AJAX request to WordPress REST API
-                    fetch(jvbSettings.api + 'terms', {
-                        method: 'POST',
-                        headers: {
-                            'Content-Type': 'application/json',
-                            'X-WP-Nonce': jvbSettings.nonce
-                        },
-                        body: JSON.stringify(data)
-                    })
-                        .then(response => response.json())
-                        .then(response => {
-                            // Hide loading
-                            loadingIndicator.hidden = true;
-
-                            if (response.success && response.items && response.items.length > 0) {
-                                // Update available items
-                                availableItems = response.items;
-
-                                // Render items
-                                renderAvailableItems();
-
-                                // Update pagination
-                                updatePagination(response.total, response.pages);
-                            } else {
-                                // Show no results
-                                noResultsMessage.hidden = false;
-                                prevPageButton.disabled = true;
-                                nextPageButton.disabled = true;
-                                currentPageSpan.textContent = '1';
-                            }
-                        })
-                        .catch(error => {
-                            console.error('Error searching items:', error);
-                            loadingIndicator.hidden = true;
-                            noResultsMessage.hidden = false;
-                        });
-                }
-
-                function renderAvailableItems() {
-                    availableList.innerHTML = '';
-
-                    availableItems.forEach(item => {
-                        const isSelected = selectedItems.includes(item.id);
-
-                        const li = document.createElement('li');
-                        li.dataset.id = item.id;
-                        li.dataset.type = item.type;
-                        li.dataset.object = item.object;
-
-                        // Create checkbox
-                        const checkbox = document.createElement('input');
-                        checkbox.type = 'checkbox';
-                        checkbox.id = `${name}-item-${item.id}`;
-
-                        // Create label for title
-                        const titleSpan = document.createElement('span');
-                        titleSpan.className = 'item-title';
-                        titleSpan.textContent = item.title;
-
-                        // Create label for type
-                        const typeSpan = document.createElement('span');
-                        typeSpan.className = 'item-type';
-                        typeSpan.textContent = item.object_label;
-
-                        // Append elements
-                        li.appendChild(checkbox);
-                        li.appendChild(titleSpan);
-                        li.appendChild(typeSpan);
-
-                        // Disable if already selected
-                        if (isSelected) {
-                            li.classList.add('disabled');
-                            checkbox.disabled = true;
-
-                            // Add note that item is already selected
-                            const note = document.createElement('span');
-                            note.className = 'item-note';
-                            note.textContent = 'Already selected';
-                            li.appendChild(note);
-                        }
-
-                        availableList.appendChild(li);
-                    });
-                }
-
-                function updatePagination(total, pages) {
-                    // Update current page display
-                    currentPageSpan.textContent = config.currentPage;
-
-                    // Update prev/next buttons
-                    prevPageButton.disabled = config.currentPage <= 1;
-                    nextPageButton.disabled = config.currentPage >= pages;
-                }
-
-                function removeItem(id, element) {
-                    // Remove from array
-                    selectedItems = selectedItems.filter(itemId => itemId !== id);
-
-                    // Remove from DOM
-                    if (element) {
-                        element.remove();
-                    }
-
-                    // Update hidden input
-                    updateHiddenInput();
-
-                    // Update buttons
-                    updateButtonStates();
-                }
-
-                function updateHiddenInput() {
-                    hiddenInput.value = selectedItems.join(',');
-                }
-
-                function updateButtonStates() {
-                    // Add button is enabled if at least one available item is selected
-                    // and we haven't reached the limit
-                    const hasSelectedAvailable = availableList.querySelector('li.selected:not(.disabled)') !== null;
-                    addButton.disabled = !hasSelectedAvailable ||
-                        (config.limit > 0 && selectedItems.length >= config.limit);
-
-                    // Remove button is enabled if at least one selected item is selected
-                    const hasSelectedItems = selectedList.querySelector('li.selected') !== null;
-                    removeButton.disabled = !hasSelectedItems;
-                }
-
-                // Initial search
-                performSearch();
-            })();
-        </script>
-        <?php
-    }
-
-    private function renderTrueFalseField(string $name, mixed $value, array $field):void
-    {
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        ?>
-        <div class="field true-false <?=$name?> row btw" <?=$conditional?> data-field="<?=$name?>">
-            <label class="toggle-switch row"<?=$describedBy?>>
-                <input
-                    type="checkbox"
-                    name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                    value="1"
-                    <?= ($value) ? ' checked':'' ?>
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-                >
-				<div class="slider"></div>
-				<span class="toggle-label"><?= esc_html($field['label']); ?></span>
-            </label>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-        <?php
-    }
-
-	private function renderUploadField(string $name, mixed $value, array $field): void
+	protected function renderHtmlField(string $name, mixed $value, array $field): void
 	{
-		$defaultConfig = [
-			//File Type
-			'subtype' => 'image', // 'image', 'video', 'document', 'any'
-			'accepted_types' => null, // null = use subtype defaults, or array of specific MIME types
-			//Upload Behaviour
-			'multiple' => false, // Single or multiple uploads
-			'limit' => 0, // Max number of uploads (0 = unlimited)
-			'mode' => 'direct', // 'direct' or 'selection'
-			//Destination
-			'destination' => 'meta', // 'meta', 'post', 'post_group'
-			//Processing Options
-			'max_size' => null, // Override default size limits
-			'convert' => 'webp', // Image conversion format
-			'quality' => 80, // Conversion quality
-			'create_thumbnails' => true,
-		];
-		$config = array_merge($defaultConfig, $field);
+		$method_name = $field['content'];
+		$content = '';
 
-		// Validate destination config
-		if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
-			error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined");
+		if (method_exists($this, $method_name)) {
+			$content = $this->$method_name();
+		}
+
+		if ($content === '') {
 			return;
 		}
 
-		// Get accepted types
-		$acceptedTypes = $this->getAllowedTypes($config);
+		echo sprintf(
+			'<div class="html-field-container" data-field-type="html" data-field="%s">%s</div>',
+			esc_attr($name),
+			$content
+		);
+	}
 
-		// Build accept attribute for input
-		$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
-		$acceptAttr = implode(',', $acceptExtensions);
+	/* ========== UTILITY METHODS ========== */
 
-		// Determine field attributes
-		$subtype = $config['subtype'] ?? 'image';
-		$multiple = $config['multiple'] ?? false;
-		$limit = $config['limit'] ?? 0;
-		$mode = $config['mode'] ?? 'direct';
-		$destination = $config['destination'];
-
-		// Get existing attachments
-		$attachmentIds = $this->parseAttachmentIds($value);
-
-		// Determine field type for UI
-		$fieldType = $multiple ? 'gallery' : 'single';
-
-		// Build data attributes
-		$dataAttrs = [
-			'data-field' => $name,
-			'data-upload-field' => '',
-			'data-mode' => $mode,
-			'data-type' => $fieldType,
-			'data-subtype' => $subtype,
-			'data-destination' => $destination,
-		];
-		if (!empty($field['content'])) {
-			$dataAttrs['data-content'] = $field['content'];
-		}
-		if ($limit > 0) {
-			$dataAttrs['data-limit'] = $limit;
+	private function handleConditionalField(array $field):string
+	{
+		if (empty($field['condition'])) {
+			return '';
 		}
 
-		// Build data attributes
-		$conditional = $this->handleConditionalField($field);
-		$describedBy = !empty($field['description']) ? ' aria-describedby="' . esc_attr($name) . '-help"' : '';
+		$condition = $field['condition'];
+		return sprintf(
+			'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"',
+			esc_attr($field['condition']['field']),
+			esc_attr($field['condition']['value']),
+			esc_attr($field['condition']['operator'] ?? '==')
+		);
+	}
 
-		if (!empty($field['group'])) {
-			$name = $field['group'] . '::' . $name;
-		}
-
-		// Convert data attributes to string
-		$dataAttrString = '';
-		foreach ($dataAttrs as $attr => $val) {
-			$dataAttrString .= ' ' . $attr . ($val !== '' ? '="' . esc_attr($val) . '"' : '');
-		}
+	protected function renderHint(string $hint): void
+	{
 		?>
-		<div class="field upload <?= esc_attr($name) ?>"
-			<?= $dataAttrString ?>
-			<?= $conditional ?>>
-
-			<div class="file-upload-container">
-				<div class="file-upload-wrapper">
-					<input type="file"
-						   name="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
-						   id="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
-						   accept="<?= esc_attr($acceptAttr) ?>"
-						   data-max-size="<?= esc_attr($this->getMaxFileSize($subtype)) ?>"
-						<?= $multiple ? 'multiple' : '' ?>
-						<?= !empty($field['required']) ? 'required' : '' ?>>
-
-					<h2><?= esc_html($field['label']) ?></h2>
-
-					<?php if (!empty($field['description'])) : ?>
-						<p><?= esc_html($field['description']) ?></p>
-					<?php endif; ?>
-
-					<p class="file-upload-text">
-						<strong>Click to upload</strong> or drag and drop<br>
-						<?= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
-						(max. <?= esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
-					</p>
-
-					<?php if ($destination === 'post_group') {
-						$plural = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['plural'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['plural'] : str_replace('_', ' ',$field['content']).'s');
-						$singular = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['singular'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['singular'] : str_replace('_', ' ',$field['content']));
-						?>
-						<p class="hint">You can group images to create separate <?= $plural ?>.</p>
-						<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
-					<?php }
-					if (!empty($field['upload_description'])) : ?>
-						<p><?= esc_html($field['upload_description']); ?></p>
-					<?php endif; ?>
-					<div class="file-error"></div>
-				</div>
-			</div>
-
-
-			<?php if ($destination === 'post_group') : ?>
-			<div class="group-display flex col" hidden>
-				<div class="preview-wrap flex col">
-					<div class="preview-actions">
-						<div class="selection-controls">
-							<div class="selected">
-								<div class="field">
-									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
-									<label for="select-all-uploads">
-										Select All
-									</label>
-								</div>
-								<div class="info" hidden>
-
-								</div>
-							</div>
-
-							<div class="selection-actions row btw" hidden>
-								<button type="button" data-action="add-to-group">
-									<?= jvbIcon('add') ?>
-									Group
-								</button>
-								<button type="button" data-action="delete-upload">
-									<?= jvbIcon('delete') ?>
-									Delete
-								</button>
-							</div>
-						</div>
-
-						<button type="button" data-action="upload" class="submit-uploads">
-							<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
-						</button>
-					</div>
-					<?php endif; ?>
-
-					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
-					<span class="count">0/0</span>'); ?>
-					<div class="item-grid preview">
-						<?php
-						// Render existing attachments
-						foreach ($attachmentIds as $attachmentId) {
-							echo $this->renderExistingAttachment($attachmentId, $subtype);
-						}
-						?>
-					</div>
-
-					<?php if ($destination === 'post_group') : ?>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
-				<div class="sidebar flex col">
-					<div class="header">
-						<h4>New <?= $plural?></h4>
-						<p class="hint">Drag or select multiple images into groups to create separate <?= $plural ?>.</p>
-					</div>
-					<div class="item-grid groups">
-						<div class="empty-group">
-							<p>Drag here to create a new <?= $singular ?>!</p>
-						</div>
-					</div>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
-			</div>
-		<?php endif; ?>
-
-			<?php if ($destination === 'meta') : ?>
-				<input type="hidden"
-					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					   value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>>
-			<?php endif; ?>
-		</div>
+		<span class="hint"><?= esc_html($hint) ?></span>
 		<?php
 	}
 
-
-	protected function getAllowedTypes(array $config):array
+	protected function renderDescription(string $description, string $name): void
 	{
-		$typeMap = [
-			'image' => [
-				'image/jpeg',
-				'image/png',
-				'image/gif',
-				'image/webp'
-			],
-			'video' => [
-				'video/mp4',
-				'video/webm',
-				'video/ogg',
-				'video/ogv',
-				'video/quicktime'
-			],
-			'document' => [
-				'application/pdf',
-				'application/msword',
-				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-				'application/vnd.ms-excel',
-				'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
-				'text/plain',
-				'text/csv'
-			],
-			'any' => [] // Will be merged from all types
-		];
-		// If specific types are defined, use those
+		?>
+		<p class="description" id="<?= esc_attr($name) ?>-help">
+			<?= wp_kses_post($description) ?>
+		</p>
+		<?php
+	}
+
+	protected function getAllowedTypes(array $config): array
+	{
 		if (!empty($config['accepted_types'])) {
-			return is_array($config['accepted_types'])
-				? $config['accepted_types']
-				: [$config['accepted_types']];
+			return $config['accepted_types'];
 		}
 
-		// Otherwise use subtype defaults
-		$subtype = $config['subtype'] ?? 'image';
-
-		if ($subtype === 'any') {
-			return array_merge(
-				$typeMap['image'],
-				$typeMap['video'],
-				$typeMap['document']
-			);
-		}
-
-		return $typeMap[$subtype] ?? $typeMap['image'];
-
-	}
-	/**
-	 * Parse attachment IDs from value
-	 */
-	private function parseAttachmentIds(mixed $value): array
-	{
-		if (empty($value)) return [];
-
-		if (is_array($value)) {
-			return array_filter(array_map('absint', $value));
-		}
-
-		return array_filter(array_map('absint', explode(',', $value)));
-	}
-
-	/**
-	 * Get file extensions for MIME types
-	 */
-	private function getMimeExtensions(array $mimeTypes): array
-	{
-		$extensionMap = [
-			'image/jpeg' => ['.jpg', '.jpeg'],
-			'image/png' => ['.png'],
-			'image/gif' => ['.gif'],
-			'image/webp' => ['.webp'],
-			'video/mp4' => ['.mp4'],
-			'video/webm' => ['.webm'],
-			'video/ogg' => ['.ogg'],
-			'video/ogv' => ['.ogv'],
-			'video/quicktime' => ['.mov'],
-			'application/pdf' => ['.pdf'],
-			'application/msword' => ['.doc'],
-			'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['.docx'],
-			'application/vnd.ms-excel' => ['.xls'],
-			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['.xlsx'],
-			'text/plain' => ['.txt'],
-			'text/csv' => ['.csv'],
+		// Default types based on subtype
+		$defaults = [
+			'image' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
+			'video' => ['video/mp4', 'video/webm', 'video/ogg'],
+			'document' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
+			'any' => ['image/*', 'video/*', 'application/pdf']
 		];
 
+		return $defaults[$config['subtype']] ?? $defaults['image'];
+	}
+
+	protected function getMimeExtensions(array $mimeTypes): array
+	{
 		$extensions = [];
 		foreach ($mimeTypes as $mime) {
-			if (isset($extensionMap[$mime])) {
-				$extensions = array_merge($extensions, $extensionMap[$mime]);
+			if (str_contains($mime, '*')) {
+				continue; // Skip wildcards
 			}
+			$ext = str_replace(['image/', 'video/', 'application/'], '', $mime);
+			$extensions[] = '.' . $ext;
 		}
-
-		return array_unique($extensions);
+		return $extensions;
 	}
 
-	/**
-	 * Get max file size for subtype
-	 */
-	private function getMaxFileSize(string $subtype): int
+	protected function parseAttachmentIds(mixed $value): array
 	{
-		$sizes = [
-			'image' => 5242880,    // 5MB
-			'video' => 104857600,  // 100MB
-			'document' => 10485760 // 10MB
-		];
-
-		return $sizes[$subtype] ?? $sizes['image'];
-	}
-	/**
-	 * Get human-readable file size label
-	 */
-	private function getMaxFileSizeLabel(string $subtype): string
-	{
-		$bytes = $this->getMaxFileSize($subtype);
-		$mb = round($bytes / 1048576);
-		return "{$mb}MB";
-	}
-	/**
-	 * Format file size for display
-	 */
-	private function formatFileSize(int $bytes): string
-	{
-		if ($bytes >= 1073741824) {
-			return number_format($bytes / 1073741824, 1) . 'GB';
-		}
-		if ($bytes >= 1048576) {
-			return number_format($bytes / 1048576, 1) . 'MB';
-		}
-		if ($bytes >= 1024) {
-			return number_format($bytes / 1024, 1) . 'KB';
-		}
-		return $bytes . 'B';
-	}
-
-	/**
-	 * Get accepted types label
-	 */
-	private function getAcceptedTypesLabel(string $subtype, array $extensions): string
-	{
-		$labels = [
-			'image' => 'JPG, PNG, GIF, or WEBP',
-			'video' => 'MP4, WEBM, or MOV',
-			'document' => 'PDF, DOC, XLS, or TXT',
-			'any' => 'Images, Videos, or Documents'
-		];
-
-		return $labels[$subtype] ?? strtoupper(implode(', ', array_map(function($ext) {
-			return ltrim($ext, '.');
-		}, array_slice($extensions, 0, 3))));
-	}
-
-	/**
-	 * Render existing attachment
-	 */
-	private function renderExistingAttachment(int $attachmentId, string $subtype): string
-	{
-		$attachment = get_post($attachmentId);
-		if (!$attachment) return '';
-
-		$url = wp_get_attachment_url($attachmentId);
-		$thumbUrl = $subtype === 'image'
-			? wp_get_attachment_image_url($attachmentId, 'medium')
-			: $url;
-
-		ob_start();
-		?>
-		<div class="upload-item existing" data-attachment-id="<?= esc_attr($attachmentId) ?>" data-subtype="<?= esc_attr($subtype) ?>">
-			<div class="preview">
-				<?php if ($subtype === 'image') : ?>
-					<img src="<?= esc_url($thumbUrl) ?>" alt="<?= esc_attr(get_post_meta($attachmentId, '_wp_attachment_image_alt', true)) ?>">
-				<?php elseif ($subtype === 'video') : ?>
-					<video src="<?= esc_url($url) ?>" controls></video>
-				<?php else : ?>
-					<div class="document-preview">
-						<?= jvbIcon('document') ?>
-						<span><?= esc_html(basename($url)) ?></span>
-					</div>
-				<?php endif; ?>
-
-				<div class="overlay">
-					<div class="actions">
-						<button type="button" class="remove" title="Remove">
-							<span class="screen-reader-text">Remove <?= esc_attr($subtype) ?></span>
-							×
-						</button>
-					</div>
-				</div>
-			</div>
-
-			<?php if ($subtype === 'image') {
-				echo jvbImageMeta();
-			} ?>
-		</div>
-		<?php
-		return ob_get_clean();
-	}
-
-    private function renderImageField(string $name, mixed $value, array $field):void
-    {
-		$image_url = $title = $alt = $caption = false;
-		if ($value !== 0 || $value !== '') {
-			$image_url = wp_get_attachment_image_url((int)$value, 'medium') ?: false;
-			$caption = wp_get_attachment_caption((int)$value);
-			$alt = get_post_meta((int)$value, '_wp_attachment_image_alt', true);
-			$title = get_the_title((int)$value);
+		if (empty($value)) {
+			return [];
 		}
 
-        $mode = array_key_exists('mode', $field) ? $field['mode'] : 'direct';
-        $multiple = ($mode === 'selection' || isset($field['multiple']));
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		$groupable = (array_key_exists('imageType', $field) && $field['imageType'] === 'groupable');
-		$singular = (array_key_exists('singular', $field)) ? $field['singular'] : 'post';
-		$plural = (array_key_exists('plural', $field)) ? $field['plural'] : 'posts';
-		$dataContent = (array_key_exists('content', $field)) ? ' data-content="'.$field['content'].'"' : '';
-		$dataType = ($groupable) ? 'groupable' : (($multiple) ? 'gallery' : 'single');
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        ?>
-        <div class="field image <?=$name?>"
-             data-field="<?= esc_attr($name); ?>"
-			 data-upload-field
-             data-mode="<?= esc_attr($mode); ?>"
-			 <?=$dataContent?>
-			<?= ' data-type="'.$dataType.'"'?>>
-
-            <div class="file-upload-container">
-                <div class="file-upload-wrapper">
-                    <input type="file"
-                           name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
-                           id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
-                           accept=".jpg,.jpeg,.png,.gif,.webp"
-                           data-max-size="<?= $this->max_file_size; ?>"
-                        <?= $multiple ? 'multiple' : ''; ?>>
-                    <h2><?= esc_html($field['label']); ?></h2>
-                    <?php if (!empty($field['description'])) : ?>
-                        <p><?= esc_html($field['description']); ?></p>
-                    <?php endif; ?>
-                    <p class="file-upload-text">
-                        <strong>Click to upload</strong> or drag and drop<br>
-                        JPG, PNG, GIF, or WEBP (max. 5MB)
-                    </p>
-					<?php if ($groupable) { ?>
-					<p class="hint">You can group images to create separate <?= $plural ?>.</p>
-					<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
-					<?php } ?>
-                    <?php if (!empty($field['upload_description'])) : ?>
-                        <p><?= esc_html($field['upload_description']); ?></p>
-                    <?php endif; ?>
-                </div>
-                <div class="file-error"></div>
-            </div>
-			<?php if ($groupable) : ?>
-			<div class="group-display" hidden>
-				<div class="preview-wrap">
-					<div class="preview-actions">
-						<div class="selection-controls">
-							<div class="selected">
-								<div class="field">
-									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
-									<label for="select-all-uploads">
-										Select All
-									</label>
-								</div>
-								<div class="info" hidden>
-									With <span class="selection-count">0</span> selected
-								</div>
-							</div>
-
-
-							<!-- Selection actions (hidden by default) -->
-							<div class="selection-actions" hidden>
-								<button type="button" class="create-from-selection">
-									<?= jvbIcon('add') ?>
-									Create New <?= $singular ?>
-								</button>
-								<button type="button" class="remove-selection">
-									<?= jvbIcon('delete') ?>
-									Remove
-								</button>
-							</div>
-						</div>
-
-						<button type="button" class="submit-uploads">
-							<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
-						</button>
-					</div>
-					<?php endif; ?>
-
-					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
-					<span class="count">0/0</span>'); ?>
-					<div class="item-grid preview">
-						<?php if ($image_url) {
-							echo jvbRenderImageForm((int)$value);
-						} ?>
-					</div>
-
-					<?php if ($groupable) : ?>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
-				<div class="sidebar">
-					<div class="header">
-						<h4>New <?= $plural?></h4>
-						<p class="hint">Drag images into groups to create separate <?= $plural ?>.</p>
-						<p class="hint">Select multiple images and click "Add to <?= $singular?>" or create new <?= $plural ?>.</p>
-					</div>
-					<button type="button" class="create-group-from-selection">
-						<?= jvbIcon('add') ?>
-						Create New <?= $singular ?>
-					</button>
-					<div class="item-grid groups">
-						<div class="empty-group">
-							<p>Drag here to create a new <?= $singular ?>!</p>
-						</div>
-					</div>
-					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
-				</div>
-			</div>
-		<?php endif; ?>
-
-			<?php if ($mode === 'direct') : ?>
-				<input type="hidden"
-					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					   value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>>
-			<?php endif; ?>
-		</div>
-		<?php
-    }
-
-    protected function renderGalleryField(string $name, string|null|false $value, array $field):void
-    {
-        $ids = ($value === '' || is_null($value) || !$value) ? [] : explode(',',$value);
-
-        if (!empty($ids)) {
-            $ids = array_map('absint', $ids);
-        }
-
-        $conditional = $this->handleConditionalField($field);
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-		//TODO: This can probably just be a wrapper for renderImageField...
-        ?>
-        <div class="field gallery <?=$name?>"
-             data-field="<?= esc_attr($name); ?>"
-            <?= $conditional ?>>
-
-            <label><?= esc_html($field['label']); ?></label>
-
-            <!-- Container for existing images -->
-            <div class="gallery-preview">
-                <?php
-                if (!empty($ids)) {
-                    foreach ($ids as $id) {
-                        $url = wp_get_attachment_image_url($id, 'medium');
-                        if ($url) {
-                            echo '<div class="preview-item" data-id="' . esc_attr($id) . '">';
-                            echo '<img src="' . esc_url($url) . '" alt="">';
-                            echo '<button type="button" class="remove-preview">' . jvbIcon('trash', ['title'=>'Remove']) . '</button>';
-                            echo '</div>';
-                        }
-                    }
-                }
-                ?>
-            </div>
-
-            <!-- Hidden file uploader that will be managed by BatchFileUploader -->
-            <div class="file-upload-container">
-                <div class="file-upload-wrapper">
-                    <input type="file"
-                           name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
-                           accept=".jpg,.jpeg,.png,.gif,.webp"
-                           multiple>
-                    <p class="file-upload-text">
-                        <strong>Click to upload</strong> or drag and drop<br>
-                        JPG, PNG, GIF, or WEBP (max. 5MB)
-                    </p>
-                </div>
-                <div class="file-error"></div>
-            </div>
-
-            <!-- Hidden input for storing the IDs -->
-            <input type="hidden"
-                   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                   value="<?= esc_attr($value); ?>"
-				<?= !empty($field['required']) ? 'required' : ''; ?>>
-
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-        </div>
-
-        <?php
-    }
-    private function renderSelectField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $conditional = $this->handleConditionalField($field);
-        $default = isset($field['default']) ? $field['default'] : '';
-        $value = !empty($value) ? $value : $default;
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" data-field="<?=$name?>" <?=$conditional?>>
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-            </label>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-            <select
-                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-				<?=$describedBy?>
-                <?= !empty($field['required']) ? 'required' : ''; ?>
-            >
-                <?php foreach ($field['options'] as $key => $label) : ?>
-                    <option value="<?= esc_attr($key); ?>"
-                        <?php selected($value, $key); ?>>
-                        <?= esc_html($label); ?>
-                    </option>
-                <?php endforeach; ?>
-            </select>
-        </div>
-        <?php
-    }
-
-    protected function renderHtmlField(string $name, mixed $value, array $field):void
-    {
-        $method_name = $field['content'];
-        $content = '';
-        if (method_exists($this, $method_name)) {
-            $content = $this->$method_name();
-        }
-
-        echo ($content == '') ? '' : sprintf(
-            '<div class="html-field-container" data-field-type="html" data-field="%s">%s</div>',
-            esc_attr($name),
-            $content
-        );
-    }
-
-    private function renderDateField(string $name, mixed $value, array $field):void
-    {
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-        $conditional = $this->handleConditionalField($field);
-        $format = !empty($field['format']) ? $field['format'] : 'Y-m-d';
-
-        // Format the date if we have a value
-        if (!empty($value)) {
-            $date = DateTime::createFromFormat($format, $value);
-            if ($date) {
-                $value = $date->format('Y-m-d'); // HTML date input requires Y-m-d format
-            }
-        }
-        if (array_key_exists('group', $field)) {
-            $name = $field['group'].'::'.$name;
-        }
-        ?>
-         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-            <label for="<?= esc_attr($name); ?>">
-                <?= esc_html($field['label']); ?>
-            </label>
-            <div class="date-wrapper"<?=$describedBy?>>
-                <input
-                    type="date"
-                    id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                    name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-                    value="<?= esc_attr($value); ?>"
-                    <?= !empty($field['required']) ? 'required' : ''; ?>
-                    data-format="<?= esc_attr($format); ?>"
-                >
-                <?= jvbIcon('event') ?>
-            </div>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-
-        </div>
-        <?php
-    }
-
-	public function renderTimeField(string $name, mixed $value, array $field):void
-	{
-		$conditional = $this->handleConditionalField($field);
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		// Convert various time formats to HTML time input format (HH:MM)
-		if (!empty($value)) {
-			// If it's already in HH:MM format, use as-is
-			if (preg_match('/^\d{2}:\d{2}$/', $value)) {
-				// Value is already in correct format
-			} else {
-				// Try to parse and convert
-				$timestamp = strtotime($value);
-				if ($timestamp !== false) {
-					$value = date('H:i', $timestamp);
-				} else {
-					$value = '';
-				}
-			}
+		if (is_array($value)) {
+			return array_filter($value, 'is_numeric');
 		}
 
-		if (array_key_exists('group', $field)) {
-			$name = $field['group'].'::'.$name;
+		if (is_string($value)) {
+			return array_filter(explode(',', $value), 'is_numeric');
 		}
-		?>
-		 <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-			<label for="<?= esc_attr($name); ?>">
-				<?= esc_html($field['label']); ?>
-			</label>
-			<div class="time-wrapper"<?=$describedBy?>>
-				<input
-					type="time"
-					id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-					<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
-					<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
-					<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
-				>
 
-			</div>
-			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-		</div>
-		<?php
+		return is_numeric($value) ? [$value] : [];
 	}
 
-	private function renderDatetimeField(string $name, mixed $value, array $field):void
-	{
-		$conditional = $this->handleConditionalField($field);
-		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
-		// Convert datetime to HTML datetime-local format (YYYY-MM-DDTHH:MM)
-		if (!empty($value)) {
-			$date = DateTime::createFromFormat('Y-m-d H:i:s', $value);
-			if (!$date) {
-				// Try alternative formats
-				$formats = ['Y-m-d\TH:i:s', 'Y-m-d\TH:i', 'Y-m-d H:i'];
-				foreach ($formats as $format) {
-					$date = DateTime::createFromFormat($format, $value);
-					if ($date) break;
-				}
-			}
-
-			if ($date) {
-				$value = $date->format('Y-m-d\TH:i'); // HTML datetime-local format
-			} else {
-				$value = '';
-			}
-		}
-
-		if (array_key_exists('group', $field)) {
-			$name = $field['group'].'::'.$name;
-		}
-		?>
-		<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
-			<label for="<?= esc_attr($name); ?>">
-				<?= esc_html($field['label']); ?>
-			</label>
-			<div class="datetime-wrapper"<?=$describedBy?>>
-				<input
-					type="datetime-local"
-					id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
-					value="<?= esc_attr($value); ?>"
-					<?= !empty($field['required']) ? 'required' : ''; ?>
-					<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
-					<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
-					<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
-				>
-				<?= jvbIcon('event') ?>
-			</div>
-			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
-			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
-		</div>
-		<?php
-	}
-
-
-    public function outputCharacterCountJS():void
-    {
-        ?>
-        <script>
-            document.querySelectorAll('[maxlength]').forEach(field => {
-                const counter = field.closest('.field')?.querySelector('.char-count .current');
-                if (counter) {
-                    const updateCount = () => counter.textContent = field.value.length;
-                    field.addEventListener('input', updateCount);
-                    updateCount();
-                }
-            });
-        </script>
-        <?php
-    }
-
-
-    //Conditional Fields
-    private function handleConditionalField(array $field):string
-    {
-        if (empty($field['condition'])) {
-            return '';
-        }
-
-        $condition = $field['condition'];
-        return sprintf(
-            'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"',
-            esc_attr($field['condition']['field']),
-            esc_attr($field['condition']['value']),
-            esc_attr($field['condition']['operator'] ?? '==')
-        );
-    }
-
-	protected function renderDescription(string $description, string $name):void
-	{
-		$id = $name.'-help';
-		$out = '<div class="has-tooltip">
-		<span class="tt-toggle">'.jvbIcon('help').'</span>
-		<div role="tooltip" id="'.$id.'"><p>'.$description.'</p></div>
-		</div>';
-		echo $out;
-	}
-
-	protected function renderHint(array|string $hint):void
-	{
-		if (is_array($hint)) {
-			$out = '';
-			foreach($hint as $h) {
-				$out .= '<p class="hint">'.$h.'</p>';
-			}
-		} else {
-			$out = '<p class="hint">'.$hint.'</p>';
-		}
-		echo $out;
-	}
 }
diff --git a/inc/meta/MetaFormOld.php b/inc/meta/MetaFormOld.php
new file mode 100644
index 0000000..1c29732
--- /dev/null
+++ b/inc/meta/MetaFormOld.php
@@ -0,0 +1,2128 @@
+<?php
+namespace JVBase\inc\meta;
+
+use JVBase\forms\TaxonomySelector;
+use JVBase\forms\PostSelector;
+use DateTime;
+
+if (!defined('ABSPATH')) {
+    exit; // Exit if accessed directly
+}
+
+/**
+ * Renders the form fields for managing the meta
+ */
+class MetaFormOld
+{
+    protected int $max_file_size = 5242880;
+
+    //Rendering fields
+    public function render(string $name, mixed $value, array $config, bool $showHidden = false, bool $return = false):mixed
+    {
+		$out = '';
+        if (jvbCheck('hidden', $config) && !$showHidden) {
+            return $out;
+        }
+        // Get conditional attributes if they exist
+        $conditional = array_key_exists('condition', $config) ?
+            $this->handleConditionalField($config) : '';
+
+        if (!array_key_exists('type', $config)) {
+            return $out;
+        }
+
+		if (array_key_exists('display', $config) && $config['display'] === 'hidden'){
+			$out = '<input type="hidden" name="'.$name.'" value="'.$value.'" />';
+			if (!$return) {
+				echo $out;
+			}
+			return $out;
+		}
+
+		ob_start();
+        $type = array_map( 'ucfirst', explode('_', $config['type']));
+        $type = implode('', $type);
+        $method = 'render' . $type . 'Field';
+
+		$nameTemp = implode('', array_map('ucfirst', explode('_', $name)));
+		$nameMethod = 'render'.$nameTemp.'Field';
+		if(function_exists($nameMethod)) {
+			call_user_func($nameMethod, $value, $config);
+		} elseif (function_exists($method)) {
+			call_user_func($method, $value, $config);
+		} elseif (method_exists($this, $method)) {
+            $this->$method($name, $value, $config);
+        }
+
+
+
+		$out = ob_get_clean();
+
+		do_action('jvbRenderFormField', $name, $config, $value);
+		$out = apply_filters('jvbFilterRenderFormField', $out, $name, $config, $value);
+		if (!$return) {
+			echo $out;
+		}
+		return $out;
+    }
+
+    public function renderTextField(string $name, mixed $value, array $field):void
+    {
+        // Use field-specific value if provided, otherwise use the meta value
+        $display_value = isset($field['value']) ? $field['value'] : $value;
+        $conditional = $this->handleConditionalField($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
+		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+            <label for="<?= esc_attr($name); ?>">
+                <?= esc_html($field['label']); ?>
+                <?php if (!empty($field['limit'])) : ?>
+                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
+                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
+                </span>
+                <?php endif; ?>
+            </label>
+            <input
+                type="<?= esc_attr($field['subtype']??'text'); ?>"
+                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                value="<?= esc_attr($display_value); ?>"
+				<?= $placeholder ?>
+				<?= $autocomplete ?>
+                <?= !empty($field['required']) ? 'required' : ''; ?>
+				<?= $describedBy ?>
+            >
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+            <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+        if (array_key_exists('limit', $field)) {
+            $this->outputCharacterCountJS();
+        }
+    }
+
+    private function renderTelField(string $name, mixed $value, array $field):void
+    	{$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        // Use field-specific value if provided, otherwise use the meta value
+        $display_value = isset($field['value']) ? $field['value'] : $value;
+        $conditional = $this->handleConditionalField($field);
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+			$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
+		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+            <label for="<?= esc_attr($name); ?>">
+                <?= esc_html($field['label']); ?>
+                <?php if (!empty($field['limit'])) : ?>
+                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
+                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
+                </span>
+                <?php endif; ?>
+            </label>
+            <input
+                type="tel"
+                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                value="<?= esc_attr($display_value); ?>"
+				<?= $placeholder ?>
+				<?= $autocomplete?>
+				<?= $describedBy ?>
+                <?= !empty($field['required']) ? 'required' : ''; ?>
+            >
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+        if (array_key_exists('limit', $field)) {
+            $this->outputCharacterCountJS();
+        }
+    }
+    private function renderEmailField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        // Use field-specific value if provided, otherwise use the meta value
+        $display_value = isset($field['value']) ? $field['value'] : $value;
+        $conditional = $this->handleConditionalField($field);
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
+		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+            <label for="<?= esc_attr($name); ?>">
+                <?= esc_html($field['label']); ?>
+                <?php if (!empty($field['limit'])) : ?>
+                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
+                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
+                </span>
+                <?php endif; ?>
+            </label>
+            <input
+                type="email"
+			 <?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                value="<?= esc_attr($display_value); ?>"
+				<?= $placeholder ?>
+				<?= $autocomplete ?>
+				<?= $describedBy ?>
+                <?= !empty($field['required']) ? 'required' : ''; ?>
+            >
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+        if (array_key_exists('limit', $field)) {
+            $this->outputCharacterCountJS();
+        }
+    }
+
+    private function renderUrlField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        // Use field-specific value if provided, otherwise use the meta value
+        $display_value = isset($field['value']) ? $field['value'] : $value;
+        $conditional = $this->handleConditionalField($field);
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
+		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+            <label for="<?= esc_attr($name); ?>">
+                <?= esc_html($field['label']); ?>
+                <?php if (!empty($field['limit'])) : ?>
+                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
+                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
+                </span>
+                <?php endif; ?>
+            </label>
+            <input
+                type="url"
+                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                value="<?= esc_attr($display_value); ?>"
+				<?= $placeholder ?>
+				<?= $describedBy ?>
+				<?= $autocomplete ?>
+                <?= !empty($field['required']) ? 'required' : ''; ?>
+            >
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+        if (array_key_exists('limit', $field)) {
+            $this->outputCharacterCountJS();
+        }
+    }
+
+    private function renderNumberField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+		$description = '<ul class="list-none"><li>Tip: hold Ctrl/Command to increase 5x</li><li>Shift to increase 10x,</li><li>Or Ctrl/Command + Shift to increase 50x</li></ul>';
+		$description .= $field['description']??'';
+        $conditional = $this->handleConditionalField($field);
+
+		$min = isset($field['min']) ? (float)$field['min'] : 0;
+		$max = isset($field['max']) ? (float)$field['max'] : 100;
+		$step = isset($field['step']) ? (float)$field['step'] : 1;
+
+		$data = '';
+		if (array_key_exists('data', $field) && !empty($field['data'])) {
+			foreach($field['data'] as $key => $v) {
+				if ($v === '') {
+					$data .= ' data-'.$key;
+				} else {
+					$data .= ' data-'.$key.'="'.$v.'"';
+				}
+			}
+		}
+
+		if (empty($value)) {
+			$value = $field['default']??0;
+		}
+
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$autocomplete = (array_key_exists('autocomplete', $field)) ? ' autocomplete="'.$field['autocomplete'].'"' : '';
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?> row" <?=$conditional?> data-field="<?=$name?>">
+            <label for="<?= esc_attr($name); ?>">
+                <?= esc_html($field['label']); ?>
+            </label>
+
+			 <div class="quantity"
+				  <?=$data?>>
+
+				 <button type="button"
+						 class="decrease"
+						 title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
+						 aria-label="Decrease <?= esc_attr($field['label']); ?>">
+					 <?= jvbIcon('minus')?>
+				 </button>
+
+				 <input type="number"
+						id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+						name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+						value="<?= esc_attr($value); ?>"
+						min="<?= esc_attr($min); ?>"
+						max="<?= esc_attr($max); ?>"
+						step="<?= esc_attr($step); ?>"
+						class="quantity-input"
+						<?= $describedBy ?>
+						<?= $autocomplete ?>
+					 <?= !empty($field['required']) ? 'required' : ''; ?>>
+
+				 <button type="button"
+						 class="increase"
+						 title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
+						 aria-label="Increase <?= esc_attr($field['label']); ?>">
+					 <?= jvbIcon('add')?>
+				 </button>
+			 </div>
+			 <?php $this->renderDescription($description, $name);  ?>
+        </div>
+        <?php
+    }
+
+    public function renderTextareaField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        $rows = isset($field['rows']) ? (int)$field['rows'] : 4;
+        $conditional = $this->handleConditionalField($field);
+        $quill = (array_key_exists('quill', $field) && $field['quill'] == true) ? ' data-editor="true"' : '';
+        if ($quill !== '') {
+            $allowImages = array_key_exists('allowImage', $field);
+            $quill .= ($allowImages) ? ' data-allowimage="true"' : ' data-allowimage="false"';
+        }
+        // Handle array values
+        if (is_array($value)) {
+            $value = implode(', ', $value);
+        }
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$placeholder = (array_key_exists('placeholder', $field)) ? ' placeholder="'.$field['placeholder'].'"' : '';
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+            <label for="<?= esc_attr($name ?? ''); ?>">
+                <?= esc_html($field['label']); ?>
+                <?php if (!empty($field['limit'])) : ?>
+                    <span class="char-count" data-limit="<?= esc_attr($field['limit']); ?>">
+                    <span class="current">0</span>/<?= esc_attr($field['limit']); ?>
+                </span>
+                <?php endif; ?>
+            </label>
+            <textarea
+                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
+                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name ?? ''); ?>"
+                    <?= $quill ?>
+                    rows="<?= esc_attr($rows); ?>"
+				<?= $placeholder ?>
+				<?= $describedBy ?>
+				<?= !empty($field['required']) ? 'required' : ''; ?>
+            <?= !empty($field['limit']) ? 'data-limit="' . esc_attr($field['limit']) . '"' : ''; ?>
+        ><?= esc_textarea($value); ?></textarea>
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+    }
+
+    private function renderSetField(string $name, mixed $value, array $field):void
+    {
+        $this->renderCheckboxField($name, $value, $field);
+    }
+
+    private function renderCheckboxField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        $value = !is_array($value) ? explode(',', $value) : $value;
+        $limit = isset($field['limit']) ? (int)$field['limit'] : 0;
+        $conditional = $this->handleConditionalField($field);
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+        ?>
+        <div class="field checkbox-group" <?= $limit ? 'data-limit="' . esc_attr($limit) . '"' : ''; ?> <?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
+            <span class="label"><?= esc_html($field['label']); ?></span>
+            <div class="checkbox-options flex">
+            <?php foreach ($field['options'] as $key => $label) : ?>
+                <input
+                    type="checkbox"
+                    id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key)?>"
+					<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name)?>[]"
+                    value="<?= esc_attr($key); ?>"
+                    <?= (in_array($key, $value)) ? 'checked' : ''; ?>
+					<?= !empty($field['required']) ? 'required' : ''; ?>
+                >
+                <label class="checkbox-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
+                    <?= esc_html($label); ?>
+                </label>
+            <?php endforeach; ?>
+        </div>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+    }
+    private function renderRadioField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        $value = (array)$value;
+        $conditional = $this->handleConditionalField($field);
+
+        if (!array_key_exists('label', $field)) {
+            error_log('No label for: '.print_r($name, true));
+        }
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+        ?>
+        <div class="field radio-group"<?=$conditional?> data-field="<?=$name?>"<?=$describedBy?>>
+            <label><?= esc_html($field['label']); ?></label>
+
+            <div class="radio-options row">
+                <?php foreach ($field['options'] as $key => $label) : ?>
+                    <input
+                        type="radio"
+                        id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name).'-'.esc_attr($key) ?>"
+                        name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                        value="<?= esc_attr($key); ?>"
+						<?= !empty($field['required']) ? 'required' : ''; ?>
+                        <?= (in_array($key, $value)) ? 'checked' : ''; ?>
+                    >
+                    <label class="radio-option" for="<?= esc_attr($name).'-'.esc_attr($key) ?>">
+                        <?= esc_html($label); ?>
+                    </label>
+                <?php endforeach; ?>
+            </div>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+
+        </div>
+        <?php
+    }
+
+    private function renderRepeaterField(string $name, mixed $value, array $field):void
+    {
+		error_log('Rendering Repeater Field!');
+        $values = is_array($value) ? $value : array();
+
+        $conditional = $this->handleConditionalField($field);
+        $row_label = isset($field['row_label']) ? $field['row_label'] : '';
+		$rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item';
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        ?>
+        <div class="field repeater <?=$name?>"
+             data-field="<?= esc_attr($name); ?>"
+			 <?= $describedBy ?>
+            <?= $row_label ? 'data-label="' . esc_attr($row_label) . '"' : ''; ?>
+            <?=$conditional?>>
+            <?php
+            if (!array_key_exists('label', $field)) {
+                error_log('No label for: '.print_r($name, true));
+            }
+            ?>
+            <h3><?= esc_html($field['label']); ?></h3>
+
+
+            <div class="repeater-items">
+                <?php
+                if (!empty($values)) {
+                    foreach ($values as $index => $row) {
+                        $this->renderRepeaterRow($field['fields'], $row, $index, $name, $rowTitle);
+                    }
+                }
+                ?>
+            </div>
+
+            <template class="<?=uniqid('repeaterTemplate')?>">
+                <?php $this->renderRepeaterRow($field['fields'], array(), '', '', $rowTitle); ?>
+            </template>
+
+            <button type="button" class="add-repeater-row">
+                <?= jvbIcon('plus', ['title'=> 'Add']); ?> <?= (array_key_exists('add_label', $field)) ? $field['add_label'] : 'Add Item'; ?>
+            </button>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+    }
+
+    private function renderRepeaterRow(array $fields, array $values, int|string $index, string $base_name, string $rowTitle = 'New Item'):void
+    {
+        $display_number = (is_string($index)) ? $index : ($index + 1);
+        ?>
+        <div class="repeater-row" data-index="<?= esc_attr($index); ?>">
+            <details <?= (is_string($index)) ? 'open' : ''; ?>>
+                <summary class="repeater-row-header row btw">
+                    <span class="drag-handle"><?= jvbIcon('grab'); ?></span>
+                    <span class="row-number">#<?= esc_html($display_number); ?></span>
+                    <span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span>
+                    <button type="button" class="remove-row" title="Remove">
+                        <?= jvbIcon('delete', ['title'=>'Remove']); ?>
+                    </button>
+                </summary>
+                <div class="repeater-row-content">
+                    <?php
+                    foreach ($fields as $slug => $field) :
+                        if ($base_name === '') {
+                            $field_name = $slug;
+                        } else {
+                            $field_name = sprintf('%s:%s:%s', $base_name, $index, $slug);
+                        }
+                        $field_value = isset($values[$slug]) ? $values[$slug] : '';
+                        $name = $field_name;
+                        $this->render($name, $field_value, $field);
+                    endforeach;
+                    ?>
+                </div>
+            </details>
+        </div>
+        <?php
+    }
+
+    private function getRowTitle(array $fields, array $values, string $rowTitle):string
+    {
+        // Try to find the first text field or textarea value to use as title
+        foreach ($fields as $slug => $field) {
+            if (in_array($field['type'], ['text', 'textarea']) &&
+                isset($values[$slug]) &&
+                !empty($values[$slug])) {
+                return $values[$slug];
+            }
+        }
+        return $rowTitle;
+    }
+
+    private function renderTaxonomyField(string $name, mixed $value, array $field):void
+    {
+		$conditional = $this->handleConditionalField($field);
+		$taxonomy = $field['taxonomy'];
+
+		// Get currently selected terms
+		$selected_terms = ($value === '') ? [] : explode(',', $value);
+
+
+		// Convert selected term IDs to the format expected by single modal
+		$processedSelected = [];
+		if (!empty($selected_terms)) {
+			foreach ($selected_terms as $termId) {
+				if (is_numeric($termId)) {
+					$term = get_term($termId, $taxonomy);
+					if ($term && !is_wp_error($term)) {
+						$processedSelected[$term->term_id] = [
+							'name' => $term->name,
+							'path' => TaxonomySelector::getTermPath($term)
+						];
+					}
+				}
+			}
+		}
+
+		// Create configuration for single modal system
+		$config = [
+			'taxonomy' => $taxonomy,
+			'max' => $field['limit'] ?? 0,
+			'search' => $field['search'] ?? true,
+			'createNew' => $field['createNew'] ?? false,
+			'selected' => $processedSelected,
+			'base' => $field['base'] ?? '',
+		];
+
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+		?>
+		<div class="field taxonomy <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
+			<div class="field-group-header">
+				<label class="toggle">
+					<?= jvbIcon(str_replace(BASE, '', $taxonomy)) ?>
+					<?= esc_html($field['label']) ?>
+				</label>
+			</div>
+
+			<?php
+			$tax = new TaxonomySelector($name, $taxonomy, $config);
+			$extra = '<input type="hidden"
+				   name="'.esc_attr($name).'"
+				   id="'.esc_attr($name).'"'.$describedBy.'
+				   data-taxonomy="'.esc_attr($taxonomy).'"
+				   value="'.esc_attr(is_array($selected_terms) ? implode(',', $selected_terms) : $selected_terms).'">';
+			echo $tax->render([], $extra);
+			?>
+
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+		</div>
+		<?php
+    }
+
+    protected function renderPostSelectorField(string $name, mixed $value, array $field):void
+    {
+		$conditional = $this->handleConditionalField($field);
+
+		// Process selected posts
+		$selected_posts = $value;
+		if (is_string($selected_posts)) {
+			$selected_posts = !empty($selected_posts) ? explode(',', $selected_posts) : [];
+		} elseif (!is_array($selected_posts)) {
+			$selected_posts = [];
+		}
+
+		// Configure the post selector
+		$config = [
+			'multiple' => $field['multiple'] ?? true,
+			'maxSelections' => $field['limit'] ?? 0,
+			'search' => true,
+			'placeholder' => $field['placeholder'] ?? 'Search posts...',
+			'noResults' => 'No posts found',
+			'shop_id' => $field['shop_id'] ?? null,
+			'onClose' => 'updateMetaFormPost'
+		];
+
+		$postSelector = new PostSelector($field['post_type'], $config);
+		$containerId = $name . '-post-selector';
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+		?>
+		<div class="field post-selector <?=$name?>" <?= $conditional ?> data-field="<?=$name?>">
+			<div class="field-group-header">
+				<label class="toggle">
+					<?= jvbIcon($field['post_type'] . '-selector') ?>
+					<?= esc_html($field['label'] ?? ucfirst($field['post_type'])) ?>
+				</label>
+				<button title="Add <?= esc_attr(ucfirst($field['post_type'])) ?>"
+						class="add-item-btn"
+						type="button">
+					<?= jvbIcon('add', ['title' => "Add " . ucfirst($field['post_type'])]) ?>
+				</button>
+			</div>
+
+			<?= $postSelector->render($selected_posts, $containerId) ?>
+
+			<!-- Hidden input for form submission -->
+			<input type="hidden"
+				   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name) ?>"
+				   class="post-selector-input"
+				   <?= $describedBy ?>
+				   data-post-type="<?= esc_attr($field['post_type']) ?>"
+				   value="<?= esc_attr(is_array($selected_posts) ? implode(',', $selected_posts) : $selected_posts) ?>">
+
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+		</div>
+		<?php
+    }
+
+	protected function renderGroupField(string $name, mixed $value, array $field):void
+	{
+		if (!array_key_exists('fields', $field) || empty($field['fields'])) {
+			return;
+		}
+
+		// Handle conditional fields
+		$conditional = $this->handleConditionalField($field);
+
+		// Ensure value is an array
+		$values = is_array($value) ? $value : [];
+		$original = $name;
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'].'::'.$name;
+		}
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+		$hidden = (array_key_exists('mode', $field) && $field['mode'] === 'hidden');
+		if (!$hidden) {
+			?>
+			<fieldset class="field group <?= esc_attr($name) ?>" <?= $conditional ?> data-field="<?=$name?>"<?= $describedBy?>>
+				<legend><?= esc_html($field['label']) ?></legend>
+			<?php
+		}
+		?>
+
+			<div class="group-fields <?=$original?>"<?= ($hidden) ? ' data-field="'.$name.'"' : ''?>"<?= $describedBy ?>>
+		<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+		<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+				<?php
+				foreach ($field['fields'] as $field_name => $config) {
+					// Set the group context for proper field naming
+					$config['group'] = $name;
+
+					// Get the value for this specific field
+					$field_value = $values[$field_name] ?? '';
+
+					// Handle conditional fields within the group
+					if (isset($config['condition'])) {
+						// Convert condition field reference to group context
+						$condition_field = $config['condition']['field'];
+						if (!str_contains($condition_field, '::')) {
+							$config['condition']['field'] = $name . '::' . $condition_field;
+						}
+					}
+
+					$this->render($field_name, $field_value, $config);
+				}
+				?>
+			</div>
+		<?php
+		if (!$hidden) {
+			?>
+			</fieldset>
+			<?php
+		}
+	}
+	protected function renderLocationField(string $name, mixed $value, array $field): void
+	{
+		$googleMaps = JVB()->connect('maps');
+		if (!$googleMaps->isSetUp()) {
+			echo '<div class="notice notice-warning"><p>Google Maps not configured. Please configure in Integrations settings.</p></div>';
+			return;
+		}
+
+		// Extract stored values
+		if (is_string($value)) {
+			$value = maybe_unserialize($value);
+		}
+		$stored_data = is_array($value) ? $value : [];
+
+		$address = $stored_data['address'] ?? '';
+		$lat = $stored_data['lat'] ?? '';
+		$lng = $stored_data['lng'] ?? '';
+
+		// Generate unique field ID
+		$field_id = esc_attr($name);
+		$map_id = $field_id . '_map';
+
+		// Handle grouped fields
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'] . '::' . $name;
+		}
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+
+		// Prepare configuration for JavaScript initialization
+		$js_config = [
+			'fieldId' => $field_id,
+			'initialCoords' => (!empty($lat) && !empty($lng)) ? [
+				'lat' => (float)$lat,
+				'lng' => (float)$lng
+			] : null
+		];
+
+		// IMPORTANT: Properly escape the JSON for use in HTML attribute
+		$json_config = htmlspecialchars(json_encode($js_config), ENT_QUOTES, 'UTF-8');
+		?>
+
+		<div class="field location <?= esc_attr($field_id) ?>"
+			 data-field="<?= esc_attr($field_id) ?>"
+			 data-location-field-init="<?= $json_config ?>"<?=$describedBy?>>
+
+			<?php
+			if (!empty($stored_data['street'])) {
+				echo '<p><b>Current location:</b> '.esc_html($stored_data['street']).'</p>';
+				echo '<p class="hint"><b>Search below to change:</b></p>';
+			}
+			?>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+
+			<div class="location-search-wrapper">
+				<div class="autocomplete-wrapper"></div>
+
+				<!-- Map container -->
+				<div class="location-preview">
+					<div id="<?= esc_attr($map_id); ?>"
+						 class="location-map">
+					</div>
+
+					<?php if (!empty($stored_data)):
+						jvbLocationLinks($stored_data);
+					endif; ?>
+				</div>
+
+				<!-- Hidden inputs for data storage -->
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[address]"
+					   value="<?= esc_attr($address); ?>"
+					   data-location-field="address">
+
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lat]"
+					   value="<?= esc_attr($lat); ?>"
+					   data-location-field="lat">
+
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[lng]"
+					   value="<?= esc_attr($lng); ?>"
+					   data-location-field="lng">
+
+				<?php
+				// Component fields
+				$components = ['street', 'city', 'province', 'postal_code', 'country'];
+				foreach ($components as $component):
+					?>
+					<input type="hidden"
+						   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>[<?= $component; ?>]"
+						   value="<?= esc_attr($stored_data[$component] ?? ''); ?>"
+						   data-location-field="<?= esc_attr($component); ?>">
+				<?php endforeach; ?>
+
+			</div>
+		</div>
+		<?php
+	}
+    //TODO: This is more or less handled by PostSelector/TaxonomySelector, no?
+    private function renderAssociationField(string $name, mixed $value, array $field):void
+    {
+        // Ensure value is an array
+        if (!is_array($value)) {
+            $value = empty($value) ? [] : [$value];
+        }
+
+        // Get field configuration
+        $limit = isset($field['limit']) ? (int)$field['limit'] : 0;
+        $object_types = isset($field['object_types']) ? $field['object_types'] : ['post'];
+        $post_types = isset($field['post_types']) ? $field['post_types'] : ['post'];
+        $taxonomies = isset($field['taxonomies']) ? $field['taxonomies'] : [];
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        // Create unique ID for this field
+        $field_id = 'association-' . esc_attr($name);
+        $conditional = $this->handleConditionalField($field);
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+        ?>
+        <div class="field association <?=$name?>" data-field="<?= esc_attr($name); ?>" <?= $conditional; ?>>
+            <label><?= esc_html($field['label']); ?></label>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+
+            <div class="association-container"<?=$describedBy?>>
+                <div class="association-search">
+                    <input type="text"
+                           id="<?= esc_attr($field_id); ?>-search"
+                           class="association-search-input"
+                           placeholder="Search items...">
+
+                    <div class="association-filter">
+                        <?php if (count($object_types) > 1 || count($post_types) > 1 || count($taxonomies) > 0) : ?>
+                            <select class="association-filter-select">
+                                <?php if (in_array('post', $object_types)) : ?>
+                                    <?php foreach ($post_types as $post_type) : ?>
+                                        <?php
+                                        $post_type_obj = get_post_type_object($post_type);
+                                        $label = $post_type_obj ? $post_type_obj->labels->singular_name : ucfirst($post_type);
+                                        ?>
+                                        <option value="post:<?= esc_attr($post_type); ?>">
+                                            <?= esc_html($label); ?>
+                                        </option>
+                                    <?php endforeach; ?>
+                                <?php endif; ?>
+
+                                <?php if (in_array('term', $object_types)) : ?>
+                                    <?php foreach ($taxonomies as $taxonomy) : ?>
+                                        <?php
+                                        $tax_obj = get_taxonomy($taxonomy);
+                                        $label = $tax_obj ? $tax_obj->labels->singular_name : ucfirst($taxonomy);
+                                        ?>
+                                        <option value="term:<?= esc_attr($taxonomy); ?>">
+                                            <?= esc_html($label); ?>
+                                        </option>
+                                    <?php endforeach; ?>
+                                <?php endif; ?>
+                            </select>
+                        <?php endif; ?>
+
+                        <button type="button" class="search-button">
+                            <?= jvbIcon('search', ['title' => 'Search']); ?>
+                        </button>
+                    </div>
+                </div>
+
+                <div class="association-results">
+                    <div class="association-available">
+                        <h4>Available Items</h4>
+                        <ul class="available-items"></ul>
+                        <div class="association-loading" hidden>
+                            Loading...
+                        </div>
+                        <div class="association-no-results" hidden>
+                            No items found
+                        </div>
+                        <div class="association-pagination">
+                            <button type="button" class="prev-page" disabled>
+                                <?= jvbIcon('arrow-left', ['title' => 'Previous']); ?>
+                            </button>
+                            <span class="page-info">Page <span class="current-page">1</span></span>
+                            <button type="button" class="next-page" disabled>
+                                <?= jvbIcon('arrow-right', ['title' => 'Next']); ?>
+                            </button>
+                        </div>
+                    </div>
+
+                    <div class="association-actions">
+                        <button type="button" class="add-selected" disabled>
+                            <?= jvbIcon('arrow-right', ['title' => 'Add selected']); ?>
+                        </button>
+                        <button type="button" class="remove-selected" disabled>
+                            <?= jvbIcon('arrow-left', ['title' => 'Remove selected']); ?>
+                        </button>
+                    </div>
+
+                    <div class="association-selected">
+                        <h4>Selected Items
+                            <?php if ($limit) : ?>
+                                <span class="limit-info">(<?= esc_html($limit); ?> max)</span>
+                            <?php endif; ?>
+                        </h4>
+                        <ul class="selected-items row">
+                            <?php
+                            // Display currently selected items
+                            foreach ($value as $item_id) {
+                                // Try to determine the type and get details
+                                $item_type = '';
+                                $item_title = '';
+                                $item_object = '';
+
+                                // Check if it's a post
+                                if (in_array('post', $object_types)) {
+                                    $post = get_post($item_id);
+                                    if ($post && in_array($post->post_type, $post_types)) {
+                                        $item_type = 'post';
+                                        $item_title = $post->post_title;
+                                        $item_object = $post->post_type;
+                                    }
+                                }
+
+                                // Check if it's a term
+                                if (empty($item_type) && in_array('term', $object_types)) {
+                                    foreach ($taxonomies as $taxonomy) {
+                                        $term = get_term($item_id, $taxonomy);
+                                        if (!is_wp_error($term) && $term) {
+                                            $item_type = 'term';
+                                            $item_title = $term->name;
+                                            $item_object = $term->taxonomy;
+                                            break;
+                                        }
+                                    }
+                                }
+
+                                // Only output if we found the item
+                                if (!empty($item_type) && !empty($item_title)) {
+                                    ?>
+                                    <li data-id="<?= esc_attr($item_id); ?>"
+                                        data-type="<?= esc_attr($item_type); ?>"
+                                        data-object="<?= esc_attr($item_object); ?>">
+                                        <span class="item-title"><?= esc_html($item_title); ?></span>
+                                        <span class="item-type"><?= esc_html(ucfirst($item_object)); ?></span>
+                                        <button type="button" class="remove-item row">
+                                            <?= jvbIcon('close', ['title' => 'Remove']); ?>
+                                        </button>
+                                    </li>
+                                    <?php
+                                }
+                            }
+                            ?>
+                        </ul>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Hidden input to store selected values -->
+            <input type="hidden" name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>" value="<?= esc_attr(implode(',', $value)); ?>">
+        </div>
+
+        <script>
+            (function() {
+                // Initialize association field
+                const container = document.querySelector('[data-field="<?= esc_attr($name); ?>"]');
+                if (!container) return;
+
+                const searchInput = container.querySelector('.association-search-input');
+                const filterSelect = container.querySelector('.association-filter-select');
+                const searchButton = container.querySelector('.search-button');
+                const availableList = container.querySelector('.available-items');
+                const selectedList = container.querySelector('.selected-items');
+                const hiddenInput = container.querySelector('input[name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"]');
+                const addButton = container.querySelector('.add-selected');
+                const removeButton = container.querySelector('.remove-selected');
+                const loadingIndicator = container.querySelector('.association-loading');
+                const noResultsMessage = container.querySelector('.association-no-results');
+                const prevPageButton = container.querySelector('.prev-page');
+                const nextPageButton = container.querySelector('.next-page');
+                const currentPageSpan = container.querySelector('.current-page');
+
+                // Configuration
+                const config = {
+                    limit: <?= $limit ?: 0; ?>,
+                    objectTypes: <?= json_encode($object_types); ?>,
+                    postTypes: <?= json_encode($post_types); ?>,
+                    taxonomies: <?= json_encode($taxonomies); ?>,
+                    perPage: 10,
+                    currentPage: 1
+                };
+
+                // Current state
+                let currentSearch = '';
+                let currentFilter = filterSelect ? filterSelect.value : (config.objectTypes.includes('post') ? 'post:' + config.postTypes[0] : 'term:' + config.taxonomies[0]);
+                let availableItems = [];
+                let selectedItems = [];
+
+                // Parse initial selected items
+                const initialValue = hiddenInput.value;
+                if (initialValue) {
+                    selectedItems = initialValue.split(',').map(id => parseInt(id, 10));
+                }
+
+                // Event Listeners
+                if (searchButton) {
+                    searchButton.addEventListener('click', performSearch);
+                }
+
+                if (searchInput) {
+                    searchInput.addEventListener('keypress', function(e) {
+                        if (e.key === 'Enter') {
+                            e.preventDefault();
+                            performSearch();
+                        }
+                    });
+                }
+
+                if (filterSelect) {
+                    filterSelect.addEventListener('change', function() {
+                        currentFilter = this.value;
+                        config.currentPage = 1;
+                        performSearch();
+                    });
+                }
+
+                if (prevPageButton) {
+                    prevPageButton.addEventListener('click', function() {
+                        if (config.currentPage > 1) {
+                            config.currentPage--;
+                            performSearch();
+                        }
+                    });
+                }
+
+                if (nextPageButton) {
+                    nextPageButton.addEventListener('click', function() {
+                        config.currentPage++;
+                        performSearch();
+                    });
+                }
+
+                // Add items
+                addButton.addEventListener('click', function() {
+                    const selectedAvailableItems = availableList.querySelectorAll('li.selected');
+                    selectedAvailableItems.forEach(item => {
+                        const id = parseInt(item.dataset.id, 10);
+                        // Check limit
+                        if (config.limit && selectedItems.length >= config.limit) {
+                            return;
+                        }
+
+                        // Skip if already selected
+                        if (selectedItems.includes(id)) {
+                            return;
+                        }
+
+                        // Add to selection
+                        selectedItems.push(id);
+
+                        // Clone and modify for selected list
+                        const clone = item.cloneNode(true);
+                        clone.classList.remove('selected');
+
+                        // Replace checkbox with remove button
+                        const checkbox = clone.querySelector('input[type="checkbox"]');
+                        if (checkbox) {
+                            const removeBtn = document.createElement('button');
+                            removeBtn.type = 'button';
+                            removeBtn.className = 'remove-item';
+                            removeBtn.innerHTML = '<?= jvbIcon('close', ['title' => 'Remove']); ?>';
+                            removeBtn.addEventListener('click', function() {
+                                removeItem(id, clone);
+                            });
+
+                            checkbox.parentNode.replaceChild(removeBtn, checkbox);
+                        }
+
+                        selectedList.appendChild(clone);
+                    });
+
+                    // Update hidden input
+                    updateHiddenInput();
+
+                    // Update UI state
+                    updateButtonStates();
+                });
+
+                // Remove items
+                removeButton.addEventListener('click', function() {
+                    const selectedSelectedItems = selectedList.querySelectorAll('li.selected');
+                    selectedSelectedItems.forEach(item => {
+                        const id = parseInt(item.dataset.id, 10);
+                        removeItem(id, item);
+                    });
+                });
+
+                // Listen for clicks on items in both lists
+                availableList.addEventListener('click', function(e) {
+                    const item = e.target.closest('li');
+                    if (!item) return;
+
+                    // If clicking checkbox, handle separately
+                    if (e.target.type === 'checkbox') {
+                        updateButtonStates();
+                        return;
+                    }
+
+                    // Toggle selection
+                    if (item.classList.contains('selected')) {
+                        item.classList.remove('selected');
+                        item.querySelector('input[type="checkbox"]').checked = false;
+                    } else {
+                        item.classList.add('selected');
+                        item.querySelector('input[type="checkbox"]').checked = true;
+                    }
+
+                    updateButtonStates();
+                });
+
+                selectedList.addEventListener('click', function(e) {
+                    const item = e.target.closest('li');
+                    if (!item) return;
+
+                    // If clicking remove button, handle it
+                    if (e.target.closest('.remove-item')) {
+                        const id = parseInt(item.dataset.id, 10);
+                        removeItem(id, item);
+                        return;
+                    }
+
+                    // Toggle selection
+                    item.classList.toggle('selected');
+                    updateButtonStates();
+                });
+
+                // Helper Functions
+                function performSearch() {
+                    currentSearch = searchInput.value.trim();
+
+                    // Show loading
+                    loadingIndicator.hidden = false;
+                    noResultsMessage.hidden = true;
+                    availableList.innerHTML = '';
+
+                    // Get filter parts
+                    const [type, object] = currentFilter.split(':');
+
+                    // Prepare data for AJAX
+                    const data = {
+                        action: 'jvb_association_search',
+                        nonce: jvbSettings.nonce,
+                        type: type,
+                        object: object,
+                        search: currentSearch,
+                        page: config.currentPage,
+                        per_page: config.perPage,
+                        selected: selectedItems
+                    };
+
+                    // Make AJAX request to WordPress REST API
+                    fetch(jvbSettings.api + 'terms', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json',
+                            'X-WP-Nonce': jvbSettings.nonce
+                        },
+                        body: JSON.stringify(data)
+                    })
+                        .then(response => response.json())
+                        .then(response => {
+                            // Hide loading
+                            loadingIndicator.hidden = true;
+
+                            if (response.success && response.items && response.items.length > 0) {
+                                // Update available items
+                                availableItems = response.items;
+
+                                // Render items
+                                renderAvailableItems();
+
+                                // Update pagination
+                                updatePagination(response.total, response.pages);
+                            } else {
+                                // Show no results
+                                noResultsMessage.hidden = false;
+                                prevPageButton.disabled = true;
+                                nextPageButton.disabled = true;
+                                currentPageSpan.textContent = '1';
+                            }
+                        })
+                        .catch(error => {
+                            console.error('Error searching items:', error);
+                            loadingIndicator.hidden = true;
+                            noResultsMessage.hidden = false;
+                        });
+                }
+
+                function renderAvailableItems() {
+                    availableList.innerHTML = '';
+
+                    availableItems.forEach(item => {
+                        const isSelected = selectedItems.includes(item.id);
+
+                        const li = document.createElement('li');
+                        li.dataset.id = item.id;
+                        li.dataset.type = item.type;
+                        li.dataset.object = item.object;
+
+                        // Create checkbox
+                        const checkbox = document.createElement('input');
+                        checkbox.type = 'checkbox';
+                        checkbox.id = `${name}-item-${item.id}`;
+
+                        // Create label for title
+                        const titleSpan = document.createElement('span');
+                        titleSpan.className = 'item-title';
+                        titleSpan.textContent = item.title;
+
+                        // Create label for type
+                        const typeSpan = document.createElement('span');
+                        typeSpan.className = 'item-type';
+                        typeSpan.textContent = item.object_label;
+
+                        // Append elements
+                        li.appendChild(checkbox);
+                        li.appendChild(titleSpan);
+                        li.appendChild(typeSpan);
+
+                        // Disable if already selected
+                        if (isSelected) {
+                            li.classList.add('disabled');
+                            checkbox.disabled = true;
+
+                            // Add note that item is already selected
+                            const note = document.createElement('span');
+                            note.className = 'item-note';
+                            note.textContent = 'Already selected';
+                            li.appendChild(note);
+                        }
+
+                        availableList.appendChild(li);
+                    });
+                }
+
+                function updatePagination(total, pages) {
+                    // Update current page display
+                    currentPageSpan.textContent = config.currentPage;
+
+                    // Update prev/next buttons
+                    prevPageButton.disabled = config.currentPage <= 1;
+                    nextPageButton.disabled = config.currentPage >= pages;
+                }
+
+                function removeItem(id, element) {
+                    // Remove from array
+                    selectedItems = selectedItems.filter(itemId => itemId !== id);
+
+                    // Remove from DOM
+                    if (element) {
+                        element.remove();
+                    }
+
+                    // Update hidden input
+                    updateHiddenInput();
+
+                    // Update buttons
+                    updateButtonStates();
+                }
+
+                function updateHiddenInput() {
+                    hiddenInput.value = selectedItems.join(',');
+                }
+
+                function updateButtonStates() {
+                    // Add button is enabled if at least one available item is selected
+                    // and we haven't reached the limit
+                    const hasSelectedAvailable = availableList.querySelector('li.selected:not(.disabled)') !== null;
+                    addButton.disabled = !hasSelectedAvailable ||
+                        (config.limit > 0 && selectedItems.length >= config.limit);
+
+                    // Remove button is enabled if at least one selected item is selected
+                    const hasSelectedItems = selectedList.querySelector('li.selected') !== null;
+                    removeButton.disabled = !hasSelectedItems;
+                }
+
+                // Initial search
+                performSearch();
+            })();
+        </script>
+        <?php
+    }
+
+    private function renderTrueFalseField(string $name, mixed $value, array $field):void
+    {
+        $conditional = $this->handleConditionalField($field);
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        ?>
+        <div class="field true-false <?=$name?> row btw" <?=$conditional?> data-field="<?=$name?>">
+            <label class="toggle-switch row"<?=$describedBy?>>
+                <input
+                    type="checkbox"
+                    name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                    value="1"
+                    <?= ($value) ? ' checked':'' ?>
+					<?= !empty($field['required']) ? 'required' : ''; ?>
+                >
+				<div class="slider"></div>
+				<span class="toggle-label"><?= esc_html($field['label']); ?></span>
+            </label>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+        <?php
+    }
+
+	private function renderUploadField(string $name, mixed $value, array $field): void
+	{
+		$defaultConfig = [
+			//File Type
+			'subtype' => 'image', // 'image', 'video', 'document', 'any'
+			'accepted_types' => null, // null = use subtype defaults, or array of specific MIME types
+			//Upload Behaviour
+			'multiple' => false, // Single or multiple uploads
+			'limit' => 0, // Max number of uploads (0 = unlimited)
+			'mode' => 'direct', // 'direct' or 'selection'
+			//Destination
+			'destination' => 'meta', // 'meta', 'post', 'post_group'
+			//Processing Options
+			'max_size' => null, // Override default size limits
+			'convert' => 'webp', // Image conversion format
+			'quality' => 80, // Conversion quality
+			'create_thumbnails' => true,
+		];
+		$config = array_merge($defaultConfig, $field);
+
+		// Validate destination config
+		if (in_array($config['destination'], ['post', 'post_group']) && empty($config['content'])) {
+			error_log("Upload field '{$name}' has destination '{$config['destination']}' but no content defined");
+			return;
+		}
+
+		// Get accepted types
+		$acceptedTypes = $this->getAllowedTypes($config);
+
+		// Build accept attribute for input
+		$acceptExtensions = $this->getMimeExtensions($acceptedTypes);
+		$acceptAttr = implode(',', $acceptExtensions);
+
+		// Determine field attributes
+		$subtype = $config['subtype'] ?? 'image';
+		$multiple = $config['multiple'] ?? false;
+		$limit = $config['limit'] ?? 0;
+		$mode = $config['mode'] ?? 'direct';
+		$destination = $config['destination'];
+
+		// Get existing attachments
+		$attachmentIds = $this->parseAttachmentIds($value);
+
+		// Determine field type for UI
+		$fieldType = $multiple ? 'gallery' : 'single';
+
+		// Build data attributes
+		$dataAttrs = [
+			'data-field' => $name,
+			'data-upload-field' => '',
+			'data-mode' => $mode,
+			'data-type' => $fieldType,
+			'data-subtype' => $subtype,
+			'data-destination' => $destination,
+		];
+		if (!empty($field['content'])) {
+			$dataAttrs['data-content'] = $field['content'];
+		}
+		if ($limit > 0) {
+			$dataAttrs['data-limit'] = $limit;
+		}
+
+		// Build data attributes
+		$conditional = $this->handleConditionalField($field);
+		$describedBy = !empty($field['description']) ? ' aria-describedby="' . esc_attr($name) . '-help"' : '';
+
+		if (!empty($field['group'])) {
+			$name = $field['group'] . '::' . $name;
+		}
+
+		// Convert data attributes to string
+		$dataAttrString = '';
+		foreach ($dataAttrs as $attr => $val) {
+			$dataAttrString .= ' ' . $attr . ($val !== '' ? '="' . esc_attr($val) . '"' : '');
+		}
+		?>
+		<div class="field upload <?= esc_attr($name) ?>"
+			<?= $dataAttrString ?>
+			<?= $conditional ?>>
+
+			<div class="file-upload-container">
+				<div class="file-upload-wrapper">
+					<input type="file"
+						   name="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
+						   id="<?= !empty($field['base']) ? esc_attr($field['base']) : '' ?><?= esc_attr($name) ?>_temp"
+						   accept="<?= esc_attr($acceptAttr) ?>"
+						   data-max-size="<?= esc_attr($this->getMaxFileSize($subtype)) ?>"
+						<?= $multiple ? 'multiple' : '' ?>
+						<?= !empty($field['required']) ? 'required' : '' ?>>
+
+					<h2><?= esc_html($field['label']) ?></h2>
+
+					<?php if (!empty($field['description'])) : ?>
+						<p><?= esc_html($field['description']) ?></p>
+					<?php endif; ?>
+
+					<p class="file-upload-text">
+						<strong>Click to upload</strong> or drag and drop<br>
+						<?= esc_html($this->getAcceptedTypesLabel($subtype, $acceptExtensions)) ?>
+						(max. <?= esc_html($this->formatFileSize($this->getMaxFileSize($subtype))) ?>)
+					</p>
+
+					<?php if ($destination === 'post_group') {
+						$plural = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['plural'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['plural'] : str_replace('_', ' ',$field['content']).'s');
+						$singular = (array_key_exists($field['content'], JVB_CONTENT)) ? JVB_CONTENT[$field['content']]['singular'] : (array_key_exists($field['content'], JVB_TAXONOMY) ? JVB_TAXONOMY[$field['content']]['singular'] : str_replace('_', ' ',$field['content']));
+						?>
+						<p class="hint">You can group images to create separate <?= $plural ?>.</p>
+						<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
+					<?php }
+					if (!empty($field['upload_description'])) : ?>
+						<p><?= esc_html($field['upload_description']); ?></p>
+					<?php endif; ?>
+					<div class="file-error"></div>
+				</div>
+			</div>
+
+
+			<?php if ($destination === 'post_group') : ?>
+			<div class="group-display flex col" hidden>
+				<div class="preview-wrap flex col">
+					<div class="preview-actions">
+						<div class="selection-controls">
+							<div class="selected">
+								<div class="field">
+									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
+									<label for="select-all-uploads">
+										Select All
+									</label>
+								</div>
+								<div class="info" hidden>
+
+								</div>
+							</div>
+
+							<div class="selection-actions row btw" hidden>
+								<button type="button" data-action="add-to-group">
+									<?= jvbIcon('add') ?>
+									Group
+								</button>
+								<button type="button" data-action="delete-upload">
+									<?= jvbIcon('delete') ?>
+									Delete
+								</button>
+							</div>
+						</div>
+
+						<button type="button" data-action="upload" class="submit-uploads">
+							<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
+						</button>
+					</div>
+					<?php endif; ?>
+
+					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
+					<span class="count">0/0</span>'); ?>
+					<div class="item-grid preview">
+						<?php
+						// Render existing attachments
+						foreach ($attachmentIds as $attachmentId) {
+							echo $this->renderExistingAttachment($attachmentId, $subtype);
+						}
+						?>
+					</div>
+
+					<?php if ($destination === 'post_group') : ?>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
+				</div>
+				<div class="sidebar flex col">
+					<div class="header">
+						<h4>New <?= $plural?></h4>
+						<p class="hint">Drag or select multiple images into groups to create separate <?= $plural ?>.</p>
+					</div>
+					<div class="item-grid groups">
+						<div class="empty-group">
+							<p>Drag here to create a new <?= $singular ?>!</p>
+						</div>
+					</div>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
+				</div>
+			</div>
+		<?php endif; ?>
+
+			<?php if ($destination === 'meta') : ?>
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					   value="<?= esc_attr($value); ?>"
+					<?= !empty($field['required']) ? 'required' : ''; ?>>
+			<?php endif; ?>
+		</div>
+		<?php
+	}
+
+
+	protected function getAllowedTypes(array $config):array
+	{
+		$typeMap = [
+			'image' => [
+				'image/jpeg',
+				'image/png',
+				'image/gif',
+				'image/webp'
+			],
+			'video' => [
+				'video/mp4',
+				'video/webm',
+				'video/ogg',
+				'video/ogv',
+				'video/quicktime'
+			],
+			'document' => [
+				'application/pdf',
+				'application/msword',
+				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+				'application/vnd.ms-excel',
+				'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+				'text/plain',
+				'text/csv'
+			],
+			'any' => [] // Will be merged from all types
+		];
+		// If specific types are defined, use those
+		if (!empty($config['accepted_types'])) {
+			return is_array($config['accepted_types'])
+				? $config['accepted_types']
+				: [$config['accepted_types']];
+		}
+
+		// Otherwise use subtype defaults
+		$subtype = $config['subtype'] ?? 'image';
+
+		if ($subtype === 'any') {
+			return array_merge(
+				$typeMap['image'],
+				$typeMap['video'],
+				$typeMap['document']
+			);
+		}
+
+		return $typeMap[$subtype] ?? $typeMap['image'];
+
+	}
+	/**
+	 * Parse attachment IDs from value
+	 */
+	private function parseAttachmentIds(mixed $value): array
+	{
+		if (empty($value)) return [];
+
+		if (is_array($value)) {
+			return array_filter(array_map('absint', $value));
+		}
+
+		return array_filter(array_map('absint', explode(',', $value)));
+	}
+
+	/**
+	 * Get file extensions for MIME types
+	 */
+	private function getMimeExtensions(array $mimeTypes): array
+	{
+		$extensionMap = [
+			'image/jpeg' => ['.jpg', '.jpeg'],
+			'image/png' => ['.png'],
+			'image/gif' => ['.gif'],
+			'image/webp' => ['.webp'],
+			'video/mp4' => ['.mp4'],
+			'video/webm' => ['.webm'],
+			'video/ogg' => ['.ogg'],
+			'video/ogv' => ['.ogv'],
+			'video/quicktime' => ['.mov'],
+			'application/pdf' => ['.pdf'],
+			'application/msword' => ['.doc'],
+			'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['.docx'],
+			'application/vnd.ms-excel' => ['.xls'],
+			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['.xlsx'],
+			'text/plain' => ['.txt'],
+			'text/csv' => ['.csv'],
+		];
+
+		$extensions = [];
+		foreach ($mimeTypes as $mime) {
+			if (isset($extensionMap[$mime])) {
+				$extensions = array_merge($extensions, $extensionMap[$mime]);
+			}
+		}
+
+		return array_unique($extensions);
+	}
+
+	/**
+	 * Get max file size for subtype
+	 */
+	private function getMaxFileSize(string $subtype): int
+	{
+		$sizes = [
+			'image' => 5242880,    // 5MB
+			'video' => 104857600,  // 100MB
+			'document' => 10485760 // 10MB
+		];
+
+		return $sizes[$subtype] ?? $sizes['image'];
+	}
+	/**
+	 * Get human-readable file size label
+	 */
+	private function getMaxFileSizeLabel(string $subtype): string
+	{
+		$bytes = $this->getMaxFileSize($subtype);
+		$mb = round($bytes / 1048576);
+		return "{$mb}MB";
+	}
+	/**
+	 * Format file size for display
+	 */
+	private function formatFileSize(int $bytes): string
+	{
+		if ($bytes >= 1073741824) {
+			return number_format($bytes / 1073741824, 1) . 'GB';
+		}
+		if ($bytes >= 1048576) {
+			return number_format($bytes / 1048576, 1) . 'MB';
+		}
+		if ($bytes >= 1024) {
+			return number_format($bytes / 1024, 1) . 'KB';
+		}
+		return $bytes . 'B';
+	}
+
+	/**
+	 * Get accepted types label
+	 */
+	private function getAcceptedTypesLabel(string $subtype, array $extensions): string
+	{
+		$labels = [
+			'image' => 'JPG, PNG, GIF, or WEBP',
+			'video' => 'MP4, WEBM, or MOV',
+			'document' => 'PDF, DOC, XLS, or TXT',
+			'any' => 'Images, Videos, or Documents'
+		];
+
+		return $labels[$subtype] ?? strtoupper(implode(', ', array_map(function($ext) {
+			return ltrim($ext, '.');
+		}, array_slice($extensions, 0, 3))));
+	}
+
+	/**
+	 * Render existing attachment
+	 */
+	private function renderExistingAttachment(int $attachmentId, string $subtype): string
+	{
+		$attachment = get_post($attachmentId);
+		if (!$attachment) return '';
+
+		$url = wp_get_attachment_url($attachmentId);
+		$thumbUrl = $subtype === 'image'
+			? wp_get_attachment_image_url($attachmentId, 'medium')
+			: $url;
+
+		ob_start();
+		?>
+		<div class="upload-item existing" data-attachment-id="<?= esc_attr($attachmentId) ?>" data-subtype="<?= esc_attr($subtype) ?>">
+			<div class="preview">
+				<?php if ($subtype === 'image') : ?>
+					<img src="<?= esc_url($thumbUrl) ?>" alt="<?= esc_attr(get_post_meta($attachmentId, '_wp_attachment_image_alt', true)) ?>">
+				<?php elseif ($subtype === 'video') : ?>
+					<video src="<?= esc_url($url) ?>" controls></video>
+				<?php else : ?>
+					<div class="document-preview">
+						<?= jvbIcon('document') ?>
+						<span><?= esc_html(basename($url)) ?></span>
+					</div>
+				<?php endif; ?>
+
+				<div class="overlay">
+					<div class="actions">
+						<button type="button" class="remove" title="Remove">
+							<span class="screen-reader-text">Remove <?= esc_attr($subtype) ?></span>
+							×
+						</button>
+					</div>
+				</div>
+			</div>
+
+			<?php if ($subtype === 'image') {
+				echo jvbImageMeta();
+			} ?>
+		</div>
+		<?php
+		return ob_get_clean();
+	}
+
+    private function renderImageField(string $name, mixed $value, array $field):void
+    {
+		$image_url = $title = $alt = $caption = false;
+		if ($value !== 0 || $value !== '') {
+			$image_url = wp_get_attachment_image_url((int)$value, 'medium') ?: false;
+			$caption = wp_get_attachment_caption((int)$value);
+			$alt = get_post_meta((int)$value, '_wp_attachment_image_alt', true);
+			$title = get_the_title((int)$value);
+		}
+
+        $mode = array_key_exists('mode', $field) ? $field['mode'] : 'direct';
+        $multiple = ($mode === 'selection' || isset($field['multiple']));
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		$groupable = (array_key_exists('imageType', $field) && $field['imageType'] === 'groupable');
+		$singular = (array_key_exists('singular', $field)) ? $field['singular'] : 'post';
+		$plural = (array_key_exists('plural', $field)) ? $field['plural'] : 'posts';
+		$dataContent = (array_key_exists('content', $field)) ? ' data-content="'.$field['content'].'"' : '';
+		$dataType = ($groupable) ? 'groupable' : (($multiple) ? 'gallery' : 'single');
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        ?>
+        <div class="field image <?=$name?>"
+             data-field="<?= esc_attr($name); ?>"
+			 data-upload-field
+             data-mode="<?= esc_attr($mode); ?>"
+			 <?=$dataContent?>
+			<?= ' data-type="'.$dataType.'"'?>>
+
+            <div class="file-upload-container">
+                <div class="file-upload-wrapper">
+                    <input type="file"
+                           name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
+                           id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
+                           accept=".jpg,.jpeg,.png,.gif,.webp"
+                           data-max-size="<?= $this->max_file_size; ?>"
+                        <?= $multiple ? 'multiple' : ''; ?>>
+                    <h2><?= esc_html($field['label']); ?></h2>
+                    <?php if (!empty($field['description'])) : ?>
+                        <p><?= esc_html($field['description']); ?></p>
+                    <?php endif; ?>
+                    <p class="file-upload-text">
+                        <strong>Click to upload</strong> or drag and drop<br>
+                        JPG, PNG, GIF, or WEBP (max. 5MB)
+                    </p>
+					<?php if ($groupable) { ?>
+					<p class="hint">You can group images to create separate <?= $plural ?>.</p>
+					<p class="hint">If a <?=$singular?> has multiple images, you can select the <?= jvbIcon('star')?> to set an image as the main one.</p>
+					<?php } ?>
+                    <?php if (!empty($field['upload_description'])) : ?>
+                        <p><?= esc_html($field['upload_description']); ?></p>
+                    <?php endif; ?>
+                </div>
+                <div class="file-error"></div>
+            </div>
+			<?php if ($groupable) : ?>
+			<div class="group-display" hidden>
+				<div class="preview-wrap">
+					<div class="preview-actions">
+						<div class="selection-controls">
+							<div class="selected">
+								<div class="field">
+									<input type="checkbox" id="select-all-uploads" name="select-all-uploads">
+									<label for="select-all-uploads">
+										Select All
+									</label>
+								</div>
+								<div class="info" hidden>
+									With <span class="selection-count">0</span> selected
+								</div>
+							</div>
+
+
+							<!-- Selection actions (hidden by default) -->
+							<div class="selection-actions" hidden>
+								<button type="button" class="create-from-selection">
+									<?= jvbIcon('add') ?>
+									Create New <?= $singular ?>
+								</button>
+								<button type="button" class="remove-selection">
+									<?= jvbIcon('delete') ?>
+									Remove
+								</button>
+							</div>
+						</div>
+
+						<button type="button" class="submit-uploads">
+							<?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
+						</button>
+					</div>
+					<?php endif; ?>
+
+					<?php jvbRenderProgressBar('<span class="text">Processing files...</span>
+					<span class="count">0/0</span>'); ?>
+					<div class="item-grid preview">
+						<?php if ($image_url) {
+							echo jvbRenderImageForm((int)$value);
+						} ?>
+					</div>
+
+					<?php if ($groupable) : ?>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
+				</div>
+				<div class="sidebar">
+					<div class="header">
+						<h4>New <?= $plural?></h4>
+						<p class="hint">Drag images into groups to create separate <?= $plural ?>.</p>
+						<p class="hint">Select multiple images and click "Add to <?= $singular?>" or create new <?= $plural ?>.</p>
+					</div>
+					<button type="button" class="create-group-from-selection">
+						<?= jvbIcon('add') ?>
+						Create New <?= $singular ?>
+					</button>
+					<div class="item-grid groups">
+						<div class="empty-group">
+							<p>Drag here to create a new <?= $singular ?>!</p>
+						</div>
+					</div>
+					<p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
+				</div>
+			</div>
+		<?php endif; ?>
+
+			<?php if ($mode === 'direct') : ?>
+				<input type="hidden"
+					   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					   value="<?= esc_attr($value); ?>"
+					<?= !empty($field['required']) ? 'required' : ''; ?>>
+			<?php endif; ?>
+		</div>
+		<?php
+    }
+
+    protected function renderGalleryField(string $name, string|null|false $value, array $field):void
+    {
+        $ids = ($value === '' || is_null($value) || !$value) ? [] : explode(',',$value);
+
+        if (!empty($ids)) {
+            $ids = array_map('absint', $ids);
+        }
+
+        $conditional = $this->handleConditionalField($field);
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+		//TODO: This can probably just be a wrapper for renderImageField...
+        ?>
+        <div class="field gallery <?=$name?>"
+             data-field="<?= esc_attr($name); ?>"
+            <?= $conditional ?>>
+
+            <label><?= esc_html($field['label']); ?></label>
+
+            <!-- Container for existing images -->
+            <div class="gallery-preview">
+                <?php
+                if (!empty($ids)) {
+                    foreach ($ids as $id) {
+                        $url = wp_get_attachment_image_url($id, 'medium');
+                        if ($url) {
+                            echo '<div class="preview-item" data-id="' . esc_attr($id) . '">';
+                            echo '<img src="' . esc_url($url) . '" alt="">';
+                            echo '<button type="button" class="remove-preview">' . jvbIcon('trash', ['title'=>'Remove']) . '</button>';
+                            echo '</div>';
+                        }
+                    }
+                }
+                ?>
+            </div>
+
+            <!-- Hidden file uploader that will be managed by BatchFileUploader -->
+            <div class="file-upload-container">
+                <div class="file-upload-wrapper">
+                    <input type="file"
+                           name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>_temp"
+                           accept=".jpg,.jpeg,.png,.gif,.webp"
+                           multiple>
+                    <p class="file-upload-text">
+                        <strong>Click to upload</strong> or drag and drop<br>
+                        JPG, PNG, GIF, or WEBP (max. 5MB)
+                    </p>
+                </div>
+                <div class="file-error"></div>
+            </div>
+
+            <!-- Hidden input for storing the IDs -->
+            <input type="hidden"
+                   name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                   value="<?= esc_attr($value); ?>"
+				<?= !empty($field['required']) ? 'required' : ''; ?>>
+
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+        </div>
+
+        <?php
+    }
+    private function renderSelectField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        $conditional = $this->handleConditionalField($field);
+        $default = isset($field['default']) ? $field['default'] : '';
+        $value = !empty($value) ? $value : $default;
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?>" data-field="<?=$name?>" <?=$conditional?>>
+            <label for="<?= esc_attr($name); ?>">
+                <?= esc_html($field['label']); ?>
+            </label>
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+            <select
+                id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+				<?=$describedBy?>
+                <?= !empty($field['required']) ? 'required' : ''; ?>
+            >
+                <?php foreach ($field['options'] as $key => $label) : ?>
+                    <option value="<?= esc_attr($key); ?>"
+                        <?php selected($value, $key); ?>>
+                        <?= esc_html($label); ?>
+                    </option>
+                <?php endforeach; ?>
+            </select>
+        </div>
+        <?php
+    }
+
+    protected function renderHtmlField(string $name, mixed $value, array $field):void
+    {
+        $method_name = $field['content'];
+        $content = '';
+        if (method_exists($this, $method_name)) {
+            $content = $this->$method_name();
+        }
+
+        echo ($content == '') ? '' : sprintf(
+            '<div class="html-field-container" data-field-type="html" data-field="%s">%s</div>',
+            esc_attr($name),
+            $content
+        );
+    }
+
+    private function renderDateField(string $name, mixed $value, array $field):void
+    {
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+        $conditional = $this->handleConditionalField($field);
+        $format = !empty($field['format']) ? $field['format'] : 'Y-m-d';
+
+        // Format the date if we have a value
+        if (!empty($value)) {
+            $date = DateTime::createFromFormat($format, $value);
+            if ($date) {
+                $value = $date->format('Y-m-d'); // HTML date input requires Y-m-d format
+            }
+        }
+        if (array_key_exists('group', $field)) {
+            $name = $field['group'].'::'.$name;
+        }
+        ?>
+         <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+            <label for="<?= esc_attr($name); ?>">
+                <?= esc_html($field['label']); ?>
+            </label>
+            <div class="date-wrapper"<?=$describedBy?>>
+                <input
+                    type="date"
+                    id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                    name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+                    value="<?= esc_attr($value); ?>"
+                    <?= !empty($field['required']) ? 'required' : ''; ?>
+                    data-format="<?= esc_attr($format); ?>"
+                >
+                <?= jvbIcon('event') ?>
+            </div>
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+
+        </div>
+        <?php
+    }
+
+	public function renderTimeField(string $name, mixed $value, array $field):void
+	{
+		$conditional = $this->handleConditionalField($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+		// Convert various time formats to HTML time input format (HH:MM)
+		if (!empty($value)) {
+			// If it's already in HH:MM format, use as-is
+			if (preg_match('/^\d{2}:\d{2}$/', $value)) {
+				// Value is already in correct format
+			} else {
+				// Try to parse and convert
+				$timestamp = strtotime($value);
+				if ($timestamp !== false) {
+					$value = date('H:i', $timestamp);
+				} else {
+					$value = '';
+				}
+			}
+		}
+
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'].'::'.$name;
+		}
+		?>
+		 <div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+			<label for="<?= esc_attr($name); ?>">
+				<?= esc_html($field['label']); ?>
+			</label>
+			<div class="time-wrapper"<?=$describedBy?>>
+				<input
+					type="time"
+					id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					value="<?= esc_attr($value); ?>"
+					<?= !empty($field['required']) ? 'required' : ''; ?>
+					<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
+					<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
+					<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
+				>
+
+			</div>
+			 <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			 <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+		</div>
+		<?php
+	}
+
+	private function renderDatetimeField(string $name, mixed $value, array $field):void
+	{
+		$conditional = $this->handleConditionalField($field);
+		$describedBy = (!empty($field['description'])) ? ' aria-describedby="'.$name.'-help"' : '';
+		// Convert datetime to HTML datetime-local format (YYYY-MM-DDTHH:MM)
+		if (!empty($value)) {
+			$date = DateTime::createFromFormat('Y-m-d H:i:s', $value);
+			if (!$date) {
+				// Try alternative formats
+				$formats = ['Y-m-d\TH:i:s', 'Y-m-d\TH:i', 'Y-m-d H:i'];
+				foreach ($formats as $format) {
+					$date = DateTime::createFromFormat($format, $value);
+					if ($date) break;
+				}
+			}
+
+			if ($date) {
+				$value = $date->format('Y-m-d\TH:i'); // HTML datetime-local format
+			} else {
+				$value = '';
+			}
+		}
+
+		if (array_key_exists('group', $field)) {
+			$name = $field['group'].'::'.$name;
+		}
+		?>
+		<div class="field <?=$field['type']?> <?=$name?>" <?=$conditional?> data-field="<?=$name?>">
+			<label for="<?= esc_attr($name); ?>">
+				<?= esc_html($field['label']); ?>
+			</label>
+			<div class="datetime-wrapper"<?=$describedBy?>>
+				<input
+					type="datetime-local"
+					id="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					name="<?= array_key_exists('base', $field) ? esc_attr($field['base']) : ''?><?= esc_attr($name); ?>"
+					value="<?= esc_attr($value); ?>"
+					<?= !empty($field['required']) ? 'required' : ''; ?>
+					<?= !empty($field['min']) ? 'min="' . esc_attr($field['min']) . '"' : ''; ?>
+					<?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
+					<?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
+				>
+				<?= jvbIcon('event') ?>
+			</div>
+			<?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
+			<?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
+		</div>
+		<?php
+	}
+
+
+    public function outputCharacterCountJS():void
+    {
+        ?>
+        <script>
+            document.querySelectorAll('[maxlength]').forEach(field => {
+                const counter = field.closest('.field')?.querySelector('.char-count .current');
+                if (counter) {
+                    const updateCount = () => counter.textContent = field.value.length;
+                    field.addEventListener('input', updateCount);
+                    updateCount();
+                }
+            });
+        </script>
+        <?php
+    }
+
+
+    //Conditional Fields
+    private function handleConditionalField(array $field):string
+    {
+        if (empty($field['condition'])) {
+            return '';
+        }
+
+        $condition = $field['condition'];
+        return sprintf(
+            'data-depends-on="%s" data-depends-value="%s" data-depends-operator="%s"',
+            esc_attr($field['condition']['field']),
+            esc_attr($field['condition']['value']),
+            esc_attr($field['condition']['operator'] ?? '==')
+        );
+    }
+
+	protected function renderDescription(string $description, string $name):void
+	{
+		$id = $name.'-help';
+		$out = '<div class="has-tooltip">
+		<span class="tt-toggle">'.jvbIcon('help').'</span>
+		<div role="tooltip" id="'.$id.'"><p>'.$description.'</p></div>
+		</div>';
+		echo $out;
+	}
+
+	protected function renderHint(array|string $hint):void
+	{
+		if (is_array($hint)) {
+			$out = '';
+			foreach($hint as $h) {
+				$out .= '<p class="hint">'.$h.'</p>';
+			}
+		} else {
+			$out = '<p class="hint">'.$hint.'</p>';
+		}
+		echo $out;
+	}
+}
diff --git a/inc/meta/MetaManager.php b/inc/meta/MetaManager.php
index 7ed3bc6..e4644f2 100644
--- a/inc/meta/MetaManager.php
+++ b/inc/meta/MetaManager.php
@@ -19,6 +19,7 @@
 	public MetaForm $form;
 	protected int|null $object_id;
 	public object|null $data;
+	protected array $fields =[];
 	protected string $field;
 	protected mixed $value;
 	protected string|null $object_type;
@@ -502,6 +503,9 @@
 
 	protected function getFields(): array
 	{
+		if (!empty($this->fields)) {
+			return $this->fields;
+		}
 		switch ($this->object_type) {
 			case 'post':
 				$type = get_post_type((int)$this->object_id);
@@ -601,6 +605,11 @@
         }
     }
 
+	public function setFieldConfig(array $fields):void
+	{
+		$this->fields = $fields;
+	}
+
     protected function getFieldConfig(string $name):array|false
     {
         $fields = $this->getFields();
diff --git a/inc/registry/PostTypeRegistrar.php b/inc/registry/PostTypeRegistrar.php
index e6f7272..9bd39c6 100644
--- a/inc/registry/PostTypeRegistrar.php
+++ b/inc/registry/PostTypeRegistrar.php
@@ -34,7 +34,7 @@
 		$args = [
 			'labels'              => $this->buildLabels($singular, $plural),
 			'public'              => $this->config['public'] ?? true,
-			'publicly_queryable'  => $this->config['publicly_queryable'] ?? true,
+			'publicly_queryable'  => $this->config['publicly_queryable'] ?? $this->config['public'] ?? true,
 			'show_ui'             => $this->config['show_ui'] ?? true,
 //			'show_in_menu'        => false,
 			'show_in_menu'        => $this->config['show_in_menu'] ?? true,
diff --git a/inc/rest/_setup.php b/inc/rest/_setup.php
index 813542b..a10f740 100644
--- a/inc/rest/_setup.php
+++ b/inc/rest/_setup.php
@@ -54,6 +54,10 @@
 	require(JVB_DIR . '/inc/rest/routes/ApprovalRoutes.php');
 }
 
+if (Features::hasIntegration('square')) {
+	require(JVB_DIR . '/inc/rest/routes/IntegrationsSquareRoutes.php');
+}
+
 require(JVB_DIR .'/inc/rest/routes/OptionsRoutes.php');
 require(JVB_DIR .'/inc/rest/routes/FormRoutes.php');
 require(JVB_DIR .'/inc/rest/routes/IntegrationsRoutes.php');
diff --git a/inc/rest/routes/IntegrationsSquareRoutes.php b/inc/rest/routes/IntegrationsSquareRoutes.php
new file mode 100644
index 0000000..af16f06
--- /dev/null
+++ b/inc/rest/routes/IntegrationsSquareRoutes.php
@@ -0,0 +1,239 @@
+<?php
+namespace JVBase\rest\routes;
+
+
+use JVBase\rest\RestRouteManager;
+use WP_REST_Request;
+use WP_REST_Response;
+use Exception;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+
+class IntegrationsSquareRoutes extends RestRouteManager
+{
+	public function registerRoutes():void
+	{
+		register_rest_route('jvb/v1/square', '/process-payment', [
+			'methods' => 'POST',
+			'callback' => [$this, 'handlePaymentProcessing'],
+			'permission_callback' => '__return_true' // Adjust based on your auth
+		]);
+
+		register_rest_route('jvb/v1/square', '/saved-cards', [
+			'methods' => 'GET',
+			'callback' => [$this, 'getSavedCards'],
+			'permission_callback' => 'is_user_logged_in'
+		]);
+
+
+		register_rest_route('jvb/v1/square', '/order-history', [
+			'methods' => 'GET',
+			'callback' => [$this, 'getOrderHistory'],
+			'permission_callback' => 'is_user_logged_in'
+		]);
+
+
+		register_rest_route('jvb/v1/square', '/order-status/(?P<order_id>[a-zA-Z0-9_-]+)', [
+			'methods' => 'GET',
+			'callback' => [$this, 'getOrderStatus'],
+			'permission_callback' => '__return_true' // Allow guests with order ID
+		]);
+	}
+
+	public function handlePaymentProcessing($request): array
+	{
+		$data = $request->get_json_params();
+
+		// Generate idempotency key from cart_id + timestamp
+		// This ensures retries use SAME key
+		$cart_id = $data['cart_id'] ?? '';
+		if (!$cart_id) {
+			return ['success' => false, 'message' => 'Missing cart ID'];
+		}
+
+		// Check if we already processed this cart
+		$existing_order = get_transient(BASE . 'cart_order_' . $cart_id);
+		if ($existing_order) {
+			// Return cached result - prevents double charge
+			return $existing_order;
+		}
+
+		// Generate idempotency key tied to this specific cart
+		$idempotency_key = 'cart_' . $cart_id . '_' . time();
+
+		// Store key to prevent reprocessing
+		set_transient(BASE . 'cart_idempotency_' . $cart_id, $idempotency_key, HOUR_IN_SECONDS);
+
+		// Validate required fields
+		$required = ['source_id', 'amount', 'items', 'customer'];
+		foreach ($required as $field) {
+			if (empty($data[$field])) {
+				return [
+					'success' => false,
+					'message' => "Missing required field: {$field}"
+				];
+			}
+		}
+
+		try {
+			$square = JVB()->connect('square');
+			// Step 1: Get or create Square customer
+			$customer_id = $square->getOrCreateSquareCustomer($data['customer']);
+
+			// Step 2: Create Order in Square
+			$order_response = $square->createSquareOrder($data['items'], $customer_id, $data);
+
+			if (is_wp_error($order_response)) {
+				throw new Exception($order_response->get_error_message());
+			}
+
+			$order_id = $order_response['order']['id'] ?? null;
+			if (!$order_id) {
+				throw new Exception('Failed to create Square order');
+			}
+
+			// Step 3: Create Payment in Square
+			$payment_response = $square->createSquarePayment(
+				$data['source_id'],
+				$data['idempotency_key'],
+				$data['amount'],
+				$order_id,
+				$customer_id
+			);
+
+			if (is_wp_error($payment_response)) {
+				throw new Exception($payment_response->get_error_message());
+			}
+
+			$payment = $payment_response['payment'] ?? null;
+			if (!$payment) {
+				throw new Exception('Failed to create Square payment');
+			}
+
+			// Step 4: Save order reference in WordPress
+			$wp_order_id = $square->saveOrderToWordPress([
+				'square_order_id' => $order_id,
+				'square_payment_id' => $payment['id'],
+				'customer' => $data['customer'],
+				'items' => $data['items'],
+				'amount' => $data['amount'],
+				'status' => $payment['status']
+			]);
+
+			$result = [
+				'success' => true,
+				'order_id' => $order_id,
+				'payment_id' => $payment['id'],
+				'wp_order_id' => $wp_order_id,
+				'status' => $payment['status'],
+				'receipt_url' => $payment['receipt_url'] ?? null
+			];
+
+			set_transient(BASE . 'cart_order_' . $cart_id, $result, HOUR_IN_SECONDS);
+
+			return $result;
+
+		} catch (Exception $e) {
+			$this->logError('Payment processing failed', [
+				'error' => $e->getMessage(),
+				'idempotency_key' => $data['idempotency_key']
+			]);
+
+			return [
+				'success' => false,
+				'message' => $e->getMessage()
+			];
+		}
+	}
+
+	public function getSavedCards($request): array
+	{
+		$user_id = get_current_user_id();
+		if (!$user_id) {
+			return ['success' => false, 'message' => 'Not logged in'];
+		}
+
+		$square = JVB()->connect('square');
+
+		// Get Square customer ID for this user
+		$square_customer_id = get_user_meta($user_id, BASE . '_square_customer_id', true);
+
+		if (!$square_customer_id) {
+			return ['success' => true, 'cards' => []];
+		}
+
+		// Fetch cards from Square (2025-compliant - separate endpoint)
+		$cards_response = $square->getRequest('cards?customer_id=' . $square_customer_id);
+
+		if (is_wp_error($cards_response)) {
+			return ['success' => false, 'message' => 'Failed to fetch cards'];
+		}
+
+		return [
+			'success' => true,
+			'cards' => $cards_response['cards'] ?? []
+		];
+	}
+
+	public function getOrderHistory($request): array
+	{
+		$user_id = get_current_user_id();
+		if (!$user_id) {
+			return ['success' => false, 'message' => 'Not logged in'];
+		}
+
+		// Get orders from custom post type
+		$orders = get_posts([
+			'post_type' => BASE . '_sq_orders',
+			'author' => $user_id,
+			'posts_per_page' => 50,
+			'orderby' => 'date',
+			'order' => 'DESC'
+		]);
+
+		$order_data = [];
+		foreach ($orders as $order) {
+			$meta = new \JVBase\meta\MetaManager($order->ID, 'post');
+			$order_data[] = [
+				'wp_order_id' => $order->ID,
+				'square_order_id' => $meta->getValue('square_order_id'),
+				'status' => $meta->getValue('status'),
+				'amount' => $meta->getValue('amount'),
+				'items' => $meta->getValue('items'),
+				'created_at' => $meta->getValue('created_at'),
+				'pickup_time' => $meta->getValue('pickup_time')
+			];
+		}
+
+		return [
+			'success' => true,
+			'orders' => $order_data
+		];
+	}
+
+	public function getOrderStatus($request): array
+	{
+		$order_id = $request->get_param('order_id');
+
+		// Find WP post by Square order ID
+		$wp_order_id = get_option(BASE . 'square_order_map_' . $order_id);
+
+		if (!$wp_order_id) {
+			return ['success' => false, 'message' => 'Order not found'];
+		}
+
+		$meta = new \JVBase\meta\MetaManager($wp_order_id, 'post');
+
+		return [
+			'success' => true,
+			'order' => [
+				'status' => $meta->getValue('status'),
+				'fulfillment_status' => $meta->getValue('fulfillment_status'),
+				'pickup_time' => $meta->getValue('pickup_time'),
+				'items' => $meta->getValue('items')
+			]
+		];
+	}
+}
diff --git a/jvb.php b/jvb.php
index edc1b76..fb13350 100644
--- a/jvb.php
+++ b/jvb.php
@@ -593,7 +593,8 @@
             'jvb-tabs',
             'jvb-selector',
             'jvb-uploader',
-			'sortable-js'
+			'sortable-js',
+			'jvb-populate-form'
         ],
         '1.0.0',
         [
@@ -775,8 +776,6 @@
 		JVB_URL.'assets/js/min/navigation.min.js',
 	);
 
-
-
     add_action('wp_head', 'jvbInlineNavStyles');
 
     wp_enqueue_script('jvb-queue');
diff --git a/src/forms/view.js b/src/forms/view.js
index 4275888..d8f6948 100644
--- a/src/forms/view.js
+++ b/src/forms/view.js
@@ -2,80 +2,85 @@
  * Frontend JavaScript for the Form Block
  * Handles form validation and submission
  */
+class FormBlock {
+	constructor() {
+		this.controller = new window.jvbForm();
 
-// Wait for DOM to be ready
+		document.querySelectorAll('.jvb-form-block form').forEach(form => {
+			this.controller.registerForm(form);
+		});
+
+		this.controller.subscribe((event, data) => {
+			if (event === 'form-submit') {
+				this.handleFormSubmission(data);
+			}
+		});
+	}
+
+	async handleFormSubmission(data) {
+		let [
+			formId,
+			formConfig,
+			formData
+		] = [
+			data.formId,
+			data.config,
+			data.data
+		];
+		let form = formConfig.element;
+
+		let headers = {
+			'X-WP-Nonce': jvbSettings.nonce,
+			'Content-Type': 'application/json'
+		};
+
+		data['form_type'] = formId;
+		data['form_id'] = form.id;
+		let block = form.closest('.jvb-form-block');
+		this.controller.showFormStatus(formId, 'uploading');
+		try {
+			const response = await fetch (`${jvbSettings.api}forms`,
+				{
+					method: 'POST',
+					headers,
+					body: JSON.stringify(formData)
+				});
+			if (!response.ok) {
+				this.controller.showFormStatus(formId, 'error');
+				const errorData = await response.json().catch(() => ({}));
+				throw new Error(errorData.message|| `Request failed with status ${response.status}`);
+			}
+			this.controller.showFormStatus(formId, 'submitted');
+
+			this.controller.showSummary(formId, '.jvb-form-block');
+			this.controller.store.delete(formId);
+
+		} catch (error) {
+			throw error;
+		}
+	}
+
+	updateUI(response, block) {
+
+		let summary = window.getTemplate('formSummary');
+		summary.querySelector('h2').textContent = 'Success!';
+		console.log('Form Response: ', response);
+		console.log(summary);
+		for (let [key, value] of Object.entries(response)) {
+			let item = summary.querySelector(`#${key}`);
+			if (item) {
+				let title = item.querySelector('h4');
+				if (title.innerText.includes('%s')) {
+					title.innerHTML = title.replace('%s', '<b>'+value+'</b>');
+				} else {
+					item.querySelector('div').innerHTML = value;
+				}
+			}
+		}
+		block.append(summary);
+	}
+}
+
 document.addEventListener('DOMContentLoaded', function() {
-	let formController = new window.jvbForm();
-
- 	document.querySelectorAll('.jvb-form-block').forEach(block => {
-		const formHandler = formController.registerForm(block.querySelector('form'));
-	});
-	formController.subscribe((event, data) => {
-		if (event === 'form-submit') {
-			handleFormSubmission(null, data.data, data.config.element);
-		}
-	});
+	new FormBlock();
 });
-
-async function handleFormSubmission(event, data, form) {
-	let headers = {
-		'X-WP-Nonce': jvbSettings.nonce,
-		'Content-Type': 'application/json'
-	};
-
-	data['form_type'] = form.dataset.formId;
-	data['form_id'] = form.id;
-	let block = form.closest('.jvb-form-block');
-	let loading = window.getTemplate('spinner');
-	form.hidden = true;
-	block.classList.add('loading');
-	block.prepend(loading);
-	try {
-		const response = await fetch (`${jvbSettings.api}forms`,
-			{
-				method: 'POST',
-				headers,
-				body: JSON.stringify(data)
-			});
-		if (!response.ok) {
-			const errorData = await response.json().catch(() => ({}));
-			throw new Error(errorData.message|| `Request failed with status ${response.status}`);
-		}
-
-		updateUI(await response.json(), block);
-	} catch (error) {
-		throw error;
-	} finally {
-		block.classList.remove('loading');
-	}
-}
-
-function updateUI(response, block) {
-	if (response.success === false) {
-		let form = block.querySelector('form');
-		form.removeAttribute('hidden');
-		if ('required' in response) {
-			response.required.forEach(required => {
-				let item = form.querySelector(`[name=${required}]`);
-				item.classList.add('required', 'error');
-				item.scrollIntoView();
-			});
-		}
-		return;
-	}
-	let summary = window.getTemplate('formSummary');
-	summary.querySelector('h2').textContent = 'Success!';
-	console.log('Form Response: ', response);
-	for (let [key, value] of Object.entries(response)) {
-		let item = summary.querySelector(`#${key}`);
-
-		let title = item.querySelector('h4');
-		if (title.innerText.includes('%s')) {
-			title.innerHTML = title.replace('%s', '<b>'+value+'</b>');
-		} else {
-			item.querySelector('div').innerHTML = value;
-		}
-	}
-	block.append(summary);
-}
-

--
Gitblit v1.10.0