From 25be5747a6e462a3d09fc6607b3639b79e4d9374 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 23 Dec 2025 20:11:26 +0000
Subject: [PATCH] =EmailManager.php refactor, Turnstile properly integrated with form submissions

---
 assets/js/concise/navigation.js          |    4 
 assets/js/min/navigation.min.js          |    2 
 assets/css/dash.min.css                  |    2 
 inc/helpers/ui.php                       |    3 
 inc/managers/OperationQueue.php          |  142 
 assets/js/min/form.min.js                |    2 
 inc/managers/CRUDManager.php             |    7 
 assets/js/min/referral.min.js            |    2 
 assets/js/concise/Integrations.js        |   84 
 assets/js/min/queue.min.js               |    2 
 inc/managers/_setup.php                  |    4 
 assets/js/concise/DataStoreOld.js        | 1099 +++++++++++
 assets/js/concise/Gallery.js             |   44 
 build/gmbreviews/render.php              |   30 
 inc/managers/LoginManager.php            |    6 
 build/gmbreviews/style-index-rtl.css     |    2 
 inc/integrations/Square.php              |   12 
 assets/js/concise/Queue.js               |  128 
 inc/managers/NotificationManager.php     |  134 
 assets/js/min/gallery.min.js             |    2 
 inc/managers/EmailManager.php            |  382 +++
 inc/ui/CRUDSkeleton.php                  |   17 
 inc/integrations/Integrations.php        |   12 
 assets/js/concise/ReferralAdmin.js       |  335 +++
 assets/js/concise/quill.js               |   20 
 inc/integrations/Cloudflare.php          |  108 -
 src/forms/view.js                        |   70 
 assets/js/concise/Referral.js            |  450 ++--
 assets/js/concise/HandleSelection.js     |    7 
 src/gmbreviews/render.php                |   30 
 assets/js/concise/FormController.js      |  219 -
 assets/js/min/handleSelection.min.js     |    2 
 inc/rest/routes/FormRoutes.php           |  462 +++-
 assets/js/concise/UploadManager.js       |  141 
 inc/managers/SEO/BreadcrumbManager.php   |   29 
 src/feed/view.js                         |    9 
 assets/js/concise/PopulateForm.js        |    6 
 assets/css/forms.min.css                 |    2 
 src/gmbreviews/style.scss                |   48 
 inc/rest/routes/ReferralRoutes.php       |   63 
 assets/js/min/integrations.min.js        |    2 
 assets/js/min/quill.min.js               |    2 
 inc/managers/ReferralManager.php         |  223 +-
 assets/js/concise/ErrorHandler.js        |   87 
 build/forms/view.asset.php               |    2 
 inc/managers/MagicLinkManager.php        |   60 
 src/glossary/view.js                     |    1 
 assets/js/min/populate.min.js            |    2 
 inc/managers/SEO/SchemaOutputManager.php |   63 
 assets/js/min/error.min.js               |    2 
 inc/rest/routes/QueueRoutes.php          |    3 
 inc/managers/ErrorHandler.php            |  164 +
 build/gmbreviews/style-index.css         |    2 
 assets/js/min/uploader.min.js            |    2 
 inc/managers/ScriptLoader.php            |    4 
 assets/js/min/referralAdmin.min.js       |    1 
 build/feed/view.asset.php                |    2 
 inc/blocks/VideoCoverBlock.php           |    3 
 assets/js/concise/DataStore.js           |  497 ++--
 webpack.jvb.js                           |    1 
 inc/admin/Integrations.php               |   28 
 /dev/null                                |  479 ----
 assets/js/min/dataStore.min.js           |    2 
 build/feed/view.js                       |    2 
 inc/rest/routes/LoginRoutes.php          |   14 
 build/forms/view.js                      |    2 
 inc/integrations/Helcim.php              |   13 
 inc/blocks/FormBlock.php                 |   28 
 68 files changed, 3,448 insertions(+), 2,366 deletions(-)

diff --git a/SystemReport.php b/SystemReport.php
deleted file mode 100644
index adcc3d4..0000000
--- a/SystemReport.php
+++ /dev/null
@@ -1,2137 +0,0 @@
-<?php
-namespace JVBase;
-
-use wpdb;
-use JVBase\managers\ErrorHandler;
-use JVBase\managers\OperationQueue;
-use JVBase\managers\NotificationManager;
-
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
-}
-/**
- * Manages system-wide reporting for edmonton.ink
- * Generates comprehensive daily reports for admins and collects various metrics
- */
-class SystemReport
-{
-    protected wpdb $wpdb;
-    protected ErrorHandler $error;
-    protected OperationQueue $queue;
-    protected NotificationManager $notifications;
-    protected string $report_interval = 'daily'; // 'daily', 'weekly'
-
-    public function __construct()
-    {
-        global $wpdb;
-        $this->wpdb = $wpdb;
-        $this->error = JVB()->error();
-        $this->queue = JVB()->queue();
-
-        add_action('jvb_generate_daily_report', [$this, 'generateDailyReport']);
-    }
-
-    /**
-     * Generate and send a daily report
-     * @return void
-     */
-    public function generateDailyReport():void
-    {
-        // Collect all metrics
-        $error_stats = $this->gatherErrorStats();
-        $queue_stats = $this->gatherQueueStats();
-        $user_metrics = $this->gatherUserMetrics();
-        $content_metrics = $this->gatherContentMetrics();
-        $system_health = $this->gatherSystemHealthMetrics();
-        $notification_metrics = $this->gatherNotificationMetrics();
-        $user_retention = $this->gatherUserRetentionMetrics();
-        $favourite_metrics = $this->gatherFavouriteMetrics();
-
-        // Format the email content
-        $subject = "[edmonton.ink] Daily System Report - " . date('Y-m-d');
-        $content = $this->formatDailyReport(
-            $error_stats,
-            $queue_stats,
-            $user_metrics,
-            $content_metrics,
-            $system_health,
-            $notification_metrics,
-            $user_retention,
-            $favourite_metrics
-        );
-
-        // Send email
-        $this->sendAdminEmail($subject, $content);
-
-        // Log successful report generation
-        $this->error->log(
-            'system_report',
-            'Daily report generated successfully',
-            [
-                'date' => date('Y-m-d'),
-                'metrics_collected' => [
-                    'error_stats', 'queue_stats', 'user_metrics',
-                    'content_metrics', 'system_health', 'notification_metrics',
-                    'user_retention', 'favourite_metrics'
-                ]
-            ],
-            'info'
-        );
-    }
-
-    /**
-     * Gather error-related statistics for the past 24 hours
-     * @return array
-     */
-    protected function gatherErrorStats():array
-    {
-        $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
-        $stats = [
-            'total_errors' => 0,
-            'by_severity' => [],
-            'by_component' => [],
-            'peak_hours' => [],
-            'frequent' => [],
-            'critical' => []
-        ];
-
-        // Get total count
-        $stats['total_errors'] = $this->wpdb->get_var($this->wpdb->prepare(
-            "SELECT COUNT(*) FROM {$this->wpdb->prefix}jvb_error_log
-             WHERE created_at > %s",
-            $yesterday
-        ));
-
-        // Get counts by severity
-        $severity_counts = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT severity, COUNT(*) as count
-             FROM {$this->wpdb->prefix}jvb_error_log
-             WHERE created_at > %s
-             GROUP BY severity
-             ORDER BY count DESC",
-            $yesterday
-        ));
-
-        foreach ($severity_counts as $row) {
-            $stats['by_severity'][$row->severity] = $row->count;
-        }
-
-        // Get counts by component
-        $component_counts = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT component, COUNT(*) as count
-             FROM {$this->wpdb->prefix}jvb_error_log
-             WHERE created_at > %s
-             GROUP BY component
-             ORDER BY count DESC
-             LIMIT 10",
-            $yesterday
-        ));
-
-        foreach ($component_counts as $row) {
-            $stats['by_component'][$row->component] = $row->count;
-        }
-
-        // Get hourly distribution
-        $hour_counts = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT HOUR(created_at) as hour, COUNT(*) as count
-             FROM {$this->wpdb->prefix}jvb_error_log
-             WHERE created_at > %s
-             GROUP BY HOUR(created_at)
-             ORDER BY count DESC
-             LIMIT 5",
-            $yesterday
-        ));
-
-        foreach ($hour_counts as $row) {
-            $stats['peak_hours'][$row->hour] = $row->count;
-        }
-
-        // Get most frequent errors
-        $stats['frequent'] = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT error_type, component, message, COUNT(*) as count
-             FROM {$this->wpdb->prefix}jvb_error_log
-             WHERE created_at > %s
-             GROUP BY error_type, component, message
-             ORDER BY count DESC
-             LIMIT 10",
-            $yesterday
-        ));
-
-        // Get most recent critical errors
-        $stats['critical'] = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT * FROM {$this->wpdb->prefix}jvb_error_log
-             WHERE severity = 'critical' AND created_at > %s
-             ORDER BY created_at DESC
-             LIMIT 5",
-            $yesterday
-        ));
-
-        return $stats;
-    }
-
-    /**
-     * Gather statistics from the bulk operation queue
-     * @return array
-     */
-    protected function gatherQueueStats():array
-    {
-        $table = $this->wpdb->prefix . BASE . 'bulk_operation';
-        $stats = [];
-
-        // Get counts by status
-        $status_counts = $this->wpdb->get_results("
-            SELECT status, COUNT(*) as count, SUM(count) as count
-            FROM $table
-            GROUP BY status
-        ");
-
-        $stats['by_status'] = [];
-        foreach ($status_counts as $row) {
-            $stats['by_status'][$row->status] = [
-                'count' => $row->count,
-                'operations' => $row->count
-            ];
-        }
-
-        // Get counts by operation type
-        $type_counts = $this->wpdb->get_results("
-            SELECT type, COUNT(*) as count
-            FROM $table
-            GROUP BY type
-            ORDER BY count DESC
-        ");
-
-        $stats['by_type'] = [];
-        foreach ($type_counts as $row) {
-            $stats['by_type'][$row->type] = $row->count;
-        }
-
-        // Get counts by priority
-        $priority_counts = $this->wpdb->get_results("
-            SELECT priority, COUNT(*) as count
-            FROM $table
-            WHERE status = 'pending'
-            GROUP BY priority
-        ");
-
-        $stats['pending_by_priority'] = [];
-        foreach ($priority_counts as $row) {
-            $stats['pending_by_priority'][$row->priority] = $row->count;
-        }
-
-        // Check if queue limit has been reached recently
-        $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
-        $limit_reached = $this->wpdb->get_var($this->wpdb->prepare("
-            SELECT COUNT(*)
-            FROM {$this->wpdb->prefix}jvb_error_log
-            WHERE error_type = 'queue_limit_reached'
-            AND created_at > %s
-        ", $yesterday));
-
-        $stats['limit_reached_count'] = $limit_reached;
-
-        // Get oldest pending operation
-        $oldest_pending = $this->wpdb->get_row("
-            SELECT id, type, created_at, TIMESTAMPDIFF(HOUR, created_at, NOW()) as age_hours
-            FROM $table
-            WHERE status = 'pending'
-            ORDER BY created_at ASC
-            LIMIT 1
-        ");
-
-        if ($oldest_pending) {
-            $stats['oldest_pending'] = [
-                'id' => $oldest_pending->id,
-                'type' => $oldest_pending->type,
-                'created_at' => $oldest_pending->created_at,
-                'age_hours' => $oldest_pending->age_hours
-            ];
-        }
-
-        // Get failed operations in the last 24 hours
-        $recent_failures = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT id, type, error_message
-            FROM $table
-            WHERE status = 'failed'
-            AND completed_at > %s
-            ORDER BY completed_at DESC
-            LIMIT 5
-        ", $yesterday));
-
-        $stats['recent_failures'] = $recent_failures;
-
-        // Get average processing time by operation type
-        $avg_processing_times = $this->wpdb->get_results("
-            SELECT
-                type,
-                AVG(TIMESTAMPDIFF(SECOND, started_at, completed_at)) as avg_seconds,
-                COUNT(*) as sample_size
-            FROM $table
-            WHERE
-                status = 'completed'
-                AND started_at IS NOT NULL
-                AND completed_at IS NOT NULL
-                AND completed_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
-            GROUP BY type
-            HAVING sample_size > 5
-        ");
-
-        $stats['avg_processing_times'] = [];
-        foreach ($avg_processing_times as $row) {
-            $stats['avg_processing_times'][$row->type] = [
-                'seconds' => round($row->avg_seconds, 2),
-                'samples' => $row->sample_size
-            ];
-        }
-
-        return $stats;
-    }
-
-    /**
-     * Gather user-related metrics for the past 24 hours
-     * @return array
-     */
-    protected function gatherUserMetrics():array
-    {
-        $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
-
-        $metrics = [
-            'logins' => [
-                'total' => 0,
-                'by_role' => []
-            ],
-            'registrations' => [
-                'total' => 0,
-                'by_role' => []
-            ],
-            'approvals' => [
-                'total' => 0,
-                'by_role' => []
-            ],
-            'new_shops' => []
-        ];
-
-        // Get login counts from activity log
-        $login_counts = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT
-                COUNT(*) as count,
-                MAX(CASE WHEN um.meta_value LIKE '%administrator%' THEN 1 ELSE 0 END) as admin,
-                MAX(CASE WHEN um.meta_value LIKE '%jvb_enthusiast%' THEN 1 ELSE 0 END) as enthusiast,
-                MAX(CASE WHEN um.meta_value LIKE '%jvb_artist%' THEN 1 ELSE 0 END) as artist,
-                MAX(CASE WHEN um.meta_value LIKE '%jvb_partner%' THEN 1 ELSE 0 END) as partner
-            FROM
-                {$this->wpdb->prefix}jvb_activity_log al
-            JOIN
-                {$this->wpdb->usermeta} um ON al.user_id = um.user_id AND um.meta_key = '{$this->wpdb->prefix}capabilities'
-            WHERE
-                al.type = 'login'
-                AND al.created_at > %s
-            GROUP BY
-                al.user_id
-        ", $yesterday));
-
-        if (!empty($login_counts)) {
-            foreach ($login_counts as $count) {
-                $metrics['logins']['total'] += $count->count;
-                if ($count->enthusiast) $metrics['logins']['by_role']['enthusiast'] = ($metrics['logins']['by_role']['enthusiast'] ?? 0) + $count->count;
-                if ($count->artist) $metrics['logins']['by_role']['artist'] = ($metrics['logins']['by_role']['artist'] ?? 0) + $count->count;
-                if ($count->partner) $metrics['logins']['by_role']['partner'] = ($metrics['logins']['by_role']['partner'] ?? 0) + $count->count;
-                if ($count->admin) $metrics['logins']['by_role']['admin'] = ($metrics['logins']['by_role']['admin'] ?? 0) + $count->count;
-            }
-        }
-
-        // Get new user registrations
-        $new_users = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT
-                u.ID,
-                u.user_registered,
-                um.meta_value as capabilities
-            FROM
-                {$this->wpdb->users} u
-            JOIN
-                {$this->wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = '{$this->wpdb->prefix}capabilities'
-            WHERE
-                u.user_registered > %s
-        ", $yesterday));
-
-        $metrics['registrations']['total'] = count($new_users);
-
-        foreach ($new_users as $user) {
-            $capabilities = maybe_unserialize($user->capabilities);
-
-            if (isset($capabilities['jvb_enthusiast'])) {
-                $metrics['registrations']['by_role']['enthusiast'] = ($metrics['registrations']['by_role']['enthusiast'] ?? 0) + 1;
-            }
-            if (isset($capabilities['jvb_artist'])) {
-                $metrics['registrations']['by_role']['artist'] = ($metrics['registrations']['by_role']['artist'] ?? 0) + 1;
-            }
-            if (isset($capabilities['jvb_partner'])) {
-                $metrics['registrations']['by_role']['partner'] = ($metrics['registrations']['by_role']['partner'] ?? 0) + 1;
-            }
-        }
-
-        // Get user approvals (role changes)
-        $approvals = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT
-                al.user_id,
-                al.data,
-                u.user_nicename
-            FROM
-                {$this->wpdb->prefix}jvb_activity_log al
-            JOIN
-                {$this->wpdb->users} u ON al.user_id = u.ID
-            WHERE
-                al.type = 'role_change'
-                AND al.created_at > %s
-        ", $yesterday));
-
-        $metrics['approvals']['total'] = count($approvals);
-
-        foreach ($approvals as $approval) {
-            $data = json_decode($approval->data, true);
-            if (isset($data['new_role'])) {
-                if ($data['new_role'] === 'jvb_artist') {
-                    $metrics['approvals']['by_role']['artist'] = ($metrics['approvals']['by_role']['artist'] ?? 0) + 1;
-                    $metrics['approvals']['artists'][] = $approval->user_nicename;
-                } elseif ($data['new_role'] === 'jvb_partner') {
-                    $metrics['approvals']['by_role']['partner'] = ($metrics['approvals']['by_role']['partner'] ?? 0) + 1;
-                    $metrics['approvals']['partners'][] = $approval->user_nicename;
-                }
-            }
-        }
-
-        // Get new shops
-        $new_shops = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT
-                t.term_id,
-                t.name
-            FROM
-                {$this->wpdb->terms} t
-            JOIN
-                {$this->wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
-            WHERE
-                tt.taxonomy = 'jvb_shop'
-                AND tt.description LIKE %s
-        ", '%' . $this->wpdb->esc_like('"created_at":"' . date('Y-m-d', strtotime('-1 day'))) . '%'));
-
-        foreach ($new_shops as $shop) {
-            $metrics['new_shops'][] = [
-                'id' => $shop->term_id,
-                'name' => $shop->name
-            ];
-        }
-
-        $metrics['new_shops_count'] = count($metrics['new_shops']);
-
-        return $metrics;
-    }
-
-    /**
-     * Gather content-related metrics for the past 24 hours
-     * @return array
-     */
-    protected function gatherContentMetrics():array
-    {
-        $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
-
-        $metrics = [
-            'uploads' => [],
-            'total_uploads' => 0,
-            'by_user' => [],
-            'taxonomy_growth' => []
-        ];
-
-        // Define content types to track
-        $content_types = [
-            'jvb_tattoo',
-            'jvb_piercing',
-            'jvb_artwork',
-            'jvb_event',
-            'jvb_offer'
-        ];
-
-        // Get counts for each content type
-        foreach ($content_types as $type) {
-            $count = $this->wpdb->get_var($this->wpdb->prepare("
-                SELECT COUNT(*)
-                FROM {$this->wpdb->posts}
-                WHERE post_type = %s
-                AND post_date > %s
-            ", $type, $yesterday));
-
-            $metrics['uploads'][str_replace(BASE, '', $type)] = (int)$count;
-            $metrics['total_uploads'] += (int)$count;
-        }
-
-        // Get top uploaders
-        $top_uploaders = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT
-                post_author,
-                COUNT(*) as count,
-                (SELECT user_nicename FROM {$this->wpdb->users} WHERE ID = post_author) as username,
-                post_type
-            FROM
-                {$this->wpdb->posts}
-            WHERE
-                post_type IN ('" . implode("','", $content_types) . "')
-                AND post_date > %s
-            GROUP BY
-                post_author, post_type
-            ORDER BY
-                count DESC
-            LIMIT 10
-        ", $yesterday));
-
-        foreach ($top_uploaders as $uploader) {
-            if (!isset($metrics['by_user'][$uploader->post_author])) {
-                $metrics['by_user'][$uploader->post_author] = [
-                    'username' => $uploader->username,
-                    'total' => 0,
-                    'types' => []
-                ];
-            }
-
-            $metrics['by_user'][$uploader->post_author]['types'][str_replace(BASE, '', $uploader->post_type)] = $uploader->count;
-            $metrics['by_user'][$uploader->post_author]['total'] += $uploader->count;
-        }
-
-        // Sort by total uploads
-        usort($metrics['by_user'], function ($a, $b) {
-            return $b['total'] - $a['total'];
-        });
-
-        // Track taxonomy growth
-        $taxonomies = [
-            'jvb_style' => 'Tattoo Styles',
-            'jvb_theme' => 'Tattoo Themes',
-            'jvb_shop' => 'Shops',
-            'jvb_city' => 'Cities',
-            'jvb_artstyle' => 'Art Styles',
-            'jvb_arttheme' => 'Art Themes',
-            'jvb_pstyle' => 'Piercing Styles',
-            'jvb_placement' => 'Placements',
-        ];
-
-        foreach ($taxonomies as $taxonomy => $label) {
-            // Get new terms added in the last day
-            $new_terms = $this->wpdb->get_results($this->wpdb->prepare("
-                SELECT t.term_id, t.name
-                FROM {$this->wpdb->terms} t
-                JOIN {$this->wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
-                WHERE tt.taxonomy = %s
-                AND tt.description LIKE %s
-            ", $taxonomy, '%' . $this->wpdb->esc_like('"created_at":"' . date('Y-m-d', strtotime('-1 day'))) . '%'));
-
-            if (!empty($new_terms)) {
-                $metrics['taxonomy_growth'][$taxonomy] = [
-                    'label' => $label,
-                    'count' => count($new_terms),
-                    'terms' => array_map(function ($term) {
-                        return [
-                            'id' => $term->term_id,
-                            'name' => $term->name
-                        ];
-                    }, $new_terms)
-                ];
-            } else {
-                $metrics['taxonomy_growth'][$taxonomy] = [
-                    'label' => $label,
-                    'count' => 0,
-                    'terms' => []
-                ];
-            }
-        }
-
-        return $metrics;
-    }
-
-    /**
-     * Gather user retention metrics
-     * @return array
-     */
-    protected function gatherUserRetentionMetrics():array
-    {
-        $metrics = [
-            'active_periods' => [
-                'daily' => 0,
-                'weekly' => 0,
-                'monthly' => 0
-            ],
-            'retention_by_role' => [],
-            'inactive_counts' => [],
-            'reactivated_users' => []
-        ];
-
-        // Get daily active users (logged in today)
-        $daily_active = $this->wpdb->get_var("
-            SELECT COUNT(DISTINCT user_id)
-            FROM {$this->wpdb->prefix}jvb_activity_log
-            WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)
-        ");
-        $metrics['active_periods']['daily'] = (int)$daily_active;
-
-        // Get weekly active users
-        $weekly_active = $this->wpdb->get_var("
-            SELECT COUNT(DISTINCT user_id)
-            FROM {$this->wpdb->prefix}jvb_activity_log
-            WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
-        ");
-        $metrics['active_periods']['weekly'] = (int)$weekly_active;
-
-        // Get monthly active users
-        $monthly_active = $this->wpdb->get_var("
-            SELECT COUNT(DISTINCT user_id)
-            FROM {$this->wpdb->prefix}jvb_activity_log
-            WHERE created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
-        ");
-        $metrics['active_periods']['monthly'] = (int)$monthly_active;
-
-        // Get active users by role type in the last week
-        $active_by_role = $this->wpdb->get_results("
-            SELECT
-                MAX(CASE WHEN um.meta_value LIKE '%administrator%' THEN 1 ELSE 0 END) as admin,
-                MAX(CASE WHEN um.meta_value LIKE '%jvb_enthusiast%' THEN 1 ELSE 0 END) as enthusiast,
-                MAX(CASE WHEN um.meta_value LIKE '%jvb_artist%' THEN 1 ELSE 0 END) as artist,
-                MAX(CASE WHEN um.meta_value LIKE '%jvb_partner%' THEN 1 ELSE 0 END) as partner,
-                COUNT(DISTINCT al.user_id) as count
-            FROM
-                {$this->wpdb->prefix}jvb_activity_log al
-            JOIN
-                {$this->wpdb->usermeta} um ON al.user_id = um.user_id AND um.meta_key = '{$this->wpdb->prefix}capabilities'
-            WHERE
-                al.created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
-            GROUP BY
-                CASE
-                    WHEN um.meta_value LIKE '%administrator%' THEN 'admin'
-                    WHEN um.meta_value LIKE '%jvb_artist%' THEN 'artist'
-                    WHEN um.meta_value LIKE '%jvb_partner%' THEN 'partner'
-                    WHEN um.meta_value LIKE '%jvb_enthusiast%' THEN 'enthusiast'
-                    ELSE 'other'
-                END
-        ");
-
-        foreach ($active_by_role as $role_data) {
-            if ($role_data->admin) $metrics['retention_by_role']['admin'] = $role_data->count;
-            if ($role_data->enthusiast) $metrics['retention_by_role']['enthusiast'] = $role_data->count;
-            if ($role_data->artist) $metrics['retention_by_role']['artist'] = $role_data->count;
-            if ($role_data->partner) $metrics['retention_by_role']['partner'] = $role_data->count;
-        }
-
-        // Count inactive users by duration
-        $inactive_periods = [
-            ['30 days', '3 months', 'inactive_1mo_3mo'],
-            ['3 months', '6 months', 'inactive_3mo_6mo'],
-            ['6 months', '1 year', 'inactive_6mo_1yr'],
-            ['1 year', null, 'inactive_1yr_plus']
-        ];
-
-        foreach ($inactive_periods as $period) {
-            $start_period = $period[0];
-            $end_period = $period[1];
-            $key = $period[2];
-
-            $query = "
-                SELECT COUNT(*)
-                FROM {$this->wpdb->users} u
-                WHERE NOT EXISTS (
-                    SELECT 1
-                    FROM {$this->wpdb->prefix}jvb_activity_log al
-                    WHERE al.user_id = u.ID
-                    AND al.created_at > DATE_SUB(NOW(), INTERVAL {$start_period})
-                )
-            ";
-
-            if ($end_period) {
-                $query .= " AND EXISTS (
-                    SELECT 1
-                    FROM {$this->wpdb->prefix}jvb_activity_log al
-                    WHERE al.user_id = u.ID
-                    AND al.created_at > DATE_SUB(NOW(), INTERVAL {$end_period})
-                )";
-            }
-
-            $count = $this->wpdb->get_var($query);
-            $metrics['inactive_counts'][$key] = (int)$count;
-        }
-
-        // Find reactivated users (inactive for 30+ days who logged in recently)
-        $reactivated = $this->wpdb->get_results("
-            SELECT
-                al1.user_id,
-                u.user_nicename,
-                MAX(al1.created_at) as recent_login,
-                DATEDIFF(
-                    MAX(al1.created_at),
-                    (
-                        SELECT MAX(al2.created_at)
-                        FROM {$this->wpdb->prefix}jvb_activity_log al2
-                        WHERE al2.user_id = al1.user_id
-                        AND al2.created_at < DATE_SUB(MAX(al1.created_at), INTERVAL 30 DAY)
-                    )
-                ) as days_inactive
-            FROM
-                {$this->wpdb->prefix}jvb_activity_log al1
-            JOIN
-                {$this->wpdb->users} u ON al1.user_id = u.ID
-            WHERE
-                al1.created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)
-            GROUP BY
-                al1.user_id
-            HAVING
-                days_inactive > 30
-            ORDER BY
-                days_inactive DESC
-            LIMIT 10
-        ");
-
-        foreach ($reactivated as $user) {
-            $metrics['reactivated_users'][] = [
-                'user_id' => $user->user_id,
-                'username' => $user->user_nicename,
-                'days_inactive' => $user->days_inactive,
-                'recent_login' => $user->recent_login
-            ];
-        }
-
-        return $metrics;
-    }
-
-    /**
-     * Gather favourite patterns metrics
-     * @return array
-     */
-    protected function gatherFavouriteMetrics():array
-    {
-        $metrics = [
-            'total_favourites' => 0,
-            'favourites_last_24h' => 0,
-            'by_content_type' => [],
-            'top_favourited' => [],
-            'top_artists' => []
-        ];
-
-        $table = $this->wpdb->prefix . BASE . 'favourites';
-
-        // Get total favourites
-        $metrics['total_favourites'] = $this->wpdb->get_var("
-            SELECT COUNT(*) FROM {$table}
-        ");
-
-        // Get favourites added in the last 24 hours
-        $metrics['favourites_last_24h'] = $this->wpdb->get_var($this->wpdb->prepare("
-            SELECT COUNT(*)
-            FROM {$table}
-            WHERE date_added > %s
-        ", date('Y-m-d H:i:s', strtotime('-24 hours'))));
-
-        // Get favourites by content type
-        $by_type = $this->wpdb->get_results("
-            SELECT type, COUNT(*) as count
-            FROM {$table}
-            GROUP BY type
-            ORDER BY count DESC
-        ");
-
-        foreach ($by_type as $type) {
-            $metrics['by_content_type'][$type->type] = $type->count;
-        }
-
-        // Get top favourited content by type
-        $content_types = ['tattoo', 'piercing', 'artwork', 'artist', 'shop'];
-
-        foreach ($content_types as $type) {
-            $top_items = $this->wpdb->get_results($this->wpdb->prepare("
-                SELECT
-                    f.target_id,
-                    COUNT(*) as favourite_count,
-                    CASE
-                        WHEN %s = 'artist' THEN (
-                            SELECT user_nicename FROM {$this->wpdb->users}
-                            WHERE ID = (SELECT post_author FROM {$this->wpdb->posts} WHERE ID = f.target_id)
-                        )
-                        WHEN %s = 'shop' THEN (
-                            SELECT name FROM {$this->wpdb->terms} WHERE term_id = f.target_id
-                        )
-                        ELSE (
-                            SELECT post_title FROM {$this->wpdb->posts} WHERE ID = f.target_id
-                        )
-                    END as name
-                FROM
-                    {$table} f
-                WHERE
-                    f.type = %s
-                GROUP BY
-                    f.target_id
-                ORDER BY
-                    favourite_count DESC
-                LIMIT 5
-            ", $type, $type, $type));
-
-            if (!empty($top_items)) {
-                $metrics['top_favourited'][$type] = array_map(function ($item) {
-                    return [
-                        'id' => $item->target_id,
-                        'name' => $item->name,
-                        'count' => $item->favourite_count
-                    ];
-                }, $top_items);
-            }
-        }
-
-        // Get top artists by favourites received
-        $top_artists = $this->wpdb->get_results("
-            SELECT
-                p.post_author as user_id,
-                u.user_nicename as artist_name,
-                COUNT(f.id) as favourite_count
-            FROM
-                {$table} f
-            JOIN
-                {$this->wpdb->posts} p ON f.target_id = p.ID
-            JOIN
-                {$this->wpdb->users} u ON p.post_author = u.ID
-            WHERE
-                f.type IN ('tattoo', 'piercing', 'artwork')
-            GROUP BY
-                p.post_author
-            ORDER BY
-                favourite_count DESC
-            LIMIT 10
-        ");
-
-        foreach ($top_artists as $artist) {
-            $metrics['top_artists'][] = [
-                'user_id' => $artist->user_id,
-                'name' => $artist->artist_name,
-                'favourite_count' => $artist->favourite_count
-            ];
-        }
-
-        return $metrics;
-    }
-
-    /**
-     * Gather system health metrics
-     * @return array
-     */
-    protected function gatherSystemHealthMetrics():array
-    {
-        $metrics = [
-            'database' => [
-                'size' => 0,
-                'tables' => [],
-                'growth' => []
-            ],
-            'media' => [
-                'total_size' => 0,
-                'file_count' => 0,
-                'by_type' => []
-            ],
-            'performance' => [
-                'average_response_time' => 0,
-                'slow_queries' => []
-            ],
-            'cache' => [
-                'hit_rate' => 0,
-                'size' => 0
-            ]
-        ];
-
-        // Get database size
-        $db_size = $this->wpdb->get_results("
-            SELECT
-                table_schema as 'database',
-                SUM(data_length + index_length) / 1024 / 1024 as size_mb
-            FROM
-                information_schema.TABLES
-            WHERE
-                table_schema = DATABASE()
-            GROUP BY
-                table_schema
-        ");
-
-        if (!empty($db_size)) {
-            $metrics['database']['size'] = round($db_size[0]->size_mb, 2);
-        }
-
-        // Get largest tables
-        $tables = $this->wpdb->get_results("
-            SELECT
-                table_name,
-                ROUND((data_length + index_length) / 1024 / 1024, 2) as size_mb,
-                table_rows
-            FROM
-                information_schema.TABLES
-            WHERE
-                table_schema = DATABASE()
-            ORDER BY
-                (data_length + index_length) DESC
-            LIMIT 10
-        ");
-
-        foreach ($tables as $table) {
-            $metrics['database']['tables'][] = [
-                'name' => $table->table_name,
-                'size_mb' => $table->size_mb,
-                'rows' => $table->table_rows
-            ];
-        }
-
-        // Get database growth (using estimations from previous reports or calculating based on wp_options)
-        $db_size_history = get_option('jvb_db_size_history', []);
-        $current_size = $metrics['database']['size'];
-
-        // Store current size to history
-        $db_size_history[date('Y-m-d')] = $current_size;
-        if (count($db_size_history) > 30) {
-            // Keep only last 30 days
-            $db_size_history = array_slice($db_size_history, -30, 30, true);
-        }
-        update_option('jvb_db_size_history', $db_size_history);
-
-        // Calculate growth rates
-        if (count($db_size_history) > 1) {
-            $dates = array_keys($db_size_history);
-            $sizes = array_values($db_size_history);
-
-            // Daily growth (if we have yesterday's data)
-            $yesterday_idx = array_search(date('Y-m-d', strtotime('-1 day')), $dates);
-            if ($yesterday_idx !== false) {
-                $yesterday_size = $sizes[$yesterday_idx];
-                $daily_growth = $current_size - $yesterday_size;
-                $metrics['database']['growth']['daily'] = round($daily_growth, 2);
-                $metrics['database']['growth']['daily_percent'] = round(($daily_growth / $yesterday_size) * 100, 2);
-            }
-
-            // Weekly growth
-            $week_ago_idx = array_search(date('Y-m-d', strtotime('-7 days')), $dates);
-            if ($week_ago_idx !== false) {
-                $week_ago_size = $sizes[$week_ago_idx];
-                $weekly_growth = $current_size - $week_ago_size;
-                $metrics['database']['growth']['weekly'] = round($weekly_growth, 2);
-                $metrics['database']['growth']['weekly_percent'] = round(($weekly_growth / $week_ago_size) * 100, 2);
-            }
-
-            // Monthly growth (estimate based on available data)
-            if (count($db_size_history) >= 7) {
-                $oldest_date = min(array_keys($db_size_history));
-                $oldest_size = $db_size_history[$oldest_date];
-                $days_diff = (strtotime('now') - strtotime($oldest_date)) / 86400;
-
-                if ($days_diff > 0) {
-                    $growth_per_day = ($current_size - $oldest_size) / $days_diff;
-                    $projected_monthly = $growth_per_day * 30;
-                    $metrics['database']['growth']['monthly_projected'] = round($projected_monthly, 2);
-                    $metrics['database']['growth']['monthly_percent_projected'] = round(($projected_monthly / $current_size) * 100, 2);
-                }
-            }
-        }
-
-        // Get media storage stats
-        $upload_dir = wp_upload_dir();
-        $upload_base = $upload_dir['basedir'];
-
-        if (function_exists('exec') && !in_array('exec', explode(',', ini_get('disable_functions')))) {
-            // Use system du command if available for more accurate results
-            exec("du -sk {$upload_base}", $output);
-            if (!empty($output)) {
-                // Extract size in KB and convert to MB
-                $size_parts = explode("\t", $output[0]);
-                $media_size_kb = (int)$size_parts[0];
-                $metrics['media']['total_size'] = round($media_size_kb / 1024, 2);
-            }
-
-            // Count files by type
-            exec("find {$upload_base} -type f | grep -c '\\.jpg", $jpg_count);
-            exec("find {$upload_base} -type f | grep -c '\\.png", $png_count);
-            exec("find {$upload_base} -type f | grep -c '\\.webp", $webp_count);
-            exec("find {$upload_base} -type f | grep -c '\\.gif", $gif_count);
-
-            $metrics['media']['by_type']['jpg'] = !empty($jpg_count) ? (int)$jpg_count[0] : 0;
-            $metrics['media']['by_type']['png'] = !empty($png_count) ? (int)$png_count[0] : 0;
-            $metrics['media']['by_type']['webp'] = !empty($webp_count) ? (int)$webp_count[0] : 0;
-            $metrics['media']['by_type']['gif'] = !empty($gif_count) ? (int)$gif_count[0] : 0;
-
-            $metrics['media']['file_count'] = array_sum($metrics['media']['by_type']);
-        } else {
-            // Fallback to database count if exec is not available
-            $attachment_count = $this->wpdb->get_var("
-                SELECT COUNT(*)
-                FROM {$this->wpdb->posts}
-                WHERE post_type = 'attachment'
-            ");
-
-            $metrics['media']['file_count'] = (int)$attachment_count;
-
-            // Estimate size based on average attachment size and count
-            // This is just a rough estimate
-            $metrics['media']['total_size'] = round($attachment_count * 0.5, 2); // Assume 0.5MB average per file
-        }
-
-        // Get cache statistics
-        // This is specific to your caching solution - this is a simple example for WP Object Cache
-        if (wp_using_ext_object_cache()) {
-            global $wp_object_cache;
-
-            if (is_object($wp_object_cache) && method_exists($wp_object_cache, 'getStats')) {
-                $cache_stats = $wp_object_cache->getStats();
-                if (!empty($cache_stats)) {
-                    $hits = $cache_stats['get'] ?? 0;
-                    $misses = $cache_stats['misses'] ?? 0;
-                    $total = $hits + $misses;
-
-                    if ($total > 0) {
-                        $metrics['cache']['hit_rate'] = round(($hits / $total) * 100, 2);
-                    }
-
-                    $metrics['cache']['size'] = round(($cache_stats['bytes'] ?? 0) / 1024 / 1024, 2);
-                }
-            }
-        }
-
-        return $metrics;
-    }
-
-    /**
-     * Gather notification metrics
-     * @return array
-     */
-    protected function gatherNotificationMetrics():array
-    {
-        $metrics = [
-            'total_sent' => 0,
-            'by_type' => [],
-            'read_rate' => 0,
-            'action_rate' => 0,
-            'most_active_users' => []
-        ];
-
-        $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
-        $notification_table = $this->wpdb->prefix . BASE . 'notifications_archive';
-        $notification_meta = $this->wpdb->prefix . BASE . 'notifications_archive_meta';
-
-        // Get total sent in last 24 hours
-        $metrics['total_sent'] = $this->wpdb->get_var($this->wpdb->prepare("
-            SELECT COUNT(*)
-            FROM {$notification_table}
-            WHERE post_date > %s
-        ", $yesterday));
-
-        // Get counts by notification type
-        $by_type = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT
-                meta_value as notification_type,
-                COUNT(*) as count
-            FROM
-                {$notification_meta} m
-            JOIN
-                {$notification_table} n ON m.notification_id = n.ID
-            WHERE
-                meta_key = 'notification_type'
-                AND n.post_date > %s
-            GROUP BY
-                meta_value
-            ORDER BY
-                count DESC
-        ", $yesterday));
-
-        foreach ($by_type as $type) {
-            $metrics['by_type'][$type->notification_type] = $type->count;
-        }
-
-        // Calculate read rates
-        $read_stats = $this->wpdb->get_row($this->wpdb->prepare("
-            SELECT
-                COUNT(*) as total,
-                SUM(CASE WHEN m.meta_value = '1' THEN 1 ELSE 0 END) as read_count
-            FROM
-                {$notification_meta} m
-            JOIN
-                {$notification_table} n ON m.notification_id = n.ID
-            WHERE
-                m.meta_key = 'is_read'
-                AND n.post_date > %s
-        ", date('Y-m-d H:i:s', strtotime('-7 days'))));
-
-        if ($read_stats && $read_stats->total > 0) {
-            $metrics['read_rate'] = round(($read_stats->read_count / $read_stats->total) * 100, 2);
-        }
-
-        // Calculate action rates (notifications that led to clicks/actions)
-        $action_stats = $this->wpdb->get_row($this->wpdb->prepare("
-            SELECT
-                COUNT(*) as total,
-                SUM(CASE WHEN m.meta_value = '1' THEN 1 ELSE 0 END) as action_count
-            FROM
-                {$notification_meta} m
-            JOIN
-                {$notification_table} n ON m.notification_id = n.ID
-            WHERE
-                m.meta_key = 'actioned'
-                AND n.post_date > %s
-        ", date('Y-m-d H:i:s', strtotime('-7 days'))));
-
-        if ($action_stats && $action_stats->total > 0) {
-            $metrics['action_rate'] = round(($action_stats->action_count / $action_stats->total) * 100, 2);
-        }
-
-        // Get users with most notifications
-        $most_notified = $this->wpdb->get_results($this->wpdb->prepare("
-            SELECT
-                post_author as user_id,
-                (SELECT user_nicename FROM {$this->wpdb->users} WHERE ID = post_author) as username,
-                COUNT(*) as notification_count
-            FROM
-                {$notification_table}
-            WHERE
-                post_date > %s
-            GROUP BY
-                post_author
-            ORDER BY
-                notification_count DESC
-            LIMIT 10
-        ", date('Y-m-d H:i:s', strtotime('-7 days'))));
-
-        foreach ($most_notified as $user) {
-            $metrics['most_active_users'][] = [
-                'user_id' => $user->user_id,
-                'username' => $user->username,
-                'notification_count' => $user->notification_count
-            ];
-        }
-
-        return $metrics;
-    }
-
-    /**
-     * Format the daily report into an HTML email
-     * @param array $error_stats
-     * @param array $queue_stats
-     * @param array $user_metrics
-     * @param array $content_metrics
-     * @param array $system_health
-     * @param array $notification_metrics
-     * @param array $user_retention
-     * @param array $favourite_metrics
-     *
-     * @return string
-     */
-    protected function formatDailyReport(
-        array $error_stats,
-        array $queue_stats,
-        array $user_metrics,
-        array $content_metrics,
-        array $system_health,
-        array $notification_metrics,
-        array $user_retention,
-        array $favourite_metrics
-    ):string {
-        $date = date('Y-m-d');
-        $css = $this->getEmailCSS();
-
-        $html = "<!DOCTYPE html><html><head><style>{$css}</style></head><body>";
-        $html .= "<div class='container'>";
-        $html .= "<header><h1>edmonton.ink System Report</h1><p class='date'>{$date}</p></header>";
-
-        // Executive Summary
-        $html .= "<section class='summary'>";
-        $html .= "<h2>Executive Summary</h2>";
-        $html .= "<div class='summary-grid'>";
-        $html .= "<div class='summary-item'><span class='label'>New Users</span><span class='value'>{$user_metrics['registrations']['total']}</span></div>";
-        $html .= "<div class='summary-item'><span class='label'>Active Users (DAU)</span><span class='value'>{$user_retention['active_periods']['daily']}</span></div>";
-        $html .= "<div class='summary-item'><span class='label'>Content Uploads</span><span class='value'>{$content_metrics['total_uploads']}</span></div>";
-        $html .= "<div class='summary-item'><span class='label'>New Favourites</span><span class='value'>{$favourite_metrics['favourites_last_24h']}</span></div>";
-        $html .= "<div class='summary-item'><span class='label'>Notifications</span><span class='value'>{$notification_metrics['total_sent']}</span></div>";
-        $html .= "<div class='summary-item'><span class='label'>Errors</span><span class='value'>{$error_stats['total_errors']}</span></div>";
-        $html .= "</div>";
-        $html .= "</section>";
-
-        // User Activity Section
-        $html .= "<section>";
-        $html .= "<h2>User Activity (Last 24 Hours)</h2>";
-
-        // Login statistics
-        $html .= "<div class='card'>";
-        $html .= "<h3>Login Activity</h3>";
-        $html .= "<p>Total Logins: <strong>{$user_metrics['logins']['total']}</strong></p>";
-        if (!empty($user_metrics['logins']['by_role'])) {
-            $html .= "<ul>";
-            foreach ($user_metrics['logins']['by_role'] as $role => $count) {
-                $role_name = ucfirst($role);
-                $html .= "<li>{$role_name}s: {$count}</li>";
-            }
-            $html .= "</ul>";
-        }
-        $html .= "</div>";
-
-        // Retention metrics
-        $html .= "<div class='card'>";
-        $html .= "<h3>User Retention</h3>";
-        $html .= "<div class='stat-group'>";
-        $html .= "<div class='stat-item'><span class='label'>Daily Active</span><span class='value'>{$user_retention['active_periods']['daily']}</span></div>";
-        $html .= "<div class='stat-item'><span class='label'>Weekly Active</span><span class='value'>{$user_retention['active_periods']['weekly']}</span></div>";
-        $html .= "<div class='stat-item'><span class='label'>Monthly Active</span><span class='value'>{$user_retention['active_periods']['monthly']}</span></div>";
-        $html .= "</div>";
-
-        // Inactive user metrics
-        $html .= "<h4>Inactive Users</h4>";
-        $html .= "<table class='data-table'>";
-        $html .= "<tr><th>Period</th><th>Count</th></tr>";
-        foreach ($user_retention['inactive_counts'] as $key => $count) {
-            $label = str_replace(['inactive_', 'mo', 'yr', '_'], ['', ' months', ' year', '-'], $key);
-            $html .= "<tr><td>{$label}</td><td>{$count}</td></tr>";
-        }
-        $html .= "</table>";
-        $html .= "</div>";
-
-        // Registration statistics
-        $html .= "<div class='card'>";
-        $html .= "<h3>New Registrations</h3>";
-        $html .= "<p>Total New Users: <strong>{$user_metrics['registrations']['total']}</strong></p>";
-        if (!empty($user_metrics['registrations']['by_role'])) {
-            $html .= "<ul>";
-            foreach ($user_metrics['registrations']['by_role'] as $role => $count) {
-                $role_name = ucfirst($role);
-                $html .= "<li>{$role_name}s: {$count}</li>";
-            }
-            $html .= "</ul>";
-        }
-        $html .= "</div>";
-
-        // Approvals
-        if ($user_metrics['approvals']['total'] > 0) {
-            $html .= "<div class='card'>";
-            $html .= "<h3>User Approvals</h3>";
-            $html .= "<p>Total Approvals: <strong>{$user_metrics['approvals']['total']}</strong></p>";
-            $html .= "<ul>";
-            if (!empty($user_metrics['approvals']['artists'])) {
-                $html .= "<li>Artists: " . count($user_metrics['approvals']['artists']) . " (" . implode(', ', $user_metrics['approvals']['artists']) . ")</li>";
-            }
-            if (!empty($user_metrics['approvals']['partners'])) {
-                $html .= "<li>Partners: " . count($user_metrics['approvals']['partners']) . " (" . implode(', ', $user_metrics['approvals']['partners']) . ")</li>";
-            }
-            $html .= "</ul>";
-            $html .= "</div>";
-        }
-
-        // New Shops
-        if (!empty($user_metrics['new_shops'])) {
-            $html .= "<div class='card'>";
-            $html .= "<h3>New Shops</h3>";
-            $html .= "<p>Total New Shops: <strong>{$user_metrics['new_shops_count']}</strong></p>";
-            $html .= "<ul>";
-            foreach ($user_metrics['new_shops'] as $shop) {
-                $html .= "<li>{$shop['name']} (ID: {$shop['id']})</li>";
-            }
-            $html .= "</ul>";
-            $html .= "</div>";
-        }
-
-        $html .= "</section>";
-
-        // Content Metrics Section
-        $html .= "<section>";
-        $html .= "<h2>Content Activity</h2>";
-
-        // Content uploads
-        $html .= "<div class='card'>";
-        $html .= "<h3>Content Uploads</h3>";
-        $html .= "<p>Total Uploads: <strong>{$content_metrics['total_uploads']}</strong></p>";
-
-        if (!empty($content_metrics['uploads'])) {
-            $html .= "<div class='chart-container'>";
-            foreach ($content_metrics['uploads'] as $type => $count) {
-                $type_name = ucfirst($type);
-                $percentage = ($content_metrics['total_uploads'] > 0)
-                    ? round(($count / $content_metrics['total_uploads']) * 100)
-                    : 0;
-
-                $html .= "<div class='chart-row'>";
-                $html .= "<span class='chart-label'>{$type_name}s</span>";
-                $html .= "<div class='chart-bar-container'>";
-                $html .= "<div class='chart-bar' style='width: {$percentage}%'></div>";
-                $html .= "<span class='chart-value'>{$count}</span>";
-                $html .= "</div>";
-                $html .= "</div>";
-            }
-            $html .= "</div>";
-        }
-
-        // Top uploaders
-        if (!empty($content_metrics['by_user'])) {
-            $html .= "<h4>Top Content Contributors</h4>";
-            $html .= "<ol class='top-list'>";
-            foreach (array_slice($content_metrics['by_user'], 0, 5) as $user_data) {
-                $html .= "<li><strong>{$user_data['username']}</strong>: {$user_data['total']} items (";
-                $types = [];
-                foreach ($user_data['types'] as $type => $count) {
-                    $types[] = ucfirst($type) . "s: {$count}";
-                }
-                $html .= implode(', ', $types) . ")</li>";
-            }
-            $html .= "</ol>";
-        }
-        $html .= "</div>";
-
-        // Taxonomy growth
-        if (!empty($content_metrics['taxonomy_growth'])) {
-            $html .= "<div class='card'>";
-            $html .= "<h3>Taxonomy Growth</h3>";
-
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Taxonomy</th><th>New Terms</th><th>Examples</th></tr>";
-
-            foreach ($content_metrics['taxonomy_growth'] as $taxonomy => $data) {
-                $examples = array_slice(array_map(function ($term) {
-                    return $term['name'];
-                }, $data['terms']), 0, 3);
-
-                $example_text = !empty($examples) ? implode(', ', $examples) : 'None';
-
-                $html .= "<tr>";
-                $html .= "<td>{$data['label']}</td>";
-                $html .= "<td>{$data['count']}</td>";
-                $html .= "<td>{$example_text}</td>";
-                $html .= "</tr>";
-            }
-
-            $html .= "</table>";
-            $html .= "</div>";
-        }
-
-        // Favourite metrics
-        $html .= "<div class='card'>";
-        $html .= "<h3>Favourite Activity</h3>";
-        $html .= "<p>New Favourites (24h): <strong>{$favourite_metrics['favourites_last_24h']}</strong></p>";
-        $html .= "<p>Total Favourites: <strong>{$favourite_metrics['total_favourites']}</strong></p>";
-
-        // Favourites by content type
-        if (!empty($favourite_metrics['by_content_type'])) {
-            $html .= "<h4>Favourites by Content Type</h4>";
-            $html .= "<div class='chart-container'>";
-
-            foreach ($favourite_metrics['by_content_type'] as $type => $count) {
-                $percentage = ($favourite_metrics['total_favourites'] > 0)
-                    ? round(($count / $favourite_metrics['total_favourites']) * 100)
-                    : 0;
-
-                $html .= "<div class='chart-row'>";
-                $html .= "<span class='chart-label'>" . ucfirst($type) . "s</span>";
-                $html .= "<div class='chart-bar-container'>";
-                $html .= "<div class='chart-bar' style='width: {$percentage}%'></div>";
-                $html .= "<span class='chart-value'>{$count}</span>";
-                $html .= "</div>";
-                $html .= "</div>";
-            }
-
-            $html .= "</div>";
-        }
-
-        // Top artists by favourites
-        if (!empty($favourite_metrics['top_artists'])) {
-            $html .= "<h4>Top Artists by Favourites</h4>";
-            $html .= "<ol class='top-list'>";
-            foreach (array_slice($favourite_metrics['top_artists'], 0, 5) as $artist) {
-                $html .= "<li><strong>{$artist['name']}</strong>: {$artist['favourite_count']} favourites</li>";
-            }
-            $html .= "</ol>";
-        }
-
-        $html .= "</div>";
-        $html .= "</section>";
-
-        // System Health Section
-        $html .= "<section>";
-        $html .= "<h2>System Health</h2>";
-
-        // Database metrics
-        $html .= "<div class='card'>";
-        $html .= "<h3>Database</h3>";
-        $html .= "<p>Total Size: <strong>{$system_health['database']['size']} MB</strong></p>";
-
-        // Database growth
-        if (!empty($system_health['database']['growth'])) {
-            $html .= "<h4>Growth</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Period</th><th>Growth (MB)</th><th>Percentage</th></tr>";
-
-            if (isset($system_health['database']['growth']['daily'])) {
-                $html .= "<tr><td>Daily</td><td>{$system_health['database']['growth']['daily']} MB</td><td>{$system_health['database']['growth']['daily_percent']}%</td></tr>";
-            }
-
-            if (isset($system_health['database']['growth']['weekly'])) {
-                $html .= "<tr><td>Weekly</td><td>{$system_health['database']['growth']['weekly']} MB</td><td>{$system_health['database']['growth']['weekly_percent']}%</td></tr>";
-            }
-
-            if (isset($system_health['database']['growth']['monthly_projected'])) {
-                $html .= "<tr><td>Monthly (Est.)</td><td>{$system_health['database']['growth']['monthly_projected']} MB</td><td>{$system_health['database']['growth']['monthly_percent_projected']}%</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        // Largest tables
-        if (!empty($system_health['database']['tables'])) {
-            $html .= "<h4>Largest Tables</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Table</th><th>Size (MB)</th><th>Rows</th></tr>";
-
-            foreach (array_slice($system_health['database']['tables'], 0, 5) as $table) {
-                $html .= "<tr><td>{$table['name']}</td><td>{$table['size_mb']}</td><td>{$table['rows']}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-        $html .= "</div>";
-
-        // Media storage
-        $html .= "<div class='card'>";
-        $html .= "<h3>Media Storage</h3>";
-        $html .= "<p>Total Size: <strong>{$system_health['media']['total_size']} MB</strong></p>";
-        $html .= "<p>Total Files: <strong>{$system_health['media']['file_count']}</strong></p>";
-
-        // Files by type
-        if (!empty($system_health['media']['by_type'])) {
-            $html .= "<h4>Files by Type</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Type</th><th>Count</th></tr>";
-
-            foreach ($system_health['media']['by_type'] as $type => $count) {
-                $html .= "<tr><td>.{$type}</td><td>{$count}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-        $html .= "</div>";
-
-        // Cache metrics
-        if ($system_health['cache']['hit_rate'] > 0) {
-            $html .= "<div class='card'>";
-            $html .= "<h3>Cache Performance</h3>";
-            $html .= "<p>Hit Rate: <strong>{$system_health['cache']['hit_rate']}%</strong></p>";
-            $html .= "<p>Cache Size: <strong>{$system_health['cache']['size']} MB</strong></p>";
-            $html .= "</div>";
-        }
-        $html .= "</section>";
-
-        // Notification Metrics Section
-        $html .= "<section>";
-        $html .= "<h2>Notification Activity</h2>";
-
-        $html .= "<div class='card'>";
-        $html .= "<h3>Notification Stats</h3>";
-        $html .= "<p>Total Sent (24h): <strong>{$notification_metrics['total_sent']}</strong></p>";
-        $html .= "<p>Read Rate: <strong>{$notification_metrics['read_rate']}%</strong></p>";
-        $html .= "<p>Action Rate: <strong>{$notification_metrics['action_rate']}%</strong></p>";
-
-        // Notifications by type
-        if (!empty($notification_metrics['by_type'])) {
-            $html .= "<h4>Notifications by Type</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Type</th><th>Count</th></tr>";
-
-            foreach ($notification_metrics['by_type'] as $type => $count) {
-                $type_display = str_replace('_', ' ', $type);
-                $html .= "<tr><td>" . ucfirst($type_display) . "</td><td>{$count}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-        $html .= "</div>";
-
-        // Most active users
-        if (!empty($notification_metrics['most_active_users'])) {
-            $html .= "<div class='card'>";
-            $html .= "<h3>Most Notified Users</h3>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>User</th><th>Notifications</th></tr>";
-
-            foreach (array_slice($notification_metrics['most_active_users'], 0, 5) as $user) {
-                $html .= "<tr><td>{$user['username']}</td><td>{$user['notification_count']}</td></tr>";
-            }
-
-            $html .= "</table>";
-            $html .= "</div>";
-        }
-        $html .= "</section>";
-
-        // Error Statistics Section
-        $html .= "<section>";
-        $html .= "<h2>Error Statistics</h2>";
-
-        $html .= "<div class='card'>";
-        $html .= "<h3>Error Overview</h3>";
-        $html .= "<p>Total Errors (24h): <strong>{$error_stats['total_errors']}</strong></p>";
-
-        // Errors by severity
-        if (!empty($error_stats['by_severity'])) {
-            $html .= "<h4>Errors by Severity</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Severity</th><th>Count</th></tr>";
-
-            foreach ($error_stats['by_severity'] as $severity => $count) {
-                $html .= "<tr><td>" . ucfirst($severity) . "</td><td>{$count}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        // Errors by component
-        if (!empty($error_stats['by_component'])) {
-            $html .= "<h4>Errors by Component</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Component</th><th>Count</th></tr>";
-
-            foreach ($error_stats['by_component'] as $component => $count) {
-                $html .= "<tr><td>" . ucfirst($component) . "</td><td>{$count}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        // Most frequent errors
-        if (!empty($error_stats['frequent'])) {
-            $html .= "<h4>Most Frequent Errors</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Error Type</th><th>Component</th><th>Count</th></tr>";
-
-            foreach (array_slice($error_stats['frequent'], 0, 5) as $error) {
-                $html .= "<tr><td>{$error->error_type}</td><td>{$error->component}</td><td>{$error->count}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        // Critical errors
-        if (!empty($error_stats['critical'])) {
-            $html .= "<h4>Recent Critical Errors</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Error Type</th><th>Component</th><th>Time</th></tr>";
-
-            foreach ($error_stats['critical'] as $error) {
-                $time = date('H:i:s', strtotime($error->created_at));
-                $html .= "<tr><td>{$error->error_type}</td><td>{$error->component}</td><td>{$time}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-        $html .= "</div>";
-        $html .= "</section>";
-
-        // Queue Statistics Section
-        $html .= "<section>";
-        $html .= "<h2>Queue Statistics</h2>";
-
-        $html .= "<div class='card'>";
-        $html .= "<h3>Queue Overview</h3>";
-
-        // Queue by status
-        if (!empty($queue_stats['by_status'])) {
-            $html .= "<div class='stat-group'>";
-
-            foreach ($queue_stats['by_status'] as $status => $data) {
-                $html .= "<div class='stat-item'>";
-                $html .= "<span class='label'>" . ucfirst($status) . "</span>";
-                $html .= "<span class='value'>{$data['count']}</span>";
-                $html .= "<span class='subtext'>{$data['operations']} operations</span>";
-                $html .= "</div>";
-            }
-
-            $html .= "</div>";
-        }
-
-        // Queue by type
-        if (!empty($queue_stats['by_type'])) {
-            $html .= "<h4>Queue by Operation Type</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Operation Type</th><th>Count</th></tr>";
-
-            foreach ($queue_stats['by_type'] as $type => $count) {
-                $html .= "<tr><td>{$type}</td><td>{$count}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        // Pending by priority
-        if (!empty($queue_stats['pending_by_priority'])) {
-            $html .= "<h4>Pending Operations by Priority</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Priority</th><th>Count</th></tr>";
-
-            foreach ($queue_stats['pending_by_priority'] as $priority => $count) {
-                $html .= "<tr><td>" . ucfirst($priority) . "</td><td>{$count}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        // Oldest pending operation
-        if (!empty($queue_stats['oldest_pending'])) {
-            $html .= "<h4>Oldest Pending Operation</h4>";
-            $html .= "<p><strong>ID:</strong> {$queue_stats['oldest_pending']['id']}</p>";
-            $html .= "<p><strong>Type:</strong> {$queue_stats['oldest_pending']['type']}</p>";
-            $html .= "<p><strong>Created:</strong> {$queue_stats['oldest_pending']['created_at']}</p>";
-            $html .= "<p><strong>Age:</strong> {$queue_stats['oldest_pending']['age_hours']} hours</p>";
-        }
-
-        // Recent failures
-        if (!empty($queue_stats['recent_failures'])) {
-            $html .= "<h4>Recent Failed Operations</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>ID</th><th>Type</th><th>Error</th></tr>";
-
-            foreach ($queue_stats['recent_failures'] as $failure) {
-                $html .= "<tr><td>{$failure->id}</td><td>{$failure->type}</td><td>{$failure->error_message}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        // Average processing times
-        if (!empty($queue_stats['avg_processing_times'])) {
-            $html .= "<h4>Average Processing Times</h4>";
-            $html .= "<table class='data-table'>";
-            $html .= "<tr><th>Operation Type</th><th>Average Time</th><th>Sample Size</th></tr>";
-
-            foreach ($queue_stats['avg_processing_times'] as $type => $data) {
-                $formatted_time = $this->formatSeconds($data['seconds']);
-                $html .= "<tr><td>{$type}</td><td>{$formatted_time}</td><td>{$data['samples']}</td></tr>";
-            }
-
-            $html .= "</table>";
-        }
-
-        $html .= "</div>";
-        $html .= "</section>";
-
-        // Footer
-        $html .= "<footer>";
-        $html .= "<p>This report was automatically generated by edmonton.ink System Reporting. </p>";
-        $html .= "<p>Next report will be delivered tomorrow at this time.</p>";
-        $html .= "</footer>";
-
-        $html .= "</div></body></html>";
-
-        return $html;
-    }
-
-    /**
-     * Format seconds into a human-readable string
-     * @param float $seconds
-     *
-     * @return string
-     */
-    protected function formatSeconds(float $seconds):string
-    {
-        if ($seconds < 1) {
-            return round($seconds * 1000) . " ms";
-        } elseif ($seconds < 60) {
-            return round($seconds, 2) . " seconds";
-        } elseif ($seconds < 3600) {
-            $minutes = floor($seconds / 60);
-            $remaining_seconds = $seconds % 60;
-            return "{$minutes}m " . round($remaining_seconds) . "s";
-        } else {
-            $hours = floor($seconds / 3600);
-            $minutes = floor(($seconds % 3600) / 60);
-            return "{$hours}h {$minutes}m";
-        }
-    }
-
-    /**
-     * Send email to admin
-     * @param string $subject
-     * @param string $content
-     *
-     * @return void
-     */
-    protected function sendAdminEmail(string $subject, string $content):void
-    {
-        $admin_email = get_option('admin_email');
-
-        JVB()->email()->sendEmail($admin_email, $subject, $content);
-
-        // Also send to any additional configured recipients
-        $additional_recipients = get_option('jvb_report_recipients', []);
-        if (!empty($additional_recipients)) {
-            foreach ($additional_recipients as $recipient) {
-				JVB()->email()->sendEmail($recipient, $subject, $content);
-            }
-        }
-    }
-
-    /**
-     * Get CSS styles for email
-     * @return string
-     */
-    protected function getEmailCSS():string
-    {
-        return '
-        body {
-            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-            line-height: 1.6;
-            color: #333;
-            background-color: #f5f5f5;
-            margin: 0;
-            padding: 0;
-        }
-        .container {
-            max-width: 900px;
-            margin: 0 auto;
-            background: #fff;
-            padding: 20px;
-            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
-        }
-        header {
-            text-align: center;
-            margin-bottom: 30px;
-            padding-bottom: 20px;
-            border-bottom: 1px solid #eee;
-        }
-        header h1 {
-            color: #FF0080;
-            margin: 0;
-            font-size: 28px;
-        }
-        header .date {
-            font-size: 16px;
-            color: #777;
-        }
-        h2 {
-            color: #333;
-            padding-bottom: 10px;
-            border-bottom: 2px solid #FF0080;
-            margin-top: 40px;
-        }
-        h3 {
-            color: #444;
-            margin-top: 25px;
-            margin-bottom: 15px;
-        }
-        h4 {
-            color: #555;
-            margin-top: 20px;
-            margin-bottom: 10px;
-            font-weight: 500;
-        }
-        section {
-            margin-bottom: 40px;
-        }
-        .card {
-            background: #fff;
-            border: 1px solid #ddd;
-            padding: 15px;
-            margin-bottom: 15px;
-            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
-        }
-        .summary {
-            background-color: #f8f8f8;
-            padding: 15px;
-            margin: 20px 0;
-            border-left: 4px solid #FF0080;
-        }
-        .summary-grid {
-            display: grid;
-            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
-            gap: 15px;
-            margin-top: 15px;
-        }
-        .summary-item {
-            text-align: center;
-            padding: 10px;
-            background: white;
-            border-radius: 4px;
-            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
-        }
-        .summary-item .label {
-            display: block;
-            font-size: 14px;
-            color: #666;
-            margin-bottom: 5px;
-        }
-        .summary-item .value {
-            display: block;
-            font-size: 24px;
-            font-weight: bold;
-            color: #FF0080;
-        }
-        .data-table {
-            width: 100%;
-            border-collapse: collapse;
-            margin: 15px 0;
-            font-size: 14px;
-        }
-        .data-table th {
-            background-color: #f5f5f5;
-            text-align: left;
-            padding: 8px;
-            border-bottom: 2px solid #ddd;
-        }
-        .data-table td {
-            padding: 8px;
-            border-bottom: 1px solid #ddd;
-        }
-        .data-table tr:nth-child(even) {
-            background-color: #f9f9f9;
-        }
-        .chart-container {
-            margin: 20px 0;
-        }
-        .chart-row {
-            display: flex;
-            align-items: center;
-            margin-bottom: 8px;
-        }
-        .chart-label {
-            width: 120px;
-            text-align: right;
-            padding-right: 10px;
-            font-size: 14px;
-        }
-        .chart-bar-container {
-            flex: 1;
-            height: 25px;
-            background-color: #eee;
-            border-radius: 3px;
-            position: relative;
-            overflow: hidden;
-        }
-        .chart-bar {
-            height: 100%;
-            background-color: #FF0080;
-            border-radius: 3px 0 0 3px;
-        }
-        .chart-value {
-            position: absolute;
-            right: 10px;
-            top: 50%;
-            transform: translateY(-50%);
-            color: #333;
-            font-weight: bold;
-            font-size: 14px;
-            text-shadow: 0 0 5px white;
-        }
-        .stat-group {
-            display: flex;
-            flex-wrap: wrap;
-            gap: 15px;
-            margin: 15px 0;
-        }
-        .stat-item {
-            background: #f8f8f8;
-            flex: 1;
-            min-width: 120px;
-            padding: 10px;
-            text-align: center;
-            border-radius: 3px;
-        }
-        .stat-item .label {
-            font-size: 14px;
-            color: #666;
-            display: block;
-        }
-        .stat-item .value {
-            font-size: 20px;
-            font-weight: bold;
-            display: block;
-            margin: 5px 0;
-            color: #333;
-        }
-        .stat-item .subtext {
-            font-size: 12px;
-            color: #999;
-            display: block;
-        }
-        .top-list {
-            margin: 15px 0;
-            padding-left: 20px;
-        }
-        .top-list li {
-            margin-bottom: 8px;
-        }
-        footer {
-            text-align: center;
-            margin-top: 30px;
-            padding-top: 15px;
-            border-top: 1px solid #eee;
-            color: #777;
-            font-size: 14px;
-        }
-        ';
-    }
-
-    /**
-     * Add recipient to the report email list
-     * @param string $email
-     *
-     * @return bool
-     */
-    public function addReportRecipient(string $email):bool
-    {
-        if (!is_email($email)) {
-            return false;
-        }
-
-        $current_recipients = get_option('jvb_report_recipients', []);
-        if (!in_array($email, $current_recipients)) {
-            $current_recipients[] = $email;
-            update_option('jvb_report_recipients', $current_recipients);
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Remove recipient from the report email list
-     * @param string $email
-     *
-     * @return bool
-     */
-    public function removeReportRecipient(string $email):bool
-    {
-        $current_recipients = get_option('jvb_report_recipients', []);
-        $key = array_search($email, $current_recipients);
-
-        if ($key !== false) {
-            unset($current_recipients[$key]);
-            update_option('jvb_report_recipients', array_values($current_recipients));
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Change report frequency
-     * @param string $interval
-     *
-     * @return bool
-     */
-    public function setReportInterval(string $interval):bool
-    {
-        if (!in_array($interval, ['daily', 'weekly'])) {
-            return false;
-        }
-
-        // Clear existing schedule
-        wp_clear_scheduled_hook('jvb_generate_daily_report');
-
-        // Set new interval
-        $this->report_interval = $interval;
-
-        // Schedule new report
-        if ($interval === 'daily') {
-            wp_schedule_event(strtotime('tomorrow 01:00:00'), 'daily', 'jvb_generate_daily_report');
-        } else {
-            wp_schedule_event(strtotime('next monday 01:00:00'), 'weekly', 'jvb_generate_daily_report');
-        }
-
-        return true;
-    }
-
-    /**
-     * Generate an on-demand report
-     * @return bool
-     */
-    public function generateOnDemandReport():bool
-    {
-        // Collect metrics like in the daily report
-        $error_stats = $this->gatherErrorStats();
-        $queue_stats = $this->gatherQueueStats();
-        $user_metrics = $this->gatherUserMetrics();
-        $content_metrics = $this->gatherContentMetrics();
-        $system_health = $this->gatherSystemHealthMetrics();
-        $notification_metrics = $this->gatherNotificationMetrics();
-        $user_retention = $this->gatherUserRetentionMetrics();
-        $favourite_metrics = $this->gatherFavouriteMetrics();
-
-        // Format report
-        $subject = "[edmonton.ink] On-Demand System Report - " . date('Y-m-d H:i');
-        $content = $this->formatDailyReport(
-            $error_stats,
-            $queue_stats,
-            $user_metrics,
-            $content_metrics,
-            $system_health,
-            $notification_metrics,
-            $user_retention,
-            $favourite_metrics
-        );
-
-        // Send to admin only
-        $admin_email = get_option('admin_email');
-        return JVB()->email()->sendEmail($admin_email, $subject, $content);
-    }
-
-    //List report:
-    /**
-     * @return bool
-     */
-    public function rebuildAllListStats():bool
-    {
-        $items_table = $this->wpdb->prefix . BASE . 'list_items';
-        $stats_table = $this->wpdb->prefix . BASE . 'list_stats';
-
-        // Start transaction
-        $this->wpdb->query('START TRANSACTION');
-
-        try {
-            // Get distinct item types and IDs
-            $distinct_items = $this->wpdb->get_results("
-            SELECT DISTINCT item_type, item_id
-            FROM {$items_table}
-        ");
-
-            // Prepare items for batch update
-            $items_to_update = [];
-            foreach ($distinct_items as $item) {
-                $items_to_update[] = [$item->item_type, $item->item_id];
-            }
-
-            // Process in reasonable batches (500 items per batch)
-            $batches = array_chunk($items_to_update, 500);
-
-            foreach ($batches as $batch) {
-                $this->batchUpdateListStats($batch);
-            }
-
-            // Commit transaction
-            $this->wpdb->query('COMMIT');
-
-            // Log success
-            JVB()->error()->log(
-                'favourites',
-                'Successfully rebuilt all list statistics',
-                ['items_processed' => count($distinct_items)],
-                'info'
-            );
-
-            return true;
-        } catch (Exception $e) {
-            // Rollback on error
-            $this->wpdb->query('ROLLBACK');
-
-            JVB()->error()->log(
-                'favourites',
-                'Error rebuilding list statistics: ' . $e->getMessage(),
-                [],
-                'error'
-            );
-
-            return false;
-        }
-    }
-
-    /**
-     * Update list statistics for multiple items at once
-     *
-     * @param array $items Array of [type, id] pairs
-     */
-    protected function batchUpdateListStats(array $items):void
-    {
-        if (empty($items)) {
-            return;
-        }
-
-        $stats_table = $this->wpdb->prefix . BASE . 'list_stats';
-        $items_table = $this->wpdb->prefix . BASE . 'list_items';
-
-        // Get counts for all items at once
-        $query_parts = [];
-        $query_params = [];
-
-        foreach ($items as $item_data) {
-            list($type, $id) = $item_data;
-            $query_parts[] = "(item_type = %s AND item_id = %d)";
-            $query_params[] = $type;
-            $query_params[] = $id;
-        }
-
-        $count_query = "SELECT item_type, item_id, COUNT(DISTINCT list_id) as list_count
-                   FROM {$items_table}
-                   WHERE " . implode(' OR ', $query_parts) . "
-                   GROUP BY item_type, item_id";
-
-        $results = $this->wpdb->get_results($this->wpdb->prepare($count_query, $query_params));
-
-        if (empty($results)) {
-            return;
-        }
-
-        $current_time = current_time('mysql');
-        $existing_items = [];
-
-        // Find existing records
-        $existing_query_parts = [];
-        $existing_query_params = [];
-
-        foreach ($results as $result) {
-            $existing_query_parts[] = "(item_type = %s AND item_id = %d)";
-            $existing_query_params[] = $result->item_type;
-            $existing_query_params[] = $result->item_id;
-        }
-
-        $existing_query = "SELECT item_type, item_id
-                      FROM {$stats_table}
-                      WHERE " . implode(' OR ', $existing_query_parts);
-
-        $existing_results = $this->wpdb->get_results($this->wpdb->prepare($existing_query, $existing_query_params));
-
-        foreach ($existing_results as $existing) {
-            $existing_items["{$existing->item_type}-{$existing->item_id}"] = true;
-        }
-
-        // Prepare update and insert statement formats
-        $update_query = "UPDATE {$stats_table}
-                    SET list_count = %d,
-                        last_added = %s
-                    WHERE item_type = %s AND item_id = %d";
-
-        $insert_query = "INSERT INTO {$stats_table}
-                    (item_type, item_id, list_count, last_added)
-                    VALUES (%s, %d, %d, %s)";
-
-        // Prepare batched updates and inserts
-        $updates = [];
-        $inserts = [];
-
-        foreach ($results as $result) {
-            $key = "{$result->item_type}-{$result->item_id}";
-
-            if (isset($existing_items[$key])) {
-                $updates[] = $this->wpdb->prepare(
-                    $update_query,
-                    $result->list_count,
-                    $current_time,
-                    $result->item_type,
-                    $result->item_id
-                );
-            } else {
-                $inserts[] = $this->wpdb->prepare(
-                    "(%s, %d, %d, %s)",
-                    $result->item_type,
-                    $result->item_id,
-                    $result->list_count,
-                    $current_time
-                );
-            }
-        }
-
-        // Execute updates in batches
-        if (!empty($updates)) {
-            foreach ($updates as $update_sql) {
-                $this->wpdb->query($update_sql);
-            }
-        }
-
-        // Execute inserts in one go if possible
-        if (!empty($inserts)) {
-            $multi_insert = "INSERT INTO {$stats_table}
-                        (item_type, item_id, list_count, last_added)
-                        VALUES " . implode(', ', $inserts);
-            $this->wpdb->query($multi_insert);
-        }
-    }
-}
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index addfcae..ae2a3ee 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--btn_)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--primary-color,#06c);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}.dashboard.settings nav.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--rgb-medium)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin:0 auto 0 var(--btn_)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index d59c952..bc9ce27 100644
--- a/assets/css/forms.min.css
+++ b/assets/css/forms.min.css
@@ -1 +1 @@
-input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}.selected-items{--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.selected-item{padding:.25rem .5rem;margin:.125em;background:var(--base-100);border-radius:.25rem;font-size:var(--txt-medium);border:1px solid var(--base-200);position:relative}.remove-item{background:0 0;border:none;padding:.25rem;cursor:pointer;color:#666;border-radius:var(--radius);width:1.5em;height:1.5em}.remove-item .close{width:.5em;height:.5em}.remove-item:hover{color:var(--action-0);background:#fee}.clear-filters{margin-left:auto;border:1px solid var(--base-200)}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:38px;height:38px;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.items-container{margin:0;padding:0;width:100%}.create-new-term{margin-top:1rem;width:100%}.create-new-term .field,.create-new-term[open] summary{margin-bottom:1rem}.create-new-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(1fr,100%))}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--content)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group{margin-bottom:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--op-1))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--op-4));opacity:1}.item-grid.preview summary span{display:none}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--radius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .group-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:var(--btn_);right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{order:-1;grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--z-9);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--btn);bottom:var(--btn);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--op-6));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--op-6))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--btn);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--btnbtn));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--btn_);bottom:var(--btn_);left:1rem;right:1rem;border-radius:var(--radius-outer);padding:1rem;z-index:var(--z-7);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px rgba(var(--base-rgb),var(--op-6));color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--radius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--txt-small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--txt-small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--txt-small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{--height:fit-content;padding:.5rem}.success-message{color:var(--success,#16a34a);background-color:var(--success-bg,#f0fdf4);border:1px solid var(--success,#16a34a);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--success-bg,#f0fdf4);border:2px solid var(--success,#16a34a);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success,#16a34a);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error,#dc2626)}.error-message{color:var(--error,#dc2626);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--error-bg,#fee);border:1px solid var(--error,#dc2626);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success,#16a34a)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
+input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}.selected-items{--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.selected-item{padding:.25rem .5rem;margin:.125em;background:var(--base-100);border-radius:.25rem;font-size:var(--txt-medium);border:1px solid var(--base-200);position:relative}.remove-item{background:0 0;border:none;padding:.25rem;cursor:pointer;color:#666;border-radius:var(--radius);width:1.5em;height:1.5em}.remove-item .close{width:.5em;height:.5em}.remove-item:hover{color:var(--action-0);background:#fee}.clear-filters{margin-left:auto;border:1px solid var(--base-200)}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:38px;height:38px;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.items-container{margin:0;padding:0;width:100%}.create-new-term{margin-top:1rem;width:100%}.create-new-term .field,.create-new-term[open] summary{margin-bottom:1rem}.create-new-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(1fr,100%))}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--content)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group{margin-bottom:0}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--op-1))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--op-4));opacity:1}.item-grid.preview summary span{display:none}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--radius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .group-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:var(--btn_);right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{order:-1;grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--z-9);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--btn);bottom:var(--btn);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--op-6));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--op-6))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--btn);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--btnbtn));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--btn_);bottom:var(--btn_);left:1rem;right:1rem;border-radius:var(--radius-outer);padding:1rem;z-index:var(--z-7);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px rgba(var(--base-rgb),var(--op-6));color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--radius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--txt-small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--txt-small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--txt-small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{min-height:0;padding:.5rem}.success-message{color:var(--success);background-color:var(--successBack);border:1px solid var(--success);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--successBack);border:2px solid var(--success);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error)}.error-message{color:var(--error);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--errorBack);border:1px solid var(--error);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 3d46224..d4cb8bc 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -14,7 +14,7 @@
 		DataStore.instance = this;
 
 		// Shared resources
-		this.dbConfig = new Map();		// Definitions for the databases
+		this.dbConfig = new Map();      // Definitions for the databases
 		this.databases = new Map();     // Shared IndexedDB connections
 		this.stores = new Map();        // Registered store namespaces
 		this.subscribers = new Map();   // Per-store event subscribers
@@ -27,8 +27,6 @@
 		this.loading = document.querySelector('dialog.loading');
 
 		this.init();
-
-		// window.addEventListener('beforeunload', () => this.destroy());
 	}
 
 	async init() {
@@ -69,7 +67,6 @@
 				throw new Error(`Store "${config.storeName}" requires keyPath`);
 			}
 
-
 			const storeKey = `${name}_${config.storeName}`;
 
 			const store = {
@@ -93,16 +90,14 @@
 					// Behavior
 					showLoading: false,
 					delayFetch: true,
-					validateData: true, // Validate data is serializable
+					validateData: true,
 					...config
 				},
 				dbKey: name,
 				storeKey: storeKey,
 				data: new Map(),
 				cache: new Map(),
-				httpHeaders: new Map(),
-				subscribers: new Map(),
-				filters: {...(config.filters || {}) },
+				filters: {...(config.filters || {})},
 				isFetching: false,
 				currentRequest: null,
 				lastResponse: null,
@@ -122,7 +117,6 @@
 			}
 		});
 
-
 		// Initialize database asynchronously
 		this.initDB(name).catch(error => {
 			console.error(`Failed to initialize store "${name}":`, error);
@@ -157,7 +151,6 @@
 
 			// Cache methods
 			clearCache: () => this.clearCache(name),
-			clearHttpHeaders: (key) => this.clearHttpHeaders(name, key),
 
 			// Event methods
 			subscribe: (callback) => this.subscribe(name, callback),
@@ -242,9 +235,10 @@
 		return formData;
 	}
 
-	/**
-	 * Initialize database for a specific store
-	 */
+	/***********************************************************************
+	 * DATABASE INITIALIZATION
+	 ***********************************************************************/
+
 	async initDB(name) {
 		const db = this.dbConfig.get(name);
 		if (!db || db._initialized) return;
@@ -290,7 +284,7 @@
 					this.loadStoreDataInBackground(storeName);
 					this.notify(storeName, 'db-init');
 				}
-			})
+			});
 
 		} catch (error) {
 			console.error(`Failed to initialize database for store "${name}":`, error);
@@ -332,29 +326,55 @@
 			});
 		}
 
-		// Cache store
+		// Cache store (now includes HTTP headers)
 		if (config.endpoint && !db.objectStoreNames.contains('cache')) {
 			const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
 			cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
 		}
+	}
 
-		// HTTP headers store
-		if (config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
-			db.createObjectStore('headers', { keyPath: 'key' });
+	/**
+	 * Generic loader for any object store
+	 */
+	async loadFromObjectStore(name, storeName, processItem) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains(storeName)) {
+			return [];
 		}
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction([storeName], 'readonly');
+			const objectStore = tx.objectStore(storeName);
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				const items = e.target.result || [];
+				items.forEach(processItem);
+				resolve(items);
+			};
+
+			request.onerror = () => resolve([]);
+		});
 	}
 
 	loadStoreDataInBackground(name) {
 		const store = this.stores.get(name);
 		if (!store?.db) return;
 
-		const tasks = [
-			this.loadStoreData(name),
-			this.loadStoreCache(name),
-			this.loadStoreHeaders(name)
-		];
+		Promise.all([
+			// Load main data
+			this.loadFromObjectStore(name, store.config.storeName, (item) => {
+				const key = this.getItemKey(item, store.config.keyPath);
+				store.data.set(key, item);
+			}),
 
-		Promise.all(tasks)
+			// Load cache (includes HTTP headers now!)
+			this.loadFromObjectStore(name, 'cache', (item) => {
+				if (this.isCacheValid(item, store.config.TTL)) {
+					store.cache.set(item.key, item);
+				}
+			})
+		])
 			.then(() => {
 				this.notify(name, 'data-ready');
 
@@ -407,71 +427,6 @@
 		}
 	}
 
-	async loadStoreData(name) {
-		const store = this.stores.get(name);
-		if (!store?.db) return;
-
-		return new Promise((resolve) => {
-			const tx = store.db.transaction([store.config.storeName], 'readonly');
-			const objectStore = tx.objectStore(store.config.storeName);
-			const request = objectStore.getAll();
-
-			request.onsuccess = (e) => {
-				const items = e.target.result || [];
-				items.forEach(item => {
-					const key = this.getItemKey(item, store.config.keyPath);
-					store.data.set(key, item);
-				});
-				this.notify(name, 'data-loaded', { count: items.length });
-				resolve(items);
-			};
-
-			request.onerror = () => resolve([]);
-		});
-	}
-
-	async loadStoreCache(name) {
-		const store = this.stores.get(name);
-		if (!store?.db || !store.db.objectStoreNames.contains('cache')) return;
-
-		return new Promise((resolve) => {
-			const tx = store.db.transaction(['cache'], 'readonly');
-			const objectStore = tx.objectStore('cache');
-			const request = objectStore.getAll();
-
-			request.onsuccess = (e) => {
-				(e.target.result || []).forEach(item => {
-					if (this.isCacheValid(item, store.config.TTL)) {
-						store.cache.set(item.key, item);
-					}
-				});
-				resolve();
-			};
-
-			request.onerror = () => resolve();
-		});
-	}
-
-	async loadStoreHeaders(name) {
-		const store = this.stores.get(name);
-		if (!store?.db || !store.db.objectStoreNames.contains('headers')) return;
-
-		return new Promise((resolve) => {
-			const tx = store.db.transaction(['headers'], 'readonly');
-			const objectStore = tx.objectStore('headers');
-			const request = objectStore.getAll();
-
-			request.onsuccess = (e) => {
-				(e.target.result || []).forEach(header => {
-					store.httpHeaders.set(header.key, header);
-				});
-				resolve();
-			};
-
-			request.onerror = () => resolve();
-		});
-	}
-
 	async ensureStoreInitialized(name) {
 		const store = this.stores.get(name);
 		if (!store) {
@@ -483,6 +438,42 @@
 		}
 	}
 
+	/***********************************************************************
+	 * TRANSACTION HELPER
+	 ***********************************************************************/
+
+	/**
+	 * Create transaction helper - reduces boilerplate
+	 */
+	async withTransaction(name, storeNames, mode, callback) {
+		const store = this.stores.get(name);
+		if (!store?.db) return null;
+
+		// Ensure storeNames is an array
+		if (typeof storeNames === 'string') storeNames = [storeNames];
+
+		return new Promise((resolve, reject) => {
+			const tx = store.db.transaction(storeNames, mode);
+			const stores = storeNames.map(name => tx.objectStore(name));
+			const objectStore = stores.length === 1 ? stores[0] : stores;
+
+			let result;
+			tx.oncomplete = () => resolve(result);
+			tx.onerror = () => reject(tx.error);
+
+			// Call callback immediately to queue operations
+			try {
+				result = callback(objectStore, tx);
+			} catch (error) {
+				reject(error);
+			}
+		});
+	}
+
+	/***********************************************************************
+	 * FETCH & DATA PROCESSING
+	 ***********************************************************************/
+
 	async fetch(name) {
 		await this.ensureStoreInitialized(name);
 
@@ -524,15 +515,11 @@
 
 			const url = this.buildFetchUrl(name);
 			const headers = { ...store.config.headers };
-			const cachedHeaders = store.httpHeaders.get(cacheKey);
 
-			if (store.config.useHttpCaching && cachedHeaders) {
-				if (cachedHeaders.etag) {
-					headers['If-None-Match'] = cachedHeaders.etag;
-				}
-				if (cachedHeaders.lastModified) {
-					headers['If-Modified-Since'] = cachedHeaders.lastModified;
-				}
+			// Use HTTP cache headers from cache entry
+			if (store.config.useHttpCaching && cached) {
+				if (cached.etag) headers['If-None-Match'] = cached.etag;
+				if (cached.lastModified) headers['If-Modified-Since'] = cached.lastModified;
 			}
 
 			const controller = new AbortController();
@@ -556,7 +543,6 @@
 				}
 
 				// No cached data but server says not modified - return empty result
-				// This can happen on first load when cache headers exist but data doesn't
 				this.notify(name, 'data-loaded', {
 					cached: false,
 					notModified: true,
@@ -581,10 +567,7 @@
 
 			const data = await response.json();
 
-			if (store.config.useHttpCaching) {
-				this.storeResponseHeaders(name, cacheKey, response);
-			}
-			await this.processFetchedData(name, data, cacheKey);
+			await this.processFetchedData(name, data, cacheKey, response);
 
 			this.notify(name, 'data-loaded', {
 				cached: false,
@@ -628,47 +611,53 @@
 		return params.toString() ? `${baseUrl}?${params}` : baseUrl;
 	}
 
-	async processFetchedData(name, data, cacheKey) {
+	/**
+	 * Process fetched data (batch from server)
+	 */
+	async processFetchedData(name, data, cacheKey, response) {
 		const store = this.stores.get(name);
 		const items = data.items || [];
+		const changes = []; // Track all changes
 
-		// Batch process all items in a single transaction
+		// Batch process with single transaction
 		if (store.db && items.length > 0) {
-			const tx = store.db.transaction([store.config.storeName], 'readwrite');
-			const objectStore = tx.objectStore(store.config.storeName);
+			await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+				items.forEach(item => {
+					try {
+						// Use shared save logic
+						const changeInfo = this._saveItem(name, item);
+						changes.push(changeInfo);
 
-			for (const item of items) {
-				const result = this.processForStorage(item, store.config.validateData);
-				if (result.valid) {
-					const key = this.getItemKey(result.data, store.config.keyPath);
-
-					// Store in memory
-					store.data.set(key, item);
-
-					// Queue for batch write
-					await objectStore.put(result.data);
-				}
-			}
-
-			// Wait for transaction to complete
-			await new Promise((resolve, reject) => {
-				tx.oncomplete = () => resolve();
-				tx.onerror = () => reject(tx.error);
+						// Queue for batch write
+						objectStore.put(changeInfo.processed);
+					} catch (error) {
+						console.error(`Error processing item:`, error);
+					}
+				});
 			});
-
 		}
 
+		// Update cache (now includes HTTP headers!)
 		const cacheEntry = {
 			key: cacheKey,
 			items: items.map(item => this.getItemKey(item, store.config.keyPath)),
 			timestamp: Date.now(),
 			endpoint: store.config.endpoint,
-			filters: { ...store.filters }
+			filters: { ...store.filters },
+			etag: response.headers.get('ETag'),
+			lastModified: response.headers.get('Last-Modified')
 		};
 
 		store.cache.set(cacheKey, cacheEntry);
-		await this.saveToCache(name, cacheKey, cacheEntry);
 
+		// Save cache to IndexedDB
+		if (store.db?.objectStoreNames.contains('cache')) {
+			await this.withTransaction(name, 'cache', 'readwrite', (objectStore) => {
+				objectStore.put(cacheEntry);
+			});
+		}
+
+		// Update lastResponse metadata
 		store.lastResponse = {
 			...data,
 			has_more: data.has_more || false,
@@ -676,13 +665,28 @@
 			pages: data.pages || 1,
 			queue_stats: data.queue_stats || {}
 		};
+
+		// Emit events for items with status changes
+		changes.forEach(changeInfo => {
+			if (changeInfo.statusChanged) {
+				this.notify(name, 'item-saved', {
+					item: changeInfo.item,
+					key: changeInfo.key,
+					previousItem: changeInfo.previousItem
+				});
+			}
+		});
 	}
 
+	/***********************************************************************
+	 * SAVE OPERATIONS
+	 ***********************************************************************/
+
 	/**
-	 * Save item to store
-	 * IMPORTANT: Item must be serializable (no DOM, FormData, Blobs)
+	 * Internal method: Save a single item with full tracking
+	 * Returns change info without writing to IndexedDB (caller handles that)
 	 */
-	async save(name, item) {
+	_saveItem(name, item) {
 		const store = this.stores.get(name);
 
 		const result = this.processForStorage(item, store.config.validateData);
@@ -693,18 +697,42 @@
 
 		const key = this.getItemKey(processed, store.config.keyPath);
 
-		// Store the original in memory (with original data intact)
+		// Capture previous state
+		const previousItem = store.data.get(key);
+
+		// Update in-memory store (with original data intact)
 		store.data.set(key, item);
 
-		// Store processed in IndexedDB
-		if (store.db) {
-			const tx = store.db.transaction([store.config.storeName], 'readwrite');
-			const objectStore = tx.objectStore(store.config.storeName);
-			await objectStore.put(processed);
-		}
+		// Return change info for event emission
+		return {
+			item,
+			previousItem,
+			key,
+			processed,
+			statusChanged: previousItem && previousItem.status !== item.status
+		};
+	}
 
-		this.notify(name, 'item-saved', { item, key });
-		return key;
+	/**
+	 * Save single item (public API)
+	 */
+	async save(name, item) {
+		const store = this.stores.get(name);
+		const changeInfo = this._saveItem(name, item);
+
+		// Write to IndexedDB immediately for single saves
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			objectStore.put(changeInfo.processed);
+		});
+
+		// Always emit for explicit saves
+		this.notify(name, 'item-saved', {
+			item: changeInfo.item,
+			key: changeInfo.key,
+			previousItem: changeInfo.previousItem
+		});
+
+		return changeInfo.key;
 	}
 
 	processForStorage(obj, validate = true, path = 'root') {
@@ -777,15 +805,17 @@
 			: { valid: true, data: null };
 	}
 
+	/***********************************************************************
+	 * DATA ACCESS
+	 ***********************************************************************/
+
 	async delete(name, id) {
 		const store = this.stores.get(name);
 		store.data.delete(id);
 
-		if (store.db) {
-			const tx = store.db.transaction([store.config.storeName], 'readwrite');
-			const objectStore = tx.objectStore(store.config.storeName);
-			await objectStore.delete(id);
-		}
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			objectStore.delete(id);
+		});
 
 		this.notify(name, 'item-deleted', { id });
 	}
@@ -821,34 +851,52 @@
 		store.data.clear();
 		store.cache.clear();
 
-		if (store.db) {
-			const tx = store.db.transaction([store.config.storeName], 'readwrite');
-			const objectStore = tx.objectStore(store.config.storeName);
-			await objectStore.clear();
-		}
+		await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+			objectStore.clear();
+		});
 
 		this.notify(name, 'data-cleared');
 	}
 
-	setFilter(name, key, value) {
-		const store = this.stores.get(name);
-		const oldValue = store.filters[key];
+	/***********************************************************************
+	 * FILTER OPERATIONS (UNIFIED)
+	 ***********************************************************************/
 
-		if (value === null || value === undefined || value === '') {
-			delete store.filters[key];
+	/**
+	 * Unified filter update - handles all filter operations
+	 */
+	async updateFilters(name, updates, clearAll = false) {
+		const store = this.stores.get(name);
+		const oldFilters = { ...store.filters };
+
+		if (clearAll) {
+			store.filters = { ...store.config.filters };
 		} else {
-			store.filters[key] = value;
+			// Apply updates (null/undefined/'' = delete)
+			Object.entries(updates).forEach(([key, value]) => {
+				if (value === null || value === undefined || value === '') {
+					delete store.filters[key];
+				} else {
+					store.filters[key] = value;
+				}
+			});
 		}
+
 		this.notify(name, 'filters-changed', {
+			oldFilters,
 			filters: store.filters,
-			changed: { key, oldValue, newValue: value }
+			updates
 		});
 
 		if (store.config.endpoint) {
-			this.fetch(name);
+			await this.fetch(name);
 		}
 	}
 
+	setFilter(name, key, value) {
+		return this.updateFilters(name, { [key]: value });
+	}
+
 	async setFilters(name, filters) {
 		const store = this.stores.get(name);
 
@@ -858,87 +906,54 @@
 
 		if (!hasChanges) return;
 
-		store.filters = { ...store.filters, ...filters };
-
-		this.notify(name, 'filters-changed', {
-			filters: store.filters,
-			changed: filters
-		});
-
-		if (store.config.endpoint) {
-			await this.fetch(name);
-		}
+		return this.updateFilters(name, filters);
 	}
 
 	removeFilter(name, key) {
-		const store = this.stores.get(name);
-		const oldValue = store.filters[key];
-
-		if (oldValue !== undefined) {
-			delete store.filters[key];
-
-			this.notify(name, 'filters-changed', {
-				filters: store.filters,
-				removed: { key, oldValue }
-			});
-
-			if (store.config.endpoint) {
-				this.fetch(name);
-			}
-		}
+		return this.updateFilters(name, { [key]: null });
 	}
 
 	clearFilters(name) {
-		const store = this.stores.get(name);
-		const oldFilters = { ...store.filters };
-
-		store.filters = { ...store.config.filters };
-
-		this.notify(name, 'filters-cleared', {
-			oldFilters,
-			filters: store.filters
-		});
-
-		if (store.config.endpoint) {
-			this.fetch(name);
-		}
+		return this.updateFilters(name, {}, true);
 	}
 
+	/***********************************************************************
+	 * CACHE OPERATIONS
+	 ***********************************************************************/
+
 	clearCache(name) {
 		const store = this.stores.get(name);
 		store.cache.clear();
 
-		if (store.db && store.db.objectStoreNames.contains('cache')) {
-			const tx = store.db.transaction(['cache'], 'readwrite');
-			const objectStore = tx.objectStore('cache');
-			objectStore.clear();
+		if (store.db?.objectStoreNames.contains('cache')) {
+			this.withTransaction(name, 'cache', 'readwrite', (objectStore) => {
+				objectStore.clear();
+			});
 		}
 
 		this.notify(name, 'cache-cleared');
 	}
 
-	clearHttpHeaders(name, cacheKey = null) {
-		const store = this.stores.get(name);
-
-		if (cacheKey) {
-			store.httpHeaders.delete(cacheKey);
-
-			if (store.db && store.db.objectStoreNames.contains('headers')) {
-				const tx = store.db.transaction(['headers'], 'readwrite');
-				const objectStore = tx.objectStore('headers');
-				objectStore.delete(cacheKey);
-			}
-		} else {
-			store.httpHeaders.clear();
-
-			if (store.db && store.db.objectStoreNames.contains('headers')) {
-				const tx = store.db.transaction(['headers'], 'readwrite');
-				const objectStore = tx.objectStore('headers');
-				objectStore.clear();
-			}
-		}
+	generateCacheKey(filters) {
+		const normalized = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+		return JSON.stringify(normalized);
 	}
 
+	isCacheValid(entry, ttl) {
+		if (!entry || !entry.timestamp) return false;
+		const age = Date.now() - entry.timestamp;
+		return age < ttl;
+	}
+
+	/***********************************************************************
+	 * EVENT SYSTEM
+	 ***********************************************************************/
+
 	subscribe(name, callback) {
 		if (!this.subscribers.has(name)) {
 			this.subscribers.set(name, new Set());
@@ -961,49 +976,9 @@
 		});
 	}
 
-	storeResponseHeaders(name, key, response) {
-		const store = this.stores.get(name);
-
-		const headers = {
-			key,
-			etag: response.headers.get('ETag'),
-			lastModified: response.headers.get('Last-Modified'),
-			timestamp: Date.now()
-		};
-
-		store.httpHeaders.set(key, headers);
-
-		if (store.db && store.db.objectStoreNames.contains('headers')) {
-			const tx = store.db.transaction(['headers'], 'readwrite');
-			const objectStore = tx.objectStore('headers');
-			objectStore.put(headers);
-		}
-	}
-
-	async saveToCache(name, key, data) {
-		const store = this.stores.get(name);
-		if (!store.db || !store.db.objectStoreNames.contains('cache')) return;
-
-		const tx = store.db.transaction(['cache'], 'readwrite');
-		const objectStore = tx.objectStore('cache');
-		await objectStore.put(data);
-	}
-
-	generateCacheKey(filters) {
-		const normalized = Object.keys(filters)
-			.sort()
-			.reduce((acc, key) => {
-				acc[key] = filters[key];
-				return acc;
-			}, {});
-		return JSON.stringify(normalized);
-	}
-
-	isCacheValid(entry, ttl) {
-		if (!entry || !entry.timestamp) return false;
-		const age = Date.now() - entry.timestamp;
-		return age < ttl;
-	}
+	/***********************************************************************
+	 * UTILITIES
+	 ***********************************************************************/
 
 	getItemKey(item, keyPath) {
 		if (typeof keyPath === 'function') {
diff --git a/assets/js/concise/DataStoreOld.js b/assets/js/concise/DataStoreOld.js
new file mode 100644
index 0000000..4aa4119
--- /dev/null
+++ b/assets/js/concise/DataStoreOld.js
@@ -0,0 +1,1099 @@
+/**
+ * DataStore - Singleton pattern managing multiple store namespaces
+ *
+ * Usage:
+ *   window.jvbStore = new DataStore();
+ *   this.store = window.jvbStore.register('feed', { config });
+ */
+class DataStoreOld {
+	constructor() {
+		// Singleton pattern
+		if (DataStore.instance) {
+			return DataStore.instance;
+		}
+		DataStore.instance = this;
+
+		// Shared resources
+		this.dbConfig = new Map();		// Definitions for the databases
+		this.databases = new Map();     // Shared IndexedDB connections
+		this.stores = new Map();        // Registered store namespaces
+		this.subscribers = new Map();   // Per-store event subscribers
+		this.pendingInits = new Map();  // Track initialization promises
+		this.fetchQueue = [];
+
+		// Global state
+		this._initialized = false;
+		this.body = document.body;
+		this.loading = document.querySelector('dialog.loading');
+
+		this.init();
+
+		// window.addEventListener('beforeunload', () => this.destroy());
+	}
+
+	async init() {
+		if (this._initialized) return;
+		this._initialized = true;
+
+		if (!('indexedDB' in window)) {
+			console.warn('IndexedDB not supported');
+		}
+	}
+
+	/**
+	 * Register a new store namespace
+	 * @param {string} name Database Name
+	 * @param {object|array} configs An object defining the store, or an array of objects defining the stores
+	 * @param {number} version the database version
+	 */
+	register(name, configs = [], version = 1.1) {
+		if (!Array.isArray(configs)) configs = [configs];
+		if (configs.length === 0) return;
+
+		if (!this.dbConfig.has(name)) {
+			this.dbConfig.set(name, {
+				dbName: `jvb_${name}`,
+				version: version,
+				stores: {},
+				_initialized: false
+			});
+		}
+
+		let dbEntry = this.dbConfig.get(name);
+
+		configs.forEach(config => {
+			if (!config.storeName) {
+				throw new Error(`Store config for "${name}" missing storeName`);
+			}
+			if (!config.keyPath) {
+				throw new Error(`Store "${config.storeName}" requires keyPath`);
+			}
+
+
+			const storeKey = `${name}_${config.storeName}`;
+
+			const store = {
+				config: {
+					// Storage
+					dbName: dbEntry.dbName,
+					storeName: 'items',
+					keyPath: 'id',
+					indexes: [],
+
+					// API
+					endpoint: null,
+					apiBase: jvbSettings.api,
+					filters: {},
+					required: null,
+
+					// Cache
+					TTL: 3600000, // 1 hour
+					useHttpCaching: true,
+
+					// Behavior
+					showLoading: false,
+					delayFetch: true,
+					validateData: true, // Validate data is serializable
+					...config
+				},
+				dbKey: name,
+				storeKey: storeKey,
+				data: new Map(),
+				cache: new Map(),
+				httpHeaders: new Map(),
+				subscribers: new Map(),
+				filters: {...(config.filters || {}) },
+				isFetching: false,
+				currentRequest: null,
+				lastResponse: null,
+				_initialized: false
+			};
+
+			store.config.headers = {
+				'X-WP-Nonce': window.auth.getNonce(),
+				...store.config.headers
+			};
+
+			dbEntry.stores[config.storeName] = storeKey;
+
+			this.stores.set(storeKey, store);
+			if (!this.subscribers.has(storeKey)) {
+				this.subscribers.set(storeKey, new Set());
+			}
+		});
+
+
+		// Initialize database asynchronously
+		this.initDB(name).catch(error => {
+			console.error(`Failed to initialize store "${name}":`, error);
+		});
+
+		const apis = {};
+		for (const [storeName, storeKey] of Object.entries(dbEntry.stores)) {
+			apis[storeName] = this.getStoreAPI(storeKey);
+		}
+		return apis;
+	}
+
+	/**
+	 * Get the API object for a registered store
+	 */
+	getStoreAPI(name) {
+		const api = {
+			// Data methods
+			fetch: () => this.fetch(name),
+			save: (item) => this.save(name, item),
+			delete: (id) => this.delete(name, id),
+			get: (id) => this.get(name, id),
+			getAll: () => this.getAll(name),
+			getFiltered: () => this.getFiltered(name),
+			clear: () => this.clear(name),
+
+			// Filter methods
+			setFilter: (key, value) => this.setFilter(name, key, value),
+			setFilters: (filters) => this.setFilters(name, filters),
+			removeFilter: (key) => this.removeFilter(name, key),
+			clearFilters: () => this.clearFilters(name),
+
+			// Cache methods
+			clearCache: () => this.clearCache(name),
+			clearHttpHeaders: (key) => this.clearHttpHeaders(name, key),
+
+			// Event methods
+			subscribe: (callback) => this.subscribe(name, callback),
+
+			// Utility
+			ensureInitialized: () => this.ensureStoreInitialized(name),
+
+			// Exposed properties (read-only)
+			get filters() {
+				return { ...api.getStore().filters };
+			},
+			get lastResponse() {
+				return api.getStore().lastResponse;
+			},
+			get data() {
+				return api.getStore().data;
+			},
+
+			getStore: () => this.stores.get(name)
+		};
+
+		return api;
+	}
+
+	/**
+	 * Convert FormData to plain object for storage
+	 */
+	formDataToObject(formData) {
+		const obj = {
+			_isFormData: true,
+			entries: {}
+		};
+
+		for (const [key, value] of formData.entries()) {
+			// Skip File/Blob objects - they're stored separately in UploadManager
+			if (value instanceof File || value instanceof Blob) {
+				continue;
+			}
+
+			// Handle multiple values for same key
+			if (obj.entries[key]) {
+				if (!Array.isArray(obj.entries[key])) {
+					obj.entries[key] = [obj.entries[key]];
+				}
+				obj.entries[key].push(value);
+			} else {
+				obj.entries[key] = value;
+			}
+		}
+
+		return obj;
+	}
+
+	/**
+	 * Convert stored object back to FormData
+	 */
+	async objectToFormData(obj) {
+		if (!obj._isFormData) return obj;
+
+		const formData = new FormData();
+
+		// Restore text entries
+		for (const [key, value] of Object.entries(obj.entries)) {
+			if (Array.isArray(value)) {
+				value.forEach(v => formData.append(key, v));
+			} else {
+				formData.append(key, value);
+			}
+		}
+
+		if (window.jvbUploads && obj.entries.upload_ids) {
+			const uploadIds = JSON.parse(obj.entries.upload_ids);
+
+			for (const uploadId of uploadIds) {
+				const file = await window.jvbUploads.getBlobData(uploadId);
+				if (file) {
+					formData.append('files[]', file);
+				}
+			}
+		}
+
+		return formData;
+	}
+
+	/**
+	 * Initialize database for a specific store
+	 */
+	async initDB(name) {
+		const db = this.dbConfig.get(name);
+		if (!db || db._initialized) return;
+
+		if (this.pendingInits.has(name)) {
+			return this.pendingInits.get(name);
+		}
+
+		const initPromise = this._performDBInit(name);
+		this.pendingInits.set(name, initPromise);
+
+		try {
+			await initPromise;
+			db._initialized = true;
+		} finally {
+			this.pendingInits.delete(name);
+		}
+	}
+
+	async _performDBInit(name) {
+		const database = this.dbConfig.get(name);
+		const { dbName, version } = database;
+		const stores = Object.values(database.stores);
+
+		try {
+			if (!this.databases.has(dbName)) {
+				const db = await this.openDatabase(dbName, version, (db) => {
+					stores.forEach(store => {
+						let storeObj = this.stores.get(store);
+						if (storeObj) {
+							this.setupStores(db, storeObj.config);
+						}
+					});
+				});
+				this.databases.set(dbName, db);
+			}
+
+			stores.forEach(storeName => {
+				let store = this.stores.get(storeName);
+				if (store) {
+					store.db = this.databases.get(dbName);
+					store._initialized = true;
+					this.loadStoreDataInBackground(storeName);
+					this.notify(storeName, 'db-init');
+				}
+			})
+
+		} catch (error) {
+			console.error(`Failed to initialize database for store "${name}":`, error);
+			throw error;
+		}
+	}
+
+	openDatabase(dbName, version, onUpgrade) {
+		return new Promise((resolve, reject) => {
+			const request = indexedDB.open(dbName, version);
+
+			request.onupgradeneeded = (e) => {
+				if (onUpgrade) {
+					onUpgrade(e.target.result, e.oldVersion, e.newVersion);
+				}
+			};
+
+			request.onsuccess = (e) => resolve(e.target.result);
+			request.onerror = (e) => reject(e.target.error);
+			request.onblocked = () => {
+				console.warn(`Database ${dbName} blocked. Close other tabs.`);
+			};
+		});
+	}
+
+	setupStores(db, config) {
+		// Main store
+		if (!db.objectStoreNames.contains(config.storeName)) {
+			const store = db.createObjectStore(config.storeName, {
+				keyPath: config.keyPath
+			});
+
+			config.indexes.forEach(index => {
+				store.createIndex(
+					index.name,
+					index.keyPath || index.name,
+					{ unique: index.unique || false }
+				);
+			});
+		}
+
+		// Cache store
+		if (config.endpoint && !db.objectStoreNames.contains('cache')) {
+			const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
+			cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
+		}
+
+		// HTTP headers store
+		if (config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
+			db.createObjectStore('headers', { keyPath: 'key' });
+		}
+	}
+
+	loadStoreDataInBackground(name) {
+		const store = this.stores.get(name);
+		if (!store?.db) return;
+
+		const tasks = [
+			this.loadStoreData(name),
+			this.loadStoreCache(name),
+			this.loadStoreHeaders(name)
+		];
+
+		Promise.all(tasks)
+			.then(() => {
+				this.notify(name, 'data-ready');
+
+				// Add to fetch queue instead of immediate fetch
+				if (store.config.endpoint && store.config.delayFetch) {
+					this.fetchQueue.push(name);
+
+					// Start processing queue if not already running
+					if (this.fetchQueue.length === 1) {
+						this.processFetchQueue();
+					}
+				} else if (store.config.endpoint && !store.config.delayFetch) {
+					// Immediate fetch
+					if ('requestIdleCallback' in window) {
+						requestIdleCallback(() => this.fetch(name), { timeout: 2000 });
+					} else {
+						setTimeout(() => this.fetch(name), 100);
+					}
+				}
+			})
+			.catch(error => {
+				console.error(`Background load error for store "${name}":`, error);
+			});
+	}
+
+	async processFetchQueue() {
+		if (this.fetchQueue.length === 0) return;
+
+		const name = this.fetchQueue.shift();
+		const store = this.stores.get(name);
+
+		if (!store) {
+			// Store was removed, continue with next
+			return this.processFetchQueue();
+		}
+
+		try {
+			await this.fetch(name);
+		} catch (error) {
+			console.error(`Queue fetch error for "${name}":`, error);
+		}
+
+		// Process next item with idle callback
+		if (this.fetchQueue.length > 0) {
+			if ('requestIdleCallback' in window) {
+				requestIdleCallback(() => this.processFetchQueue(), { timeout: 2000 });
+			} else {
+				setTimeout(() => this.processFetchQueue(), 50);
+			}
+		}
+	}
+
+	async loadStoreData(name) {
+		const store = this.stores.get(name);
+		if (!store?.db) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction([store.config.storeName], 'readonly');
+			const objectStore = tx.objectStore(store.config.storeName);
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				const items = e.target.result || [];
+				items.forEach(item => {
+					const key = this.getItemKey(item, store.config.keyPath);
+					store.data.set(key, item);
+				});
+				this.notify(name, 'data-loaded', { count: items.length });
+				resolve(items);
+			};
+
+			request.onerror = () => resolve([]);
+		});
+	}
+
+	async loadStoreCache(name) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains('cache')) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction(['cache'], 'readonly');
+			const objectStore = tx.objectStore('cache');
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				(e.target.result || []).forEach(item => {
+					if (this.isCacheValid(item, store.config.TTL)) {
+						store.cache.set(item.key, item);
+					}
+				});
+				resolve();
+			};
+
+			request.onerror = () => resolve();
+		});
+	}
+
+	async loadStoreHeaders(name) {
+		const store = this.stores.get(name);
+		if (!store?.db || !store.db.objectStoreNames.contains('headers')) return;
+
+		return new Promise((resolve) => {
+			const tx = store.db.transaction(['headers'], 'readonly');
+			const objectStore = tx.objectStore('headers');
+			const request = objectStore.getAll();
+
+			request.onsuccess = (e) => {
+				(e.target.result || []).forEach(header => {
+					store.httpHeaders.set(header.key, header);
+				});
+				resolve();
+			};
+
+			request.onerror = () => resolve();
+		});
+	}
+
+	async ensureStoreInitialized(name) {
+		const store = this.stores.get(name);
+		if (!store) {
+			throw new Error(`Store "${name}" not registered`);
+		}
+
+		if (!store._initialized) {
+			await this.initDB(store.dbKey);
+		}
+	}
+
+	async fetch(name) {
+		await this.ensureStoreInitialized(name);
+
+		const store = this.stores.get(name);
+
+		if (store.isFetching) return;
+
+		// Check required filters
+		if (store.config.required) {
+			const required = Array.isArray(store.config.required)
+				? store.config.required
+				: [store.config.required];
+
+			const missing = required.some(key =>
+				!store.filters[key] || store.filters[key] === ''
+			);
+
+			if (missing) return;
+		}
+
+		store.isFetching = true;
+
+		try {
+			// Check cache
+			const cacheKey = this.generateCacheKey(store.filters);
+			const cached = store.cache.get(cacheKey);
+
+			if (cached && this.isCacheValid(cached, store.config.TTL)) {
+				this.notify(name, 'data-loaded', {
+					cached: true,
+					items: cached.items || []
+				});
+				return cached;
+			}
+
+			if (store.config.showLoading) {
+				this.setLoading(true);
+			}
+
+			const url = this.buildFetchUrl(name);
+			const headers = { ...store.config.headers };
+			const cachedHeaders = store.httpHeaders.get(cacheKey);
+
+			if (store.config.useHttpCaching && cachedHeaders) {
+				if (cachedHeaders.etag) {
+					headers['If-None-Match'] = cachedHeaders.etag;
+				}
+				if (cachedHeaders.lastModified) {
+					headers['If-Modified-Since'] = cachedHeaders.lastModified;
+				}
+			}
+
+			const controller = new AbortController();
+			store.currentRequest = controller;
+
+			const response = await fetch(url, {
+				method: 'GET',
+				headers,
+				signal: controller.signal
+			});
+
+			if (response.status === 304) {
+				// 304 means "Not Modified" - use cached data if available
+				if (cached) {
+					this.notify(name, 'data-loaded', {
+						cached: true,
+						notModified: true,
+						items: cached.items || []
+					});
+					return cached;
+				}
+
+				// No cached data but server says not modified - return empty result
+				// This can happen on first load when cache headers exist but data doesn't
+				this.notify(name, 'data-loaded', {
+					cached: false,
+					notModified: true,
+					items: []
+				});
+
+				// Initialize empty lastResponse
+				store.lastResponse = {
+					has_more: false,
+					total: 0,
+					pages: 1,
+					queue_stats: {}
+				};
+
+				return { items: [] };
+			}
+
+			// Now check for other non-OK responses
+			if (!response.ok) {
+				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+			}
+
+			const data = await response.json();
+
+			if (store.config.useHttpCaching) {
+				this.storeResponseHeaders(name, cacheKey, response);
+			}
+			await this.processFetchedData(name, data, cacheKey);
+
+			this.notify(name, 'data-loaded', {
+				cached: false,
+				items: data.items || []
+			});
+
+			return data;
+
+		} catch (error) {
+			if (error.name !== 'AbortError') {
+				console.error(`Fetch error for store "${name}":`, error);
+				this.notify(name, 'fetch-error', { error });
+			}
+			throw error;
+
+		} finally {
+			store.isFetching = false;
+			store.currentRequest = null;
+
+			if (store.config.showLoading) {
+				this.setLoading(false);
+			}
+		}
+	}
+
+	buildFetchUrl(name) {
+		const store = this.stores.get(name);
+		const params = new URLSearchParams();
+
+		Object.entries(store.filters).forEach(([key, value]) => {
+			if (value !== null && value !== undefined && value !== '') {
+				if (typeof value === 'object') {
+					params.set(key, JSON.stringify(value));
+				} else {
+					params.set(key, value);
+				}
+			}
+		});
+
+		const baseUrl = store.config.apiBase + store.config.endpoint;
+		return params.toString() ? `${baseUrl}?${params}` : baseUrl;
+	}
+
+	/**
+	 * Process fetched data (batch from server)
+	 */
+	async processFetchedData(name, data, cacheKey) {
+		const store = this.stores.get(name);
+		const items = data.items || [];
+		const changes = []; // Track all changes
+
+		// Batch process with single transaction
+		if (store.db && items.length > 0) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+
+			for (const item of items) {
+				try {
+					// Use shared save logic
+					const changeInfo = await this._saveItem(name, item, false);
+					changes.push(changeInfo);
+
+					// Queue for batch write
+					await objectStore.put(changeInfo.processed);
+				} catch (error) {
+					console.error(`Error processing item:`, error);
+				}
+			}
+
+			// Wait for transaction to complete
+			await new Promise((resolve, reject) => {
+				tx.oncomplete = () => resolve();
+				tx.onerror = () => reject(tx.error);
+			});
+		}
+
+		// Update cache
+		const cacheEntry = {
+			key: cacheKey,
+			items: items.map(item => this.getItemKey(item, store.config.keyPath)),
+			timestamp: Date.now(),
+			endpoint: store.config.endpoint,
+			filters: { ...store.filters }
+		};
+
+		store.cache.set(cacheKey, cacheEntry);
+		await this.saveToCache(name, cacheKey, cacheEntry);
+
+		// Update lastResponse metadata
+		store.lastResponse = {
+			...data,
+			has_more: data.has_more || false,
+			total: data.total || items.length,
+			pages: data.pages || 1,
+			queue_stats: data.queue_stats || {}
+		};
+
+		// Emit events for items with status changes
+		changes.forEach(changeInfo => {
+			if (changeInfo.statusChanged) {
+				this.notify(name, 'item-saved', {
+					item: changeInfo.item,
+					key: changeInfo.key,
+					previousItem: changeInfo.previousItem
+				});
+			}
+		});
+	}
+
+
+	/**
+	 * Internal method: Save a single item with full tracking
+	 */
+	async _saveItem(name, item, emitEvent = true) {
+		const store = this.stores.get(name);
+
+		const result = this.processForStorage(item, store.config.validateData);
+		if (!result.valid) {
+			throw new Error(`Non-serializable data: ${result.error}`);
+		}
+		const processed = result.data;
+
+		const key = this.getItemKey(processed, store.config.keyPath);
+
+		// Capture previous state
+		const previousItem = store.data.get(key);
+
+		// Update in-memory store (with original data intact)
+		store.data.set(key, item);
+
+		// Write to IndexedDB happens in batch transaction (if provided)
+		// or individual transaction (if not)
+
+		// Return change info for event emission
+		return {
+			item,
+			previousItem,
+			key,
+			processed,
+			statusChanged: previousItem && previousItem.status !== item.status
+		};
+	}
+
+	/**
+	 * Save single item (public API)
+	 */
+	async save(name, item) {
+		const store = this.stores.get(name);
+		const changeInfo = await this._saveItem(name, item);
+
+		// Write to IndexedDB immediately for single saves
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.put(changeInfo.processed);
+		}
+
+		// Always emit for explicit saves
+		this.notify(name, 'item-saved', {
+			item: changeInfo.item,
+			key: changeInfo.key,
+			previousItem: changeInfo.previousItem
+		});
+
+		return changeInfo.key;
+	}
+
+	processForStorage(obj, validate = true, path = 'root') {
+		if (obj === null || obj === undefined) return { valid: true, data: obj };
+
+		const type = typeof obj;
+
+		// Handle primitives
+		if (['string', 'number', 'boolean'].includes(type)) {
+			return { valid: true, data: obj };
+		}
+
+		// Reject functions
+		if (type === 'function') {
+			return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
+		}
+
+		// DOM elements
+		if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
+			return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
+		}
+
+		// FormData - convert and continue
+		if (obj instanceof FormData) {
+			return validate
+				? { valid: false, error: `FormData at ${path}` }
+				: { valid: true, data: this.formDataToObject(obj) };
+		}
+
+		// Preserve safe types
+		if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+			return { valid: true, data: obj };
+		}
+
+		// Convert Sets to Arrays
+		if (obj instanceof Set) {
+			const arr = Array.from(obj);
+			return this.processForStorage(arr, validate, path);
+		}
+
+		// Convert Maps to Objects
+		if (obj instanceof Map) {
+			obj = Object.fromEntries(obj);
+		}
+
+		// Arrays
+		if (Array.isArray(obj)) {
+			const processed = [];
+			for (let i = 0; i < obj.length; i++) {
+				const result = this.processForStorage(obj[i], validate, `${path}[${i}]`);
+				if (!result.valid) return result;
+				if (result.data !== null) processed.push(result.data);
+			}
+			return { valid: true, data: processed };
+		}
+
+		// Objects
+		if (type === 'object') {
+			const processed = {};
+			for (const [key, value] of Object.entries(obj)) {
+				const result = this.processForStorage(value, validate, `${path}.${key}`);
+				if (!result.valid) return result;
+				if (result.data !== null) processed[key] = result.data;
+			}
+			return { valid: true, data: processed };
+		}
+
+		return validate
+			? { valid: false, error: `Unknown type at ${path}` }
+			: { valid: true, data: null };
+	}
+
+	async delete(name, id) {
+		const store = this.stores.get(name);
+		store.data.delete(id);
+
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.delete(id);
+		}
+
+		this.notify(name, 'item-deleted', { id });
+	}
+
+	get(name, id) {
+		const store = this.stores.get(name);
+		return store.data.get(id);
+	}
+
+	getAll(name) {
+		const store = this.stores.get(name);
+		return Array.from(store.data.values());
+	}
+
+	getFiltered(name) {
+		const store = this.stores.get(name);
+		const cacheKey = this.generateCacheKey(store.filters);
+		const cacheEntry = store.cache.get(cacheKey);
+
+		if (cacheEntry && cacheEntry.items) {
+			return cacheEntry.items.reduce((acc, id) => {
+				const item = store.data.get(id);
+				if (item) acc.push(item);
+				return acc;
+			}, []);
+		}
+
+		return this.getAll(name);
+	}
+
+	async clear(name) {
+		const store = this.stores.get(name);
+		store.data.clear();
+		store.cache.clear();
+
+		if (store.db) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+			await objectStore.clear();
+		}
+
+		this.notify(name, 'data-cleared');
+	}
+
+	setFilter(name, key, value) {
+		const store = this.stores.get(name);
+		const oldValue = store.filters[key];
+
+		if (value === null || value === undefined || value === '') {
+			delete store.filters[key];
+		} else {
+			store.filters[key] = value;
+		}
+		this.notify(name, 'filters-changed', {
+			filters: store.filters,
+			changed: { key, oldValue, newValue: value }
+		});
+
+		if (store.config.endpoint) {
+			this.fetch(name);
+		}
+	}
+
+	async setFilters(name, filters) {
+		const store = this.stores.get(name);
+
+		const hasChanges = Object.keys(filters).some(
+			key => store.filters[key] !== filters[key]
+		);
+
+		if (!hasChanges) return;
+
+		store.filters = { ...store.filters, ...filters };
+
+		this.notify(name, 'filters-changed', {
+			filters: store.filters,
+			changed: filters
+		});
+
+		if (store.config.endpoint) {
+			await this.fetch(name);
+		}
+	}
+
+	removeFilter(name, key) {
+		const store = this.stores.get(name);
+		const oldValue = store.filters[key];
+
+		if (oldValue !== undefined) {
+			delete store.filters[key];
+
+			this.notify(name, 'filters-changed', {
+				filters: store.filters,
+				removed: { key, oldValue }
+			});
+
+			if (store.config.endpoint) {
+				this.fetch(name);
+			}
+		}
+	}
+
+	clearFilters(name) {
+		const store = this.stores.get(name);
+		const oldFilters = { ...store.filters };
+
+		store.filters = { ...store.config.filters };
+
+		this.notify(name, 'filters-cleared', {
+			oldFilters,
+			filters: store.filters
+		});
+
+		if (store.config.endpoint) {
+			this.fetch(name);
+		}
+	}
+
+	clearCache(name) {
+		const store = this.stores.get(name);
+		store.cache.clear();
+
+		if (store.db && store.db.objectStoreNames.contains('cache')) {
+			const tx = store.db.transaction(['cache'], 'readwrite');
+			const objectStore = tx.objectStore('cache');
+			objectStore.clear();
+		}
+
+		this.notify(name, 'cache-cleared');
+	}
+
+	clearHttpHeaders(name, cacheKey = null) {
+		const store = this.stores.get(name);
+
+		if (cacheKey) {
+			store.httpHeaders.delete(cacheKey);
+
+			if (store.db && store.db.objectStoreNames.contains('headers')) {
+				const tx = store.db.transaction(['headers'], 'readwrite');
+				const objectStore = tx.objectStore('headers');
+				objectStore.delete(cacheKey);
+			}
+		} else {
+			store.httpHeaders.clear();
+
+			if (store.db && store.db.objectStoreNames.contains('headers')) {
+				const tx = store.db.transaction(['headers'], 'readwrite');
+				const objectStore = tx.objectStore('headers');
+				objectStore.clear();
+			}
+		}
+	}
+
+	subscribe(name, callback) {
+		if (!this.subscribers.has(name)) {
+			this.subscribers.set(name, new Set());
+		}
+		const subscribers = this.subscribers.get(name);
+		subscribers.add(callback);
+		return () => subscribers.delete(callback);
+	}
+
+	notify(name, event, data = {}) {
+		const subscribers = this.subscribers.get(name);
+		if (!subscribers) return;
+
+		subscribers.forEach(callback => {
+			try {
+				callback(event, data);
+			} catch (error) {
+				console.error(`Subscriber error for store "${name}":`, error);
+			}
+		});
+	}
+
+	storeResponseHeaders(name, key, response) {
+		const store = this.stores.get(name);
+
+		const headers = {
+			key,
+			etag: response.headers.get('ETag'),
+			lastModified: response.headers.get('Last-Modified'),
+			timestamp: Date.now()
+		};
+
+		store.httpHeaders.set(key, headers);
+
+		if (store.db && store.db.objectStoreNames.contains('headers')) {
+			const tx = store.db.transaction(['headers'], 'readwrite');
+			const objectStore = tx.objectStore('headers');
+			objectStore.put(headers);
+		}
+	}
+
+	async saveToCache(name, key, data) {
+		const store = this.stores.get(name);
+		if (!store.db || !store.db.objectStoreNames.contains('cache')) return;
+
+		const tx = store.db.transaction(['cache'], 'readwrite');
+		const objectStore = tx.objectStore('cache');
+		await objectStore.put(data);
+	}
+
+	generateCacheKey(filters) {
+		const normalized = Object.keys(filters)
+			.sort()
+			.reduce((acc, key) => {
+				acc[key] = filters[key];
+				return acc;
+			}, {});
+		return JSON.stringify(normalized);
+	}
+
+	isCacheValid(entry, ttl) {
+		if (!entry || !entry.timestamp) return false;
+		const age = Date.now() - entry.timestamp;
+		return age < ttl;
+	}
+
+	getItemKey(item, keyPath) {
+		if (typeof keyPath === 'function') {
+			return keyPath(item);
+		}
+
+		const keys = keyPath.split('.');
+		let value = item;
+
+		for (const key of keys) {
+			value = value?.[key];
+		}
+
+		return value;
+	}
+
+	setLoading(on) {
+		this.body.classList.toggle('loading', on);
+		if (on) {
+			this.loading?.showModal();
+		} else {
+			this.loading?.close();
+		}
+	}
+
+	destroy() {
+		this.stores.forEach(store => {
+			if (store.currentRequest) {
+				store.currentRequest.abort();
+			}
+		});
+
+		this.databases.forEach(db => db.close());
+		this.stores.clear();
+		this.subscribers.clear();
+		this.databases.clear();
+		this.pendingInits.clear();
+	}
+}
+
+// Initialize singleton on DOMContentLoaded
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbStore = new DataStore();
+		}
+	});
+});
diff --git a/assets/js/concise/ErrorHandler.js b/assets/js/concise/ErrorHandler.js
index 029b37b..bf98e9e 100644
--- a/assets/js/concise/ErrorHandler.js
+++ b/assets/js/concise/ErrorHandler.js
@@ -272,11 +272,6 @@
     async retryWithBackoff(callback) {
         const backoffTime = Math.min(1000 * Math.pow(2, this.retryCount), 10000);
 
-        // Display retry notification
-        if (this.options.displayNotifications) {
-            this.displayRetryNotification(backoffTime);
-        }
-
         // Wait before retrying
         await new Promise(resolve => setTimeout(resolve, backoffTime));
 
@@ -284,20 +279,6 @@
         return callback();
     }
 
-    /**
-     * Display retry notification
-     */
-    displayRetryNotification(backoffTime) {
-        if (window.jvbNotifications) {
-            window.jvbNotifications.queuePopupNotification({
-                type: 'info',
-                message: `Retrying in ${backoffTime/1000} seconds...`,
-                icon: 'refresh',
-                priority: 'medium',
-                displayDuration: backoffTime
-            });
-        }
-    }
 
     /**
      * Reset retry counter
@@ -305,74 +286,6 @@
     resetRetryCount() {
         this.retryCount = 0;
     }
-
-    /**
-     * Handle user feedback for errors
-     */
-    collectUserFeedback(errorInfo) {
-        // Create a modal for collecting feedback
-        const modal = document.createElement('dialog');
-        modal.className = 'error-feedback-modal';
-
-        modal.innerHTML = `
-            <h2>Help Us Improve</h2>
-            <p>We encountered an error. Would you like to tell us what happened?</p>
-            <form method="dialog" data-save="error">
-                <textarea placeholder="What were you trying to do when this error occurred?"></textarea>
-                <div class="actions">
-                    <button value="cancel">Skip</button>
-                    <button value="submit" class="primary">Send Feedback</button>
-                </div>
-            </form>
-        `;
-
-        document.body.appendChild(modal);
-
-        return new Promise((resolve) => {
-            modal.addEventListener('close', () => {
-                const feedback = modal.returnValue === 'submit'
-                    ? modal.querySelector('textarea').value
-                    : null;
-
-                document.body.removeChild(modal);
-                resolve(feedback);
-            });
-
-            modal.showModal();
-        });
-    }
-
-    /**
-     * Handle global errors
-     */
-    setupGlobalErrorHandling() {
-        // Handle uncaught errors
-        window.addEventListener('error', event => {
-            this.log(
-                event.error || new Error(event.message),
-                {
-                    message: event.message,
-                    filename: event.filename,
-                    lineno: event.lineno,
-                    colno: event.colno,
-                    type: 'global_error'
-                }
-            );
-
-            // Don't prevent default - let browser show its own error if needed
-        });
-
-        // Handle unhandled promise rejections
-        window.addEventListener('unhandledrejection', event => {
-            this.log(
-                event.reason,
-                {
-                    type: 'unhandled_promise',
-                    message: event.reason?.message || 'Unhandled promise rejection'
-                }
-            );
-        });
-    }
 }
 document.addEventListener('DOMContentLoaded', async function () {
 	window.auth.subscribe((event) => {
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 38c1c22..8e5fbdc 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -289,6 +289,7 @@
 			status: '',
 			options: {
 				autosave: 'autosave' in formElement.dataset,
+				autoUpload: true,
 				saveDelay: this.autoSaveDefaults.delay,
 				endpoint: formElement.dataset.save ?? '',
 				formStatus: true,
@@ -334,7 +335,7 @@
 		this.initCharacterLimits(form);
 
 		// Initialize image upload fields
-		this.initImageUploadFields(form);
+		this.initImageUploadFields(form, formConfig);
 
 		// Initialize tabs if present
 		if (window.jvbTabs && form.querySelector('nav.tabs')) {
@@ -958,8 +959,8 @@
 	/**
 	 * Initialize image upload fields
 	 */
-	initImageUploadFields(form) {
-		window.jvbUploads.scanFields(form);
+	initImageUploadFields(form, config) {
+		window.jvbUploads.scanFields(form, config.options.autoUpload);
 	}
 
 	/* ========== Event Handlers ========== */
@@ -981,14 +982,7 @@
 				fullData: formData,
 				config: formConfig
 			});
-
-			// Don't delete yet - wait for success/error from subscriber
-			return;
 		}
-
-		// For forms that submit normally (not prevented)
-		// We can clean up the cache on successful submission
-		// This would typically be called from handleFormSuccess
 	}
 
 	handleFormSuccess(form, data) {
@@ -1320,7 +1314,7 @@
 				message: 'Please enter a valid URL starting with https://'
 			},
 			phone: {
-				pattern: /^[\d\s\-\+\(\)\.]+$/,
+				pattern: /^[\d\s\-+().]+$/,
 				message: 'Please enter a valid phone number'
 			},
 			number: {
@@ -1543,145 +1537,6 @@
 		}
 	}
 
-	/**
-	 * 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
@@ -1875,7 +1730,7 @@
 
 	/* ========== Form Data Methods ========== */
 
-	collectFormData(form, isInit = false) {
+	collectFormData(form) {
 		if (Object.hasOwn(form.dataset, 'timeline')) {
 			return this.collectTimeline(form);
 		}
@@ -1912,7 +1767,7 @@
 			if (this.ignore.includes(key) || key.endsWith('_temp')) {
 				continue;
 			}
-			const match = key.match(/^\[(\d+)\](.+)$/);
+			const match = key.match(/^\[(\d+)](.+)$/);
 			if (match) {
 				// Timeline-specific field: [postId]fieldName
 				const [, postId, fieldName] = match;
@@ -1950,7 +1805,7 @@
 	getFieldProcessor(key) {
 		if (key.includes('::')) return this.processGroupField;
 		if (key.includes(':')) return this.processRepeaterField;
-		if (/\[[^\]]+\]/.test(key)) return this.processLocationField;
+		if (/\[[^\]]+]/.test(key)) return this.processLocationField;
 		return this.processRegularField;
 	}
 
@@ -2122,7 +1977,9 @@
 			let p = field.querySelector('p');
 
 			title.textContent = fieldInfo.label;
-			let formatted = this.formatFieldValue(value, fieldInfo.type);
+
+
+			let formatted = this.formatFieldValue(value, fieldInfo.type, form);
 			if (this.isHtmlContent(formatted)) {
 				p.innerHTML = formatted;
 			} else {
@@ -2131,6 +1988,27 @@
 
 			summary.append(field);
 		}
+		let uploads = form.querySelectorAll('[data-upload-field]');
+		if (uploads) {
+			uploads.forEach(upload => {
+				let label = upload.querySelector('h2').textContent;
+
+				let imgs = upload.querySelectorAll('.item-grid.preview img');
+				if (imgs) {
+					let field = wrapper.cloneNode(true);
+					let title = field.querySelector('h3');
+					let p = field.querySelector('p');
+					p.remove();
+
+					title.textContent = label;
+					imgs.forEach(img => {
+						img = img.cloneNode(true);
+						field.append(img);
+					});
+					summary.append(field);
+				}
+			});
+		}
 
 		// Remove template
 		wrapper.remove();
@@ -2152,10 +2030,8 @@
 		if (Array.isArray(value) && value.length === 0) {
 			return true;
 		}
-		if (typeof value === 'object' && Object.keys(value).length === 0) {
-			return true;
-		}
-		return false;
+		return typeof value === 'object' && Object.keys(value).length === 0;
+
 	}
 
 	/**
@@ -2166,8 +2042,6 @@
 		// Try to find label by 'for' attribute (exact match)
 		let label = form.querySelector(`label[for="${fieldName}"]`);
 		let input = form.querySelector(`[name=${fieldName}]`);
-		let fieldWrapper = input?.closest('.field');
-
 		// Try to find the input field - check multiple patterns
 		if (!input) {
 			// Try exact match first
@@ -2193,12 +2067,12 @@
 			// Try closest field wrapper first
 			const field = input.closest('.field, fieldset');
 			if (field) {
-				label = field.querySelector('label, legend');
+				label = field.querySelector('label, legend, h2');
 			}
 		}
 
 		// Get field wrapper - always use base name (no special characters)
-		fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
+		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"], fieldset[data-field="${fieldName}"]`);
 
 		// Determine field type
 		let fieldType = 'text';
@@ -2258,7 +2132,7 @@
 				if (Array.isArray(value)) {
 					return this.formatArrayValue(value);
 				}
-				return (value === '1' || value === 1 || value === true) ? 'Yes' : 'No';
+				return (value === '1' || value === 1 || value === true) ? 'Yes' : value;
 
 			case 'select':
 				// Handle both single and multi-select
@@ -2285,8 +2159,7 @@
 			case 'location':
 				return this.formatLocationValue(value);
 
-			case 'file':
-			case 'image':
+			case 'upload':
 				return this.formatFileValue(value);
 
 			case 'number':
@@ -2529,13 +2402,6 @@
 	}
 
 	/**
-	 * Convert newlines to <br> tags (kept for backwards compatibility)
-	 */
-	nl2br(text) {
-		return this.formatPlainText(text);
-	}
-
-	/**
 	 * Event system
 	 */
 	subscribe(callback) {
@@ -2594,6 +2460,11 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbForm = FormController;
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			window.jvbForm = FormController;
+		}
+	});
+
 });
diff --git a/assets/js/concise/Gallery.js b/assets/js/concise/Gallery.js
index 9c3e0bd..4890b50 100644
--- a/assets/js/concise/Gallery.js
+++ b/assets/js/concise/Gallery.js
@@ -56,7 +56,7 @@
 				closeMessage: 'Closed Gallery',
 			}
 		);
-		this.modal.subscribe((event, data) => {
+		this.modal.subscribe((event) => {
 			if (event === 'modal-close') {
 				this.toggleGallery(false);
 			}
@@ -260,45 +260,6 @@
 		this.handleZoom(increment, e.clientX, e.clientY);
 	}
 
-	clampPan() {
-		const BORDER = 32; // 2rem
-		const wrap = this.ui.gallery.wrap;
-		if (!wrap) return;
-
-		const wrapRect = wrap.getBoundingClientRect();
-
-		// MUST use natural dimensions
-		const naturalW = 1920;
-		const naturalH = 1920;
-
-		// But they must be constrained by how the CSS fits them (max-width: 90vw, max-height: 85vh)
-		// So compute the base display size:
-		const displayRatio = Math.min(
-			wrapRect.width / naturalW,
-			wrapRect.height / naturalH
-		);
-
-		const baseWidth  = naturalW * displayRatio;
-		const baseHeight = naturalH * displayRatio;
-
-		const scaledWidth  = baseWidth  * this.zoom.scale;
-		const scaledHeight = baseHeight * this.zoom.scale;
-
-		// Allowed pan range
-		const minX = wrapRect.width - scaledWidth - BORDER;
-		const maxX = BORDER;
-
-		const minY = wrapRect.height - scaledHeight - BORDER;
-		const maxY = BORDER;
-
-		// clamp
-		this.zoom.x = Math.min(maxX, Math.max(minX, this.zoom.x));
-		this.zoom.y = Math.min(maxY, Math.max(minY, this.zoom.y));
-	}
-
-
-
-
 	handleZoom(increment, clientX=null, clientY=null) {
 		const oldScale = this.zoom.scale;
 		let newScale = oldScale + increment;
@@ -357,9 +318,8 @@
 	/**
 	 *
 	 * @param {boolean} open
-	 * @param {null|number} index
 	 */
-	toggleGallery(open, index= null) {
+	toggleGallery(open) {
 		if (open) {
 			// Disable native image dragging
 			this.ui.gallery.image.draggable = false;
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
index 52e233d..16ad1bf 100644
--- a/assets/js/concise/HandleSelection.js
+++ b/assets/js/concise/HandleSelection.js
@@ -294,13 +294,6 @@
 	}
 
 	/**
-	 * Get all selected item IDs as array
-	 */
-	getSelected() {
-		return Array.from(this.selectedItems);
-	}
-
-	/**
 	 * Check if an item is selected
 	 */
 	isSelected(id) {
diff --git a/assets/js/concise/Integrations.js b/assets/js/concise/Integrations.js
index 0beba0b..b4d5a7c 100644
--- a/assets/js/concise/Integrations.js
+++ b/assets/js/concise/Integrations.js
@@ -196,90 +196,6 @@
 		return this.forms.get(service)??false;
 	}
 
-	/**
-	 * Handle OAuth authorization link clicks
-	 * Opens OAuth in a popup window with proper handling
-	 */
-	handleOAuthClick(link) {
-		const service = link.dataset.service;
-		const href = link.href;
-
-		// Calculate center position for popup
-		const width = 600;
-		const height = 700;
-		const left = (screen.width - width) / 2;
-		const top = (screen.height - height) / 2;
-
-		// Show loading notification
-		this.showNotification('Opening authorization window...', 'info');
-
-		// Add loading state to the link
-		link.classList.add('loading');
-		link.setAttribute('aria-busy', 'true');
-
-		// Open OAuth in popup
-		const popup = window.open(
-			href,
-			'oauth_' + service,
-			`width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=yes,status=yes,resizable=yes`
-		);
-
-		if (!popup) {
-			// Popup was blocked
-			this.showNotification('Popup was blocked. Please allow popups and try again.', 'error');
-			link.classList.remove('loading');
-			link.removeAttribute('aria-busy');
-			return true; // Allow default behavior as fallback
-		}
-
-		// Focus the popup
-		popup.focus();
-
-		// Update notification
-		this.showNotification('Waiting for authorization...', 'info');
-
-		// Poll for popup close
-		const pollTimer = setInterval(() => {
-			try {
-				if (popup.closed) {
-					clearInterval(pollTimer);
-
-					// Remove loading state
-					link.classList.remove('loading');
-					link.removeAttribute('aria-busy');
-
-					// Show checking notification
-					this.showNotification('Checking authorization status...', 'info');
-
-					// Wait a moment for redirect to complete, then check for messages
-					setTimeout(() => {
-						this.checkForOAuthMessages();
-
-						// If no messages found, reload to get updated connection status
-						setTimeout(() => {
-							const urlParams = new URLSearchParams(window.location.search);
-							if (!urlParams.has('success') && !urlParams.has('error')) {
-								// No messages in URL, reload to check server-side status
-								window.location.reload();
-							}
-						}, 500);
-					}, 500);
-				}
-			} catch (error) {
-				// Ignore cross-origin errors during polling
-			}
-		}, 500);
-
-		// Safety timeout - stop polling after 5 minutes
-		setTimeout(() => {
-			clearInterval(pollTimer);
-			link.classList.remove('loading');
-			link.removeAttribute('aria-busy');
-		}, 300000);
-
-		return false; // Prevent default link behavior
-	}
-
 	async handleAction(input) {
 		const form = input.closest('form');
 		const service = form.dataset.service;
diff --git a/assets/js/concise/PopulateForm.js b/assets/js/concise/PopulateForm.js
index 1f4273d..8cef082 100644
--- a/assets/js/concise/PopulateForm.js
+++ b/assets/js/concise/PopulateForm.js
@@ -166,7 +166,6 @@
 			fieldWrapper.querySelector('textarea:not([data-editor="true"])');
 
 		if (textarea) {
-			const oldValue = textarea.value;
 			textarea.value = String(fieldValue || '');
 
 			// Trigger change event to update any dependencies
@@ -511,6 +510,7 @@
 	}
 
 	populateTimelineGallery(fieldWrapper, fieldName, fieldValue, imagesData) {
+		console.log('populating Timeline Gallery');
 		if (!fieldValue || typeof fieldValue !== 'object') {
 			return;
 		}
@@ -528,7 +528,7 @@
 		if (grid) {
 			window.removeChildren(grid);
 			console.log(imageIds);
-			for (let [index, data] of Object.entries(fieldValue)) {
+			for (let data of Object.entries(fieldValue)) {
 				let imageId = data['post_thumbnail'];
 				const template = window.getTemplate('timelineItem');
 				if (!template) return;
@@ -596,7 +596,7 @@
 	/**
 	 * Populate repeater fields
 	 */
-	populateRepeaterField(fieldWrapper, fieldName, fieldValue, options = {}) {
+	populateRepeaterField(fieldWrapper, fieldName, fieldValue) {
 		if (!fieldValue || !Array.isArray(fieldValue)) {
 			return;
 		}
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 4590141..d0fb19b 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -87,36 +87,25 @@
 	}
 
 	async initQueue() {
-		const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false)
-
-		if (incomplete.length > 0) {
-			this.startPolling();
-		} else {
+		let polling = this.maybeStartPolling();
+		if (!polling) {
 			this.updateStatusPanel('synced');
 		}
 
+
 		this.store.subscribe((event, data) => {
 			switch (event) {
 				case 'data-loaded':
 				case 'items-saved':
-					// Initial load from IndexedDB
-					const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
-					if (incomplete.length > 0) {
-						this.startPolling();
-					}
+					this.maybeStartPolling();
 					this.updateUI();
 					break;
 				case 'item-saved':
-					// Check for status changes
-					if (data.item) {
-						const oldItem = this.store.data.get(data.item.id);
-						if (oldItem && oldItem.status !== data.item.status) {
-							this.handleOperationStatusChange(data.item, oldItem.status);
-						}
+					console.log(data,'Item saved data');
+					if (data.previousItem && data.previousItem.status !== data.item.status) {
+						this.handleOperationStatusChange(data.item, data.previousItem.status);
 					}
-					if (this.hasQueuedOperations()) {
-						this.startPolling();
-					}
+					this.maybeStartPolling();
 					break;
 				default:
 					this.updateUI();
@@ -124,18 +113,27 @@
 			}
 
 		});
-		this.notify('queue-initialized', {operations: incomplete});
+	}
+
+	maybeStartPolling()
+	{
+		const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
+		if (incomplete.length > 0) {
+			this.startPolling();
+			return true;
+		}
+		return false;
 	}
 
 	/**
 	 * Handle operation status changes and notify subscribers
 	 */
-	handleOperationStatusChange(operation, oldStatus) {
-		if (!operation || oldStatus === operation.status) return;
+	handleOperationStatusChange(operation) {
 
 		// Notify based on new status
 		switch(operation.status) {
 			case 'completed':
+				console.log(operation);
 				this.notify('operation-completed', operation);
 				break;
 			case 'failed':
@@ -239,7 +237,6 @@
 	}
 
 	clearQueue(itemID) {
-		const item = this.store.get(itemID);
 		this.store.delete(itemID);
 	}
 
@@ -256,8 +253,6 @@
 	}
 
 	resetActivityTimer() {
-		this.lastActivity = Date.now();
-
 		if (this.activityTimer) {
 			clearTimeout(this.activityTimer);
 		}
@@ -315,13 +310,7 @@
 		this.setProcessing(false);
 		this.stopActivityTracking();
 
-		const pending = this.getOperationsByStatus(['queued', 'completed', 'failed_permanent'], false);
-		if (pending.length > 0) {
-			this.startPolling();
-			this.showQueue();
-		} else {
-			this.hideQueue();
-		}
+		this.maybeStartPolling() ? this.showQueue() : this.hideQueue();
 	}
 
 	async processOperation(operation) {
@@ -359,39 +348,11 @@
 			if (response.ok && result.success !== false) {
 				// Handle server-side merge
 				if (result.id && operation.id !== result.id) {
-
-					// Check if the returned ID exists locally
-					const existingOp = this.getQueue(result.id);
-
-					if (existingOp) {
-						// Merge data from both operations
-						existingOp.data = window.deepMerge(existingOp.data, operation.data);
-						existingOp.status = result.status || 'pending';
-						existingOp.serverData = result;
-						this.updateOperationStatus(existingOp.id, existingOp.status);
-						// Update the existing operation
-						this.setQueue(existingOp);
-
-						this.removeOperationFromUI(operation.id);
-
-						// Switch reference to the merged operation
-						operation = existingOp;
-					} else {
-						// Server merged with an operation we don't have locally
-						// Update the ID and continue
-						this.clearQueue(operation.id);
-						operation.id = result.id;
-						operation.status = result.status || 'pending';
-						operation.serverData = result;
-						this.updateOperationStatus(operation.id, operation.status);
-						this.setQueue(operation);
-					}
+					operation = await this.handleServerMerge(operation, result);
 				} else {
-					// Normal processing - no merge
 					operation.status = result.status || 'pending';
 					operation.serverData = result;
 					this.updateOperationStatus(operation.id, operation.status);
-					this.setQueue(operation);
 				}
 
 				this.a11y.announce(`${operation.title} sent to server for processing.`);
@@ -417,6 +378,29 @@
 		}
 	}
 
+	async handleServerMerge(operation, result) {
+		const existingOp = this.getQueue(result.id);
+
+		if (existingOp) {
+			// Merge with existing local operation
+			existingOp.data = window.deepMerge(existingOp.data, operation.data);
+			existingOp.status = result.status || 'pending';
+			existingOp.serverData = result;
+			this.updateOperationStatus(existingOp.id, existingOp.status);
+			this.removeOperationFromUI(operation.id);
+			this.clearQueue(operation.id);
+			return existingOp;
+		} else {
+			// Server merged with unknown operation
+			this.clearQueue(operation.id);
+			operation.id = result.id;
+			operation.status = result.status || 'pending';
+			operation.serverData = result;
+			this.updateOperationStatus(operation.id, operation.status);
+			return operation;
+		}
+	}
+
 	startPolling() {
 		if (this.isPolling) return;
 
@@ -428,8 +412,7 @@
 				this.store.clearCache();
 				await this.store.fetch(); // Fetches from server, updates store.data
 
-				const incomplete = this.getOperationsByStatus(['completed', 'failed_permanent'], false);
-				if (incomplete.length === 0) {
+				if (!this.maybeStartPolling()) {
 					this.stopPolling();
 					this.updateStatusPanel('synced');
 				}
@@ -451,7 +434,9 @@
 			this.countdownTimer = null;
 		}
 	}
-
+	getOperationIds(operations) {
+		return operations.map(op => op.id);
+	}
 	/***********************************************************
 	USER ACTIONS
 	 ***********************************************************/
@@ -560,10 +545,9 @@
 		};
 		this.handleOffline = () => this.updateStatusPanel('offline');
 		this.handleBeforeUnload = (e) => {
-			const hasPending = this.getOperationsByStatus(['queued', 'uploading']);
-			if (hasPending.length > 0) {
+			if (this.isPolling || this.isProcessing) {
 				e.preventDefault();
-				return 'You have unsaved changes in the queue.';
+				return 'You have unsaved changes in the queue. Proceed?';
 			}
 		};
 
@@ -580,16 +564,14 @@
 			this.store.clearHttpHeaders(); // Clear cached headers first
 			this.store.fetch();
 		} else if (e.target.closest(this.selectors.clearButton)) {
-			const completedOps = this.getOperationsByStatus('completed');
+			const completedOps = this.getOperationIds(this.getOperationsByStatus('completed'));
 			if (completedOps.length > 0) {
-				const ids = completedOps.map(op => op.id);
-				this.updateServerOperations(ids, 'dismiss');
+				this.updateServerOperations(completedOps, 'dismiss');
 			}
 		} else if (e.target.closest(this.selectors.retryButton)) {
-			const failedOps = this.getOperationsByStatus('failed');
+			const failedOps = this.getOperationIds(this.getOperationsByStatus('failed'));
 			if (failedOps.length > 0) {
-				const ids = failedOps.map(op => op.id);
-				this.updateServerOperations(ids, 'retry');
+				this.updateServerOperations(failedOps, 'retry');
 			}
 		} else if (e.target.closest('[data-action]')) {
 			const button = e.target.closest('[data-action]');
diff --git a/assets/js/concise/Referral.js b/assets/js/concise/Referral.js
index d3d1a4b..6435e98 100644
--- a/assets/js/concise/Referral.js
+++ b/assets/js/concise/Referral.js
@@ -15,7 +15,6 @@
 
 		this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
 		this.initElements();
-		this.storesInited = false;
 		this.initStore();
 		this.initListeners();
 		this.checkForReferral();
@@ -27,16 +26,12 @@
 			checkCode: '.check-code-btn',
 			submit: '[type=submit]',
 			recentList: '.recent-referrals-list',
-			invite: 'form.invite',
-			adminList: '.items-list.referral',
-			dash: '.replace .referral-dashboard',
 			stats: {
 				codeUsed: '[data-stat="code_used"]',
 				consultations: '[data-stat="consultations"]',
 				treatments: '[data-stat="treatments"]',
 				rewards: '[data-stat="total_rewards"]'
 			},
-			list: '.referrals-list'
 		};
 
 		this.forms = this.container.querySelectorAll('form');
@@ -61,10 +56,7 @@
 
 		this.ui = window.uiFromSelectors(this.selectors);
 
-		this.dashTabs = null;
-		if (this.ui.dash) {
-			this.dashTabs = new window.jvbTabs(this.ui.dash);
-		}
+
 
 		if (!this.hasCopy) {
 			document.querySelectorAll(this.selectors.copyBtn).forEach(btn => {
@@ -72,32 +64,6 @@
 			});
 		}
 		this.formController = null;
-
-		if (this.ui.invite) {
-			this.formController = new window.jvbForm();
-			this.formController.registerForm(
-				this.ui.invite,
-				{
-					autosave: true,
-					endpoint: 'referrals',
-					formStatus: false,
-				}
-			);
-
-			this.formController.subscribe((event, data) => {
-				if (event === 'form-submit') {
-					data = data.fullData;
-					data.action = 'invite';
-					window.jvbQueue.addToQueue(
-						{
-							endpoint: 'referrals',
-							data: data,
-							title: 'Submitting invitations',
-						}
-					);
-				}
-			});
-		}
 	}
 
 	initStore() {
@@ -147,27 +113,9 @@
 		if (this.listStore) {
 			this.listStore.subscribe(this.handleListEvent.bind(this));
 		}
-
-		if (this.ui.dash) {
-			this.initViewController();
-		}
 	}
 
-	initViewController() {
-		if (!this.listStore || !this.ui.adminList) return;
 
-		this.view = new window.jvbViews(this.ui.adminList, this.listStore);
-		this.view.subscribe((event, data) => {
-			switch(event) {
-				case 'item-action':
-					this.handleItemAction(data);
-					break;
-				case 'bulk-action':
-					this.handleBulkAction(data);
-					break;
-			}
-		});
-	}
 
 	initListeners() {
 		this.clickHandler = this.handleClick.bind(this);
@@ -254,82 +202,7 @@
 		}
 	}
 
-	/**
-	 * Handle item actions (remove, resend)
-	 */
-	handleItemAction(data) {
-		const { action, itemId } = data;
 
-		switch(action) {
-			case 'remove':
-				this.removeReferral(itemId);
-				break;
-			case 'resend':
-				this.resendInvite(itemId);
-				break;
-		}
-	}
-
-	/**
-	 * Remove referral from list
-	 */
-	async removeReferral(id) {
-		if (!confirm('Remove this referral from your list?')) return;
-
-		try {
-			const response = await fetch(`${jvbSettings.api}referrals`, {
-				method: 'POST',
-				headers: {
-					'Content-Type': 'application/json',
-					'X-WP-Nonce': window.auth.getNonce()
-				},
-				body: JSON.stringify({
-					action: 'remove',
-					referral_id: id
-				})
-			});
-
-			const result = await response.json();
-
-			if (result.success) {
-				// Refresh DataStore
-				if (this.listStore) this.listStore.fetch();
-				if (this.statsStore) this.statsStore.fetch();
-				this.a11y?.announce('Referral removed');
-			}
-		} catch (error) {
-			console.error('Error removing referral:', error);
-		}
-	}
-
-	/**
-	 * Resend invite email
-	 */
-	async resendInvite(id) {
-		try {
-			const response = await fetch(`${jvbSettings.api}referrals`, {
-				method: 'POST',
-				headers: {
-					'Content-Type': 'application/json',
-					'X-WP-Nonce': window.auth.getNonce()
-				},
-				body: JSON.stringify({
-					action: 'resend',
-					referral_id: id
-				})
-			});
-
-			const result = await response.json();
-
-			if (result.success) {
-				this.a11y?.announce('Invitation resent');
-			} else {
-				alert(result.message || 'Cannot resend yet. Wait 7 days between invites.');
-			}
-		} catch (error) {
-			console.error('Error resending invite:', error);
-		}
-	}
 
 
 	handleClick(e) {
@@ -359,65 +232,203 @@
 		// Try clipboard API first
 		if (this.hasCopy) {
 			navigator.clipboard.writeText(text).then(() => {
-				this.showCopySuccess(button);
-			}).catch(() => {
-				// Fallback to selection
-				this.selectText(codeElement);
-				this.showCopyFallback(button);
+				button.classList.toggle('success');
+				setTimeout(() => {
+					button.classList.remove('success');
+				}, 1500);
 			});
 		}
 	}
 
+
 	/**
-	 * Select text in element
+	 * Handle error response with field-specific feedback
 	 */
-	selectText(element) {
-		if (window.getSelection && document.createRange) {
-			const selection = window.getSelection();
-			const range = document.createRange();
-			range.selectNodeContents(element);
-			selection.removeAllRanges();
-			selection.addRange(range);
-		} else if (document.body.createTextRange) {
-			// IE fallback
-			const range = document.body.createTextRange();
-			range.moveToElementText(element);
-			range.select();
+	handleError(form, result) {
+		const { message, code, field } = result;
+
+		// If there's a specific field, highlight it
+		if (field) {
+			this.showFieldError(form, field, message);
+		} else {
+			// Show general form error using FormController pattern
+			this.showFormStatus(form, 'error', message || 'Something went wrong. Please try again.');
+		}
+
+		// Handle specific error codes
+		switch(code) {
+			case 'duplicate_email':
+				// Could add additional UI feedback
+				break;
+			case 'invalid_code':
+				// Unlock the referral code field so user can correct it
+				const codeInput = form.querySelector('[name="referral_code"]');
+				if (codeInput) {
+					codeInput.readOnly = false;
+					codeInput.focus();
+				}
+				break;
+			case 'turnstile_failed':
+				// Refresh Turnstile widget if available
+				if (window.turnstile && form.querySelector('.cf-turnstile')) {
+					window.turnstile.reset();
+				}
+				break;
 		}
 	}
 
 	/**
-	 * Show copy success feedback
+	 * Show error for specific field
 	 */
-	showCopySuccess(button) {
-		const originalHTML = button.innerHTML;
-		button.innerHTML = window.jvbIcon('check', {size: 16}) + ' Copied!';
-		button.classList.add('success');
+	showFieldError(form, fieldName, message) {
+		// Find the field wrapper (handles both direct names and referral_ prefixed names)
+		let fieldWrapper = form.querySelector(`.field[data-field="${fieldName}"]`);
+		if (!fieldWrapper) {
+			fieldWrapper = form.querySelector(`.field[data-field="referral_${fieldName}"]`);
+		}
 
-		setTimeout(() => {
-			button.innerHTML = originalHTML;
-			button.classList.remove('success');
-		}, 2000);
+		if (!fieldWrapper) {
+			// If no field wrapper found, show as general form error
+			this.showFormStatus(form, 'error', message);
+			return;
+		}
+
+		const input = fieldWrapper.querySelector('input, textarea, select');
+		const validationMessage = fieldWrapper.querySelector('.validation-message');
+		const errorIcon = fieldWrapper.querySelector('.validation-icon.error');
+		const successIcon = fieldWrapper.querySelector('.validation-icon.success');
+
+		if (!input) {
+			this.showFormStatus(form, 'error', message);
+			return;
+		}
+
+		// Apply error state (following FormController pattern)
+		fieldWrapper.classList.remove('has-success');
+		fieldWrapper.classList.add('has-error');
+		input.classList.add('error');
+		input.setAttribute('aria-invalid', 'true');
+
+		// Show error icon, hide success icon
+		if (errorIcon) errorIcon.hidden = false;
+		if (successIcon) successIcon.hidden = true;
+
+		// Show error message
+		if (validationMessage) {
+			validationMessage.textContent = message;
+			validationMessage.hidden = false;
+		}
+
+		// Focus the problematic field
+		input.focus();
+
+		// Announce to screen readers
+		this.a11y?.announce(`Error in ${fieldName}: ${message}`);
+	}
+
+	showFormStatus(form, status, message = '') {
+		const statusWrap = form.querySelector('.fstatus');
+		if (!statusWrap) {
+			console.warn('No .fstatus element found in form');
+			return;
+		}
+
+		statusWrap.hidden = false;
+		const statusElement = statusWrap.querySelector('.message');
+
+		// Clear previous state
+		statusWrap.querySelector('.icon')?.remove();
+		statusWrap.querySelector('.actions')?.remove();
+
+		// Status messages
+		const messages = {
+			'saving': 'Sending...',
+			'submitted': 'Sent successfully!',
+			'error': 'Something went wrong',
+			'checking': 'Checking code...'
+		};
+
+		// Status icons (using window.getIcon like FormController)
+		const icons = {
+			'submitted': 'check-circle',
+			'error': 'close-circle',
+			'checking': 'loading'
+		};
+
+		// Add icon if available
+		if (icons[status] && window.getIcon) {
+			const icon = window.getIcon(icons[status]);
+			if (icon) {
+				statusWrap.prepend(icon);
+			}
+		}
+
+		// Set message
+		if (statusElement) {
+			statusElement.textContent = message || messages[status] || status;
+		}
+
+		// Add loading class for pending states
+		statusWrap.classList.toggle('loading', ['saving', 'checking'].includes(status));
+
+		// Auto-hide success messages
+		if (status === 'submitted') {
+			setTimeout(() => statusWrap.hidden = true, 3000);
+		}
+
+		// Announce to screen readers
+		if (this.a11y) {
+			this.a11y.announce(message || messages[status] || status);
+		}
 	}
 
 	/**
-	 * Show fallback message
+	 * Clear all form errors
 	 */
-	showCopyFallback(button) {
-		const originalHTML = button.innerHTML;
-		button.innerHTML = '✓ Selected - Press Ctrl+C';
-		button.classList.add('selected');
+	clearFormErrors(form) {
+		// Clear field-level errors
+		form.querySelectorAll('.field.has-error, .field.has-success').forEach(fieldWrapper => {
+			this.clearFieldValidation(fieldWrapper);
+		});
 
-		setTimeout(() => {
-			button.innerHTML = originalHTML;
-			button.classList.remove('selected');
-		}, 3000);
+		// Hide form status
+		const statusWrap = form.querySelector('.fstatus');
+		if (statusWrap) {
+			statusWrap.hidden = true;
+		}
+	}
+
+	clearFieldValidation(fieldWrapper) {
+		if (!fieldWrapper) return;
+
+		const input = fieldWrapper.querySelector('input, textarea, select');
+		const validationMessage = fieldWrapper.querySelector('.validation-message');
+		const validationIcons = fieldWrapper.querySelectorAll('.validation-icon');
+
+		// Remove classes
+		fieldWrapper.classList.remove('has-error', 'has-success');
+		if (input) {
+			input.classList.remove('error');
+			input.removeAttribute('aria-invalid');
+		}
+
+		// Hide icons and messages
+		validationIcons.forEach(icon => icon.hidden = true);
+		if (validationMessage) {
+			validationMessage.hidden = true;
+			validationMessage.textContent = '';
+		}
 	}
 
 	handleInput(e) {
 		if (e.target.id === 'referral_code' || e.target.name === 'referral_code') {
 			e.target.value = e.target.value.toUpperCase();
 		}
+		// Clear field error when user types
+		const fieldWrapper = e.target.closest('.field');
+		if (fieldWrapper && fieldWrapper.classList.contains('has-error')) {
+			this.clearFieldValidation(fieldWrapper);
+		}
 	}
 
 	/**
@@ -585,63 +596,6 @@
 	}
 
 	/**
-	 * Load user stats
-	 */
-	async loadStats() {
-		const statsContainer = this.container.querySelector('.stats-summary');
-		if (!statsContainer) return;
-
-		try {
-			const response = await fetch(`${jvbSettings.api}referrals/my-stats?user=${window.auth.getUser()}`, {
-				headers: { 'X-WP-Nonce': window.auth.getNonce() }
-			});
-
-			const data = await response.json();
-			if (data.success && data.stats) {
-				this.updateStats(data.stats);
-			}
-		} catch (error) {
-			console.error('Error loading stats:', error);
-		}
-	}
-
-	async loadSidebarStats() {
-		try {
-			const response = await fetch(
-				`${jvbSettings.api}referrals/stats?user=${window.auth.getUser()}&type=quick`,
-				{ headers: { 'X-WP-Nonce': window.auth.getNonce() } }
-			);
-
-			const data = await response.json();
-			if (data.success && data.stats) {
-				this.updateSidebarStats(data.stats);
-			}
-		} catch (error) {
-			console.error('Error loading sidebar stats:', error);
-		}
-	}
-
-
-	/**
-	 * Update stats display
-	 */
-	updateStats(stats) {
-		const elements = {
-			total: this.container.querySelector('[data-stat="total"]'),
-			treated: this.container.querySelector('[data-stat="treated"]'),
-			pending: this.container.querySelector('[data-stat="pending"]'),
-			rewards: this.container.querySelector('[data-stat="rewards"]')
-		};
-
-		if (elements.total) elements.total.textContent = stats.total_referrals || 0;
-		if (elements.treated) elements.treated.textContent = stats.treated_count || 0;
-		if (elements.pending) elements.pending.textContent = stats.pending_count || 0;
-		if (elements.rewards) {
-			elements.rewards.textContent = '$' + parseFloat(stats.available_rewards || 0).toFixed(2);
-		}
-	}
-
-	/**
 	 * Render recent referrals list
 	 */
 	renderRecentReferrals() {
@@ -664,22 +618,6 @@
 	}
 
 	/**
-	 * Format date nicely
-	 */
-	formatDate(dateString) {
-		const date = new Date(dateString);
-		const now = new Date();
-		const diffTime = Math.abs(now - date);
-		const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
-
-		if (diffDays === 0) return 'Today';
-		if (diffDays === 1) return 'Yesterday';
-		if (diffDays < 7) return `${diffDays} days ago`;
-
-		return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
-	}
-
-	/**
 	 * Handle form submission
 	 */
 	async handleFormSubmit(event) {
@@ -688,6 +626,8 @@
 		const form = event.target;
 		const formData = new FormData(form);
 
+		// Clear any existing errors
+		this.clearFormErrors(form);
 		this.setFormLoading(true, form);
 
 		try {
@@ -695,32 +635,43 @@
 
 			if (form.id === 'referral-code-form') {
 				// Registration with referral code - goes to LoginRoutes
-				const data = {
+				let data = {
 					name: formData.get('referral_name'),
 					email: formData.get('referral_email'),
 					referral_code: formData.get('referral_code')
 				};
 
+				if (formData.get('cf-turnstile-response')) {
+					data['cf-turnstile-response'] = formData.get('cf-turnstile-response');
+				}
+
 				if (!data.name || !data.email || !data.referral_code) {
 					result.message = 'Please fill in all fields';
 				} else {
 					result = await this.makeRequest('auth/register', data); // UPDATED endpoint
 				}
 			} else if (form.id === 'login-form') {
-				const data = {
+				let data = {
 					type: 'login',
 					email: formData.get('login_email'),
 					context: {
 						redirect_to: window.location.href + '?seeReferral=1'
 					}
 				};
-				result = await this.makeRequest('magic', data);
+				if (formData.get('cf-turnstile-response')) {
+					data['cf-turnstile-response'] = formData.get('cf-turnstile-response');
+				}
+				if (!data.email) {
+					result.message = 'Please fill in your email';
+				} else {
+					result = await this.makeRequest('magic', data);
+				}
 			}
 
 			if (result.success) {
 				this.handleSuccess(form, result);
 			} else {
-				this.showFormMessage(form, result.message || 'Something went wrong. Please try again.', 'error');
+				this.handleError(form, result);
 			}
 		} catch (error) {
 			console.error('Error submitting form:', error);
@@ -814,20 +765,11 @@
 	 * Set form loading state
 	 */
 	setFormLoading(loading, form) {
-		const inputs = form.querySelectorAll('input, button');
+		const inputs = form.querySelectorAll('input, button, textarea, select');
 		inputs.forEach(input => input.disabled = loading);
 
-		const status = form.querySelector('.status');
-		if (status) {
-			status.classList.toggle('loading', loading);
-
-			if (loading) {
-				status.hidden = false;
-				const message = status.querySelector('.message');
-				if (message) {
-					message.textContent = 'Sending...';
-				}
-			}
+		if (loading) {
+			this.showFormStatus(form, 'saving');
 		}
 	}
 
@@ -841,6 +783,8 @@
 		});
 		this.container.dispatchEvent(event);
 	}
+
+
 }
 
 document.addEventListener('DOMContentLoaded', async function () {
diff --git a/assets/js/concise/ReferralAdmin.js b/assets/js/concise/ReferralAdmin.js
new file mode 100644
index 0000000..5de7a76
--- /dev/null
+++ b/assets/js/concise/ReferralAdmin.js
@@ -0,0 +1,335 @@
+class ReferralAdmin {
+	constructor() {
+		this.a11y = window.jvbA11y;
+		this.referral = window.jvbReferral;
+		this.hasCopy = navigator.clipboard && navigator.clipboard.writeText;
+		this.initElements();
+		this.initListeners();
+
+	}
+
+	initElements() {
+		this.selectors = {
+			copyBtn: '.copy-btn',
+			invite: 'form.invite',
+			adminList: '.items-list.referral',
+			dash: '.replace .referral-dashboard',
+			list: '.referrals-list'
+		}
+		this.ui = window.uiFromSelectors(this.selectors);
+
+		this.tabs = null;
+		if (this.ui.dash) {
+			this.tabs = new window.jvbTabs(this.ui.dash);
+			this.initViewController();
+		}
+		if (this.ui.invite) {
+			this.formController = new window.jvbForm();
+			this.formController.registerForm(
+				this.ui.invite,
+				{
+					autosave: true,
+					endpoint: 'referrals',
+					formStatus: false,
+				}
+			);
+
+			this.formController.subscribe((event, data) => {
+				if (event === 'form-submit') {
+					data = data.fullData;
+					data.action = 'invite';
+					window.jvbQueue.addToQueue(
+						{
+							endpoint: 'referrals',
+							data: data,
+							title: 'Submitting invitations',
+						}
+					);
+				}
+			});
+		}
+
+	}
+
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		document.addEventListener('click', this.clickHandler);
+		if (window.jvbQueue) {
+			window.jvbQueue.subscribe(this.handleQueueEvent.bind(this));
+		}
+	}
+
+	handleClick(e) {
+		const target = e.target.closest('.copy-btn');
+		if (target) {
+			this.handleCopyClick(target);
+		}
+	}
+
+	handleCopyClick(button) {
+		const targetId = button.dataset.target;
+		const codeElement = button.closest('.row').querySelector(`#${targetId}`);
+
+		if (!codeElement) return;
+
+		const text = codeElement.textContent.trim();
+
+		// Try clipboard API first
+		if (this.hasCopy) {
+			navigator.clipboard.writeText(text).then(() => {
+				button.classList.toggle('success');
+				setTimeout(() => {
+					button.classList.remove('success');
+				}, 1500);
+			});
+		}
+	}
+
+	initViewController() {
+		if (!this.referral.listStore || !this.ui.adminList) return;
+
+		this.view = new window.jvbViews(this.ui.adminList, this.referral.listStore);
+		this.view.subscribe((event, data) => {
+			switch(event) {
+				case 'item-action':
+					this.handleItemAction(data);
+					break;
+				case 'bulk-action':
+					this.handleBulkAction(data);
+					break;
+			}
+		});
+	}
+
+	/**
+	 * Handle item actions (remove, resend)
+	 */
+	handleItemAction(data) {
+		const { action, itemId } = data;
+
+		switch(action) {
+			case 'remove':
+				this.removeReferral(itemId);
+				break;
+			case 'resend':
+				this.resendInvite(itemId);
+				break;
+		}
+	}
+
+	/**
+	 * Remove referral from list
+	 */
+	async removeReferral(id) {
+		if (!confirm('Remove this referral from your list?')) return;
+
+		try {
+			const response = await fetch(`${jvbSettings.api}referrals`, {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+					'X-WP-Nonce': window.auth.getNonce()
+				},
+				body: JSON.stringify({
+					action: 'remove',
+					referral_id: id
+				})
+			});
+
+			const result = await response.json();
+
+			if (result.success) {
+				// Refresh DataStore
+				if (this.referral.listStore) this.referral.listStore.fetch();
+				if (this.referral.statsStore) this.referral.statsStore.fetch();
+				this.a11y?.announce('Referral removed');
+			}
+		} catch (error) {
+			console.error('Error removing referral:', error);
+		}
+	}
+
+	/**
+	 * Resend invite email
+	 */
+	async resendInvite(id) {
+		try {
+			const response = await fetch(`${jvbSettings.api}referrals`, {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+					'X-WP-Nonce': window.auth.getNonce()
+				},
+				body: JSON.stringify({
+					action: 'resend',
+					referral_id: id
+				})
+			});
+
+			const result = await response.json();
+
+			if (result.success) {
+				this.a11y?.announce('Invitation resent');
+			} else {
+				alert(result.message || 'Cannot resend yet. Wait 7 days between invites.');
+			}
+		} catch (error) {
+			console.error('Error resending invite:', error);
+		}
+	}
+
+	/***************************
+	BACKEND stuff
+	 ***************************/
+	handleQueueEvent(event, data) {
+		if (event !== 'operation-complete') return;
+		if (!data.details) return; // Not our operation
+
+		// Check if it's our invite operation
+		if (data.details.successful || data.details.failed) {
+			this.showInviteResults(data.details);
+		}
+	}
+
+	// showInviteResults(details) {
+	// 	const successful = details.successful?.length || 0;
+	// 	const failed = details.failed?.length || 0;
+	//
+	// 	if (failed === 0) {
+	// 		this.a11y?.announce(`All ${successful} invitations sent successfully!`);
+	// 		// Clear the form
+	// 		this.ui.invite?.reset();
+	// 	} else {
+	// 		// Show which ones failed
+	// 		const failureList = details.failed
+	// 			.map(f => `• ${f.name} (${f.email}): ${f.reason}`)
+	// 			.join('\n');
+	//
+	// 		const message = `${successful} sent, ${failed} failed:\n${failureList}`;
+	//
+	// 		// Show in a modal or persistent notification
+	// 		alert(message); // Or use a nicer notification system
+	// 	}
+	// }
+
+	showInviteResults(details) {
+		if (!this.ui.invite) return;
+
+		const tagListField = this.ui.invite.querySelector('[data-field="invite"]');
+		if (!tagListField) return;
+
+		const tagList = tagListField.querySelector('.tag-list');
+		if (!tagList) return;
+
+		// Map results by email for easy lookup
+		const resultMap = new Map();
+
+		details.successful?.forEach(item => {
+			resultMap.set(item.email, { status: 'success', name: item.name });
+		});
+
+		details.failed?.forEach(item => {
+			resultMap.set(item.email, {
+				status: 'error',
+				name: item.name,
+				reason: item.reason
+			});
+		});
+
+		// Update each tag with status
+		const tags = tagList.querySelectorAll('.tag');
+		tags.forEach(tag => {
+			const tagData = JSON.parse(tag.dataset.value || '{}');
+			const result = resultMap.get(tagData.email);
+
+			if (result) {
+				this.updateTagStatus(tag, result);
+			}
+		});
+
+		// Show summary notification
+		this.showInviteSummary(details);
+	}
+
+	updateTagStatus(tag, result) {
+		// Remove existing status
+		tag.classList.remove('success', 'error');
+		const existingIcon = tag.querySelector('.status-icon');
+		if (existingIcon) existingIcon.remove();
+
+		// Add new status
+		tag.classList.add(result.status);
+
+		const icon = document.createElement('span');
+		icon.className = 'status-icon';
+		icon.innerHTML = result.status === 'success'
+			? window.jvbIcon('check-circle', { size: 14 })
+			: window.jvbIcon('warning-circle', { size: 14 });
+
+		if (result.reason) {
+			icon.title = result.reason;
+		}
+
+		// Insert icon before the remove button
+		const removeBtn = tag.querySelector('.remove-tag');
+		if (removeBtn) {
+			tag.insertBefore(icon, removeBtn);
+		} else {
+			tag.appendChild(icon);
+		}
+	}
+
+	showInviteSummary(details) {
+		const successful = details.successful?.length || 0;
+		const failed = details.failed?.length || 0;
+
+		let message = `Invites sent! ${successful} successful`;
+		if (failed > 0) {
+			message += `, ${failed} failed`;
+		}
+
+		// Show in form status or as toast notification
+		if (this.formController) {
+			this.formController.showStatus(this.ui.invite, 'submitted', message);
+		}
+
+		// Optionally show detailed failures
+		if (failed > 0) {
+			this.showFailureDetails(details.failed);
+		}
+	}
+
+	showFailureDetails(failed) {
+		// Create a details element or modal showing why each failed
+		const detailsHTML = `
+        <details class="invite-failures" open>
+            <summary>${failed.length} invitation(s) failed - click for details</summary>
+            <ul>
+                ${failed.map(item => `
+                    <li>
+                        <strong>${window.escapeHtml(item.name)}</strong>
+                        (${window.escapeHtml(item.email)}):
+                        <em>${window.escapeHtml(item.reason)}</em>
+                    </li>
+                `).join('')}
+            </ul>
+        </details>
+    `;
+
+		// Insert after the form or in a notification area
+		const statusArea = this.ui.invite.querySelector('.fstatus');
+		if (statusArea) {
+			const existingDetails = statusArea.querySelector('.invite-failures');
+			if (existingDetails) existingDetails.remove();
+			statusArea.insertAdjacentHTML('beforeend', detailsHTML);
+		}
+	}
+}
+
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbAdminReferral = new ReferralAdmin();
+		}
+	});
+});
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index cf6f6cd..c8f39f7 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -112,7 +112,7 @@
 	}
 
 	async init() {
-		this.initializeFields();
+		// this.initializeFields();
 		this.initListeners();
 
 		// Queue integration - handle completion/failure
@@ -165,21 +165,18 @@
 	/*******************************************************************************
 	 * FIELD MANAGEMENT
 	 *******************************************************************************/
-	initializeFields() {
-		const fields = document.querySelectorAll(this.selectors.field.field);
-		fields.forEach(uploader => this.registerUploader(uploader));
-	}
-
-	scanFields(container) {
+	scanFields(container, autoUpload) {
+		console.log(autoUpload, 'autoUpload');
 		const fields = container.querySelectorAll(this.selectors.field.field);
-		fields.forEach(uploader => this.registerUploader(uploader));
+		fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
 	}
 
-	registerUploader(uploader) {
+	registerUploader(uploader, autoUpload) {
 		const fieldId = this.determineFieldId(uploader);
-		const config = this.extractFieldConfig(uploader);
+		const config = this.extractFieldConfig(uploader, autoUpload);
 		const ui = this.buildFieldUI(uploader);
 
+		console.log(config, 'registering with config');
 		// Store field data with Sets for runtime
 		const fieldData = {
 			id: fieldId,
@@ -206,8 +203,9 @@
 		return fieldId;
 	}
 
-	extractFieldConfig(fieldElement) {
+	extractFieldConfig(fieldElement, autoUpload) {
 		return {
+			autoUpload: autoUpload,
 			destination: fieldElement.dataset.destination || 'meta',
 			content: fieldElement.dataset.content || null,
 			mode: fieldElement.dataset.mode || 'direct',
@@ -493,8 +491,6 @@
 		const fieldWrapper = grid.closest('.field, .upload');
 		if (!fieldWrapper) return;
 
-		const movedItems = evt.items && evt.items.length > 0 ? evt.items : [evt.item];
-
 		// Get current order from DOM
 		let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
 			.map(upload => upload.dataset.uploadId)
@@ -640,6 +636,9 @@
 		// Meta field changes
 		if (fieldId) {
 			const fieldData = this.getFieldData(fieldId);
+			if (!fieldData.config.autoUpload) {
+				return;
+			}
 			if (fieldData?.config.destination === 'post_group') {
 				this.handleGroupMetaChange(e.target);
 			} else {
@@ -760,7 +759,7 @@
 		this.refreshSortable(fieldId);
 
 		// Queue for upload if in direct mode
-		if (fieldData.config.destination !== 'post_group') {
+		if (fieldData.config.autoUpload && fieldData.config.destination !== 'post_group') {
 			await this.queueUpload(fieldId);
 			this.maybeLockUploads(fieldId);
 		}
@@ -1554,7 +1553,8 @@
 		this.refreshSortable(fieldKey);
 
 		// Queue for upload if needed
-		if (config.mode === 'direct' && config.destination !== 'post_group') {
+		console.log(config);
+		if (config.autoUpload && config.mode === 'direct' && config.destination !== 'post_group') {
 			await this.queueUpload(fieldKey);
 		}
 	}
@@ -2488,16 +2488,6 @@
 		// Can add custom logic here if needed
 	}
 
-	getCurrentSelection(fieldId) {
-		let selected = [];
-		for (let [key, handler] of this.selectionHandlers) {
-			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
-				selected = selected.concat([...handler.selectedItems]);
-			}
-		}
-		return selected;
-	}
-
 	/*******************************************************************************
 	 * HELPER METHODS
 	 *******************************************************************************/
@@ -2632,27 +2622,6 @@
 		return progress[status] || 0;
 	}
 
-	getModalType(fieldEl) {
-		if (!fieldEl?.element) return null;
-		if (fieldEl._cachedModalType !== undefined) {
-			return fieldEl._cachedModalType;
-		}
-
-		const dialog = fieldEl.element.closest('dialog');
-		if (!dialog) {
-			fieldEl._cachedModalType = null;
-			return null;
-		}
-
-		let modalType = null;
-		if (dialog.classList.contains('edit')) modalType = 'edit';
-		else if (dialog.classList.contains('create')) modalType = 'create';
-		else if (dialog.classList.contains('bulkEdit')) modalType = 'bulkEdit';
-		else modalType = dialog.className;
-
-		fieldEl._cachedModalType = modalType;
-		return modalType;
-	}
 
 	createUploadElement(upload, draggable = false) {
 		let image = window.getTemplate('uploadItem');
@@ -2728,34 +2697,6 @@
 	 * PERSISTENCE
 	 *******************************************************************************/
 
-	/**
-	 * Normalize field data loaded from IndexedDB
-	 * Converts Arrays back to Sets, handles missing properties
-	 */
-	normalizeFieldData(fieldData) {
-		if (!fieldData) return null;
-
-		// Convert uploads array back to Set
-		if (Array.isArray(fieldData.uploads)) {
-			fieldData.uploads = new Set(fieldData.uploads);
-		} else if (!fieldData.uploads) {
-			fieldData.uploads = new Set();
-		}
-
-		// Convert groups array, ensure proper structure
-		if (!Array.isArray(fieldData.groups)) {
-			fieldData.groups = [];
-		}
-
-		// Ensure each group has uploads array
-		fieldData.groups = fieldData.groups.map(group => ({
-			...group,
-			uploads: Array.isArray(group.uploads) ? group.uploads : []
-		}));
-
-		return fieldData;
-	}
-
 	schedulePersistance(fieldId) {
 		const key = `persist_${fieldId}`;
 		window.debouncer.schedule(
@@ -2804,6 +2745,56 @@
 	}
 
 	/*******************************************************************************
+	 HELPER to GET UPLOADED FILES
+	*******************************************************************************/
+	/**
+	 * Get all files for a form (searches all upload fields within form)
+	 */
+	async getFilesForForm(formElement) {
+		const uploadFields = formElement.querySelectorAll('[data-upload-field]');
+		const allFiles = [];
+
+		for (const field of uploadFields) {
+			const fieldId = this.determineFieldId(field);
+			const files = await this.getFilesForField(fieldId);
+			allFiles.push(...files);
+		}
+
+		return allFiles;
+	}
+
+	/**
+	 * Get all files for a specific field
+	 */
+	async getFilesForField(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads) return [];
+
+		const files = [];
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
+
+		for (const uploadId of uploadsArray) {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) continue;
+
+			// Get the actual File object from blob data
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				files.push({
+					file: file,
+					uploadId: uploadId,
+					fieldName: fieldData.config.name,
+					meta: upload.meta || {}
+				});
+			}
+		}
+
+		return files;
+	}
+
+	/*******************************************************************************
 	 * RECOVERY & RESTORATION
 	 *******************************************************************************/
 
@@ -2856,7 +2847,7 @@
 		});
 		if (pendingFields.length === 0) return;
 
-		this.showRecoveryNotification(pendingFields);
+		await this.showRecoveryNotification(pendingFields);
 	}
 
 	async showRecoveryNotification(pendingFields) {
diff --git a/assets/js/concise/navigation.js b/assets/js/concise/navigation.js
index fb06d10..77c15e9 100644
--- a/assets/js/concise/navigation.js
+++ b/assets/js/concise/navigation.js
@@ -44,10 +44,6 @@
 		});
 	}
 
-	navIDs() {
-		return Array.from(this.navs.keys()).map((nav) => `#${nav}`);
-	}
-
 	initListeners() {
 		this.clickListener = this.handleClick.bind(this);
 		this.escapeListener = this.handleEscape.bind(this);
diff --git a/assets/js/concise/quill.js b/assets/js/concise/quill.js
index 3f16b31..0670b00 100644
--- a/assets/js/concise/quill.js
+++ b/assets/js/concise/quill.js
@@ -12,7 +12,7 @@
 			editor.className = 'editor';
 			toolbar = document.createElement('div');
 			toolbar.className = 'toolbar';
-			const image = textarea.dataset.allowimage === true ? `<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>` : '';
+			const image = textarea.dataset.allowimage === true ? `<button type="button" class="ql-jvb_image">\n                    ${window.getIcon('image')}\n                </button>` : '';
 			toolbar.id = `toolbar-${textarea.id}`;
 			toolbar.innerHTML = `
                 <span class="ql-formats">
@@ -93,10 +93,10 @@
 						h1: function() { this.quill.format('header', 1); },
 						h2: function() { this.quill.format('header', 2); },
 						h3: function() { this.quill.format('header', 3); },
-						jvb_bold: function() {this.quill.format('bold', true)},
-						jvb_italic: function() {this.quill.format('italic', true)},
-						jvb_strike: function() {this.quill.format('strike', true)},
-						jvb_underline: function() {this.quill.format('underline', true)},
+						'jvb_bold': function() {this.quill.format('bold', true)},
+						'jvb_italic': function() {this.quill.format('italic', true)},
+						'jvb_strike': function() {this.quill.format('strike', true)},
+						'jvb_underline': function() {this.quill.format('underline', true)},
 						'jvb_align': function(value) {
 							this.quill.format('align', value === this.quill.getFormat().list ? false : value);
 						},
@@ -107,8 +107,6 @@
 							if (value) {
 								const range = this.quill.getSelection();
 								if (range == null || range.length === 0) return;
-								// Get the existing link if any
-								const preview = this.quill.getText(range.index, range.length);
 								const existingLink = this.quill.getFormat(range).link;
 
 								// Create modal for link input
@@ -196,11 +194,6 @@
 									formData.append('post_id', objectID);
 								}
 
-								// Show loading state
-								if (window.jvbLoading) {
-									window.jvbLoading.showLoading('Uploading image...', 'Processing Upload');
-								}
-
 								try {
 									const response = await fetch(
 										`${jvbSettings.api}uploads/`,
@@ -229,9 +222,6 @@
 										'italic': true
 									}, true);
 								} finally {
-									if (window.jvbLoading) {
-										window.jvbLoading.hide();
-									}
 									input.remove();
 								}
 							};
diff --git a/assets/js/dash/CRUDOld.js b/assets/js/dash/CRUDOld.js
deleted file mode 100644
index 6f071e5..0000000
--- a/assets/js/dash/CRUDOld.js
+++ /dev/null
@@ -1,1824 +0,0 @@
-/**
- * The base CRUD manager: helps reduce code duplication between managing different content types
- */
-
-class CRUD {
-	/**
-	 * @param config
-	 */
-	constructor(config) {
-		console.log('Initializing Crud.js');
-		this.config = {
-			content: '',
-			type: 'post',
-
-			api: 'content',
-			batchSize: 10,
-			upload: {
-				mode: 'direct',
-				allowMultiple: true,
-				allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
-			},
-			selectors: {
-				container: '.replace',
-				gridWrap: '.items-list',
-				grid: '.items-list .item-grid',
-				view: '.radio-options.view',
-				columnsSelect: 'details.multi-select',
-				bulk: '.bulk-controls',
-				bulkActions: '.bulk-action-select',
-				selectAll: 'input.select-all',
-				count: '.bulk-controls .selected-count',
-				search: 'input[type="search"]',
-				scroll: '.scroll-sentinel',
-				add: '.create-item',
-				dateRange: 'dialog.date-range',
-				filters: '.items-list .all-filters',
-				clearButton: '.clear-filters',
-				uploader: 'details.uploader'
-			},
-			filters: {},
-			modals: {},
-			tabs: {},
-			...config
-		};
-
-
-		this.editing = false;
-
-		this.api = this.config.api;
-		this.content = this.config.content;
-
-		this.plural = jvbSettings.labels[this.content.replace('-', '_')].plural ?? this.content + 's';
-		this.single = jvbSettings.labels[this.content.replace('-', '_')].single ?? this.content;
-
-		this.batchSize = this.config.batchSize; // for renderItems document fragment
-		this.tabs = window.isEmptyObject(this.config.tabs) ? false : this.config.tabs;
-		this.tabNav = false;
-		this.editModal = document.querySelector('dialog.edit-modal').firstElementChild.cloneNode(true).cloneNode(true);
-		this.bulkEditModal = document.querySelector('dialog.bulk-edit-modal').firstElementChild.cloneNode(true).cloneNode(true);
-		this.createModal = document.querySelector('dialog.create-modal').firstElementChild.cloneNode(true).cloneNode(true);
-
-		this.config.modals = {
-			create: {
-				open: '.create-item',
-				selector: '.create-modal',
-				openMessage: 'Opened Create New ' + this.single + ' Modal',
-				closeMessage: 'Closed Create New ' + this.single + ' Modal',
-				onOpen: () => this.openCreateModal(),
-				onRender: () => this.renderCreateModal(),
-				onClose: () => this.closeCreateModal(),
-				onSave: (changes) => this.handleCreateModalSave(changes),
-			},
-			edit: {
-				selector: '.edit-modal',
-				open: 'button[data-action="edit"]',
-				openMessage: 'Opened Edit ' + this.single + ' Modal',
-				closeMessage: 'Closed Edit ' + this.single + ' Modal',
-				// onOpen: (e, modal) => this.openEditModal(e, modal),
-				onRender: (e, modal) => this.renderEditModal(e, modal),
-				onClose: (e, changes) => this.closeEditModal(e, changes),
-				onSave: (changes) => this.handleEditModalSave(changes),
-			},
-			bulkEdit: {
-				selector: '.bulk-edit-modal',
-				openMessage: 'Opened Bulk Edit ' + this.plural + ' Modal',
-				closeMessage: 'Closed Bulk Edit ' + this.plural + ' Modal',
-				onOpen: (e, modal) => this.openBulkEditModal(e, modal),
-				onRender: () => this.renderBulkEditModal(),
-				onClose: (e, changes) => this.closeBulkEditModal(e, changes),
-				onSave: (changes) => this.handleBulkEditModalSave(changes),
-			},
-			dateRange: {
-				selector: '.date-range',
-				openMessage: 'Opened Date Range selection',
-				closeMessage: 'Closed Date Range selection',
-				onOpen: () => this.handleDateRangeOpen(),
-				onClose: () => this.handleDateRangeClose(),
-			}
-			,
-			...this.config.modals
-		};
-
-		//Core components
-		this.a11y = window.jvbA11y;
-		this.errors = window.jvbError;
-		this.cache = window.jvbCache;
-		this.queue = window.jvbQueue;
-		this.loading = window.jvbLoading;
-
-		window.jvbLoading.setContent(this.content);
-
-		//Filters Management
-		this.filters = {};
-		this.resetFilters();
-		this.isLoading = false;
-		this.hasMore = true;
-		this.totalItems = null;
-		this.pages = 1;
-
-		//View Management
-		this.view = null;
-		this.views = new Map();
-		this.viewSettings = {};
-
-		//Stores fetched posts
-		this.posts = new Map();
-		//Stores selected items in a map
-		this.selected = new Set();
-
-		this.initElements();
-		this.initListeners();
-
-		this.table = null;
-		this.isTable = false;
-
-		this.initView();
-
-		this.handleEscape = (e) => {
-			if (e.key === 'Escape' && this.selected.size > 0) {
-				this.clearSelection();
-				this.elements.selectAll.nextElementSibling.firstElementChild.innerText = 'Select All';
-				this.a11y.announce('Selection cleared');
-			}
-		}
-
-		this.lastSelectedItem = null; // Track last selected item for range selection
-		this.selectionMode = false;
-
-	}
-
-	async initView() {
-		this.views.set('grid', {
-			name: 'grid',
-			type: 'visual',
-			template: 'gridView',
-			containerClass: 'grid-view',
-			init: () => this.initVisualView('grid'),
-			activate: () => this.activateVisualView('grid'),
-			deactivate: () => this.deactivateVisualView('grid'),
-			render: (items, append) => this.renderVisualItems(items, 'grid', append)
-		});
-
-		this.views.set('list', {
-			name: 'list',
-			type: 'visual',
-			template: 'listView',
-			containerClass: 'list-view',
-			init: () => this.initVisualView('list'),
-			activate: () => this.activateVisualView('list'),
-			deactivate: () => this.deactivateVisualView('list'),
-			render: (items, append) => this.renderVisualItems(items, 'list', append)
-		});
-
-		this.views.set('table', {
-			name: 'table',
-			type: 'editable',
-			template: 'tableView',
-			containerClass: 'table-view',
-			tableContainer: null,
-			tableForm: null,
-			init: () => this.initTableView(),
-			activate: () => this.activateTableView(),
-			deactivate: () => this.deactivateTableView(),
-			render: (items, append) => this.renderTableItems(items, append),
-			cleanup: () => this.cleanupTableView()
-		});
-		let view = await this.loadViewSettings();
-		await this.switchView(view);
-		await this.loadContent(true);
-	}
-
-	clearSelection() {
-		if (this.selected.size === 0) {
-			return;
-		}
-
-		this.selected.clear();
-
-		let checkboxes;
-		if (this.isTable) {
-			checkboxes = document.querySelectorAll('table .select-checkbox:checked');
-		} else {
-			checkboxes = this.elements.grid.querySelectorAll('.select-checkbox:checked');
-		}
-		checkboxes.forEach((check) => {
-			check.checked = false;
-		});
-		this.elements.selectAll.checked = false;
-		this.lastSelectedItem = null; // Reset last selected item
-		this.selectionMode = false;   // Exit selection mode
-		this.updateBulkControls();
-	}
-
-	initElements() {
-		this.elements = {};
-
-		// Cache all configured selectors
-		Object.entries(this.config.selectors).forEach(([key, selector]) => {
-			if (typeof selector === 'object') {
-				this.elements[key] = {};
-				Object.entries(selector).forEach(([subKey, subSelector]) => {
-					this.elements[key][subKey] = document.querySelector(subSelector);
-				});
-			} else {
-				this.elements[key] = document.querySelector(selector);
-			}
-		});
-
-		// Ensure required elements exist
-		if (!this.elements.container) {
-			throw new Error(`CRUD Manager: Container element not found for ${this.config.content}`);
-		}
-
-		if (!this.elements.grid) {
-			throw new Error(`CRUD Manager: Grid element not found for ${this.config.content}`);
-		}
-	}
-
-	initListeners() {
-		this.clickHandler = this.handleClick.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-		this.keyHandler = this.handleKeydown.bind(this);
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('change', this.changeHandler);
-		document.addEventListener('keydown', this.keyHandler);
-		this.initInfiniteScroll();
-		this.initUploader();
-		this.initModals();
-		this.initTabs();
-		this.initFilters();
-	}
-
-	handleClick(e) {
-		if (window.targetCheck(e, '.item-select') && e.shiftKey) {
-			e.preventDefault();
-			this.handleRangeSelection(e.target);
-		} else if (window.targetCheck(e, '.item-select')) {
-			const checkbox = e.target.closest('.item-select').querySelector('.select-checkbox');
-			if (checkbox) {
-				this.lastSelectedItem = checkbox.closest('.item');
-				this.selectionMode = this.selected.size > 0;
-			}
-		} else if (window.targetCheck(e, '.action') && window.targetCheck(e, '.item')) {
-
-			let action = window.targetCheck(e, '.action');
-			let item = window.targetCheck(e, '.item');
-			this.handleItemAction(action.dataset.action, item);
-		} else if (this.isTable && window.targetCheck(e, '.create-item')) {
-			this.addEmptyRow();
-		} else if (window.targetCheck(e, '.apply-bulk')) {
-			this.handleBulkControl();
-		} else if (window.targetCheck(e, '.cancel-bulk')) {
-			this.clearSelection();
-		} else if (window.targetCheck(e, this.config.selectors.clearButton)) {
-			this.handleClearFilters();
-		} else if (window.targetCheck(e, 'details.uploader summary')) {
-			this.saveViewSettings();
-		}
-
-	}
-
-	handleRangeSelection(target) {
-		if (!this.lastSelectedItem) {
-			// If no last selected item, treat as normal selection
-			this.updateSelected(target.closest('.item-select').querySelector('.select-checkbox'));
-			return;
-		}
-
-		if (this.isTable) {
-
-		}
-		let container = (this.isTable) ? document.querySelector('table') : this.elements.grid;
-
-		const currentItem = target.closest('.item');
-		const allItems = Array.from(container.querySelectorAll('.item'));
-
-		const lastIndex = allItems.indexOf(this.lastSelectedItem);
-		const currentIndex = allItems.indexOf(currentItem);
-
-		if (lastIndex === -1 || currentIndex === -1) {
-			// Fallback to normal selection if items not found
-			this.updateSelected(target.closest('.item-select').querySelector('.select-checkbox'));
-			return;
-		}
-
-		// Determine range
-		const startIndex = Math.min(lastIndex, currentIndex);
-		const endIndex = Math.max(lastIndex, currentIndex);
-
-		// Select all items in range
-		let newSelections = 0;
-		for (let i = startIndex; i <= endIndex; i++) {
-			const item = allItems[i];
-			const checkbox = item.querySelector('.select-checkbox');
-
-			if (checkbox && !checkbox.checked) {
-				checkbox.checked = true;
-				this.selected.add(checkbox.value);
-				newSelections++;
-			}
-		}
-
-		this.updateBulkControls();
-		this.selectionMode = this.selected.size > 0;
-
-		// Announce to screen readers
-		if (window.jvbA11y) {
-			const rangeSize = endIndex - startIndex + 1;
-			window.jvbA11y.announce(`Selected range of ${rangeSize} items. ${newSelections} new selections. ${this.selected.size} total selected.`);
-		}
-	}
-
-	handleKeydown(e) {
-		if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
-			e.preventDefault();
-			this.elements.selectAll.checked = true;
-			this.selectAll();
-		}
-	}
-
-	handleChange(e) {
-
-		if (this.isTable && window.targetCheck(e, 'input#vertical')) {
-			e.preventDefault();
-			this.viewSettings.tabNav = e.target.checked;
-			// Save preference
-			localStorage.setItem('jvbTabNav', e.target.checked ? 'vertical' : 'horizontal');
-
-			// Announce change
-			window.jvbA11y.announce(
-				this.viewSettings.tabNav ? 'Changed to vertical navigation' : 'Changed to horizontal navigation'
-			);
-
-			// Save view settings
-			this.saveViewSettings();
-			return;
-		} else if (this.isTable && window.targetCheck(e, '.multi-select')) {
-			this.handleColumnVisibility(e);
-		}
-		if (window.targetCheck('select.date-filter')) {
-			let value = e.target.value;
-			if (value !== 'custom') {
-				if (this.elements.dateRange.open) {
-					this.modals.dateRange.handleClose();
-				}
-
-				const monthSelect = this.elements.dateRange.querySelector('.month-select');
-				if (monthSelect) {
-					monthSelect.value = '';
-				}
-				this.setDateFilter(value);
-
-			}
-		}
-		switch (true) {
-			case e.target.value === 'custom':
-				this.openModal('dateRange');
-				break;
-			case 'action' in e.target.dataset:
-
-				break;
-			case 'taxonomy' in e.target.dataset:
-				this.updateFilters(e.target.dataset.filter, e.target.value, e.target.dataset.taxonomy);
-				break;
-			case 'filter' in e.target.dataset:
-				this.updateFilters(e.target.dataset.filter, e.target.value);
-				break;
-			case 'view' in e.target.dataset:
-				this.switchView(e.target.dataset.view);
-				break;
-		}
-		if (window.targetCheck(e, '.item-select')) {
-			this.updateSelected(e.target);
-		} else if (window.targetCheck(e, this.config.selectors.selectAll)) {
-			this.selectAll();
-		} else if (window.targetCheck(e, '.date-range .month-select')) {
-			this.handleMonthSelect(e);
-		} else if (window.targetCheck(e, '.date-range .date-start') || window.targetCheck(e, '.date-range .date-end')) {
-			let start = e.target.closest('.date-range').querySelector('.date-start').value;
-			let end = e.target.closest('.date-range').querySelector('.date-end').value;
-
-			if (start && end) {
-				let startDate = new Date(start);
-				let endDate = new Date(end);
-				endDate.setHours(23, 59, 59, 999);
-				this.setDateFilter('custom', startDate, endDate);
-				this.modals.dateRange.handleClose();
-			}
-		}
-	}
-
-
-	selectAll() {
-		if (this.elements.selectAll.checked) {
-			this.elements.selectAll.nextElementSibling.firstElementChild.innerText = 'Clear Selection';
-			let container = (this.isTable) ? document.querySelector('table') : this.elements.grid;
-			container.querySelectorAll('.select-checkbox:not(:checked)').forEach((check) => {
-				check.checked = true;
-				this.selected.add(check.value);
-			});
-			this.updateBulkControls();
-		} else {
-			this.elements.selectAll.nextElementSibling.firstElementChild.innerText = 'Select All';
-			this.clearSelection();
-		}
-
-	}
-
-	openModal(modal) {
-		if (this.modals[modal]) {
-			this.modals[modal].handleOpen();
-		}
-	}
-
-	initInfiniteScroll() {
-		if (!this.elements.scroll) return;
-		const observer = new IntersectionObserver(entries => {
-			entries.forEach(entry => {
-				if (entry.isIntersecting && this.hasMore) {
-					this.loadContent();
-				}
-			})
-		});
-
-		observer.observe(this.elements.scroll);
-	}
-
-	initModals() {
-		this.modals = {};
-		for (let [modal, config] of Object.entries(this.config.modals)) {
-			let m = document.querySelector(config.selector);
-			if (m) {
-				this.modals[modal] = new window.jvbModal(m, config);
-			}
-		}
-	}
-
-	initUploader() {
-		const uploader = document.querySelector('details.uploader .field.image');
-		if (!uploader) {
-			return;
-		}
-
-		// Register with centralized UploadManager instead of creating new instance
-		// Store reference for later use
-		this.uploaderFieldId = window.jvbUploadManager.registerUploader(uploader, {
-			content: this.content,
-			onUploadComplete: (result) => this.handleUploadComplete(result),
-			onGroupingComplete: (result) => this.loadContent(true)
-		});
-
-	}
-
-	handleUploadComplete(result) {
-
-		if (result.success && result.data) {
-			location.reload();
-		}
-	}
-
-	showPreview(e) {
-		console.log(e);
-	}
-
-	initTabs() {
-	}
-
-	initFilters() {
-	}
-
-	createContent() {
-	}
-
-	editContent() {
-	}
-
-	bulkEditContent() {
-	}
-
-	updateSelected(element) {
-		if (element.checked) {
-			this.selected.add(element.value);
-			this.lastSelectedItem = element.closest('.item'); // Update last selected
-		} else if (this.selected.has(element.value)) {
-			this.selected.delete(element.value);
-			this.lastSelectedItem = null;
-		}
-
-		this.selectionMode = this.selected.size > 0;
-		this.updateBulkControls();
-	}
-
-	updateBulkControls() {
-		const selecting = this.selected.size > 0;
-		this.elements.grid.classList.toggle('selecting', selecting);
-		this.elements.bulk.querySelector('.bulk-actions').hidden = !selecting;
-
-		if (selecting) {
-			document.addEventListener('keydown', this.handleEscape);
-		} else {
-			document.removeEventListener('keydown', this.handleEscape);
-		}
-
-		if (this.elements.count) {
-			this.elements.count.textContent = selecting ? `( ${this.selected.size} selected )` : '';
-		}
-
-		window.removeChildren(this.elements.bulkActions);
-		let options;
-		if (this.filters.status === 'trash') {
-			options = window.getTemplate('trashOptions');
-		} else {
-			options = window.getTemplate('notTrashOptions');
-		}
-		options.querySelectorAll('option').forEach((option) => {
-			this.elements.bulkActions.append(option);
-		});
-		this.elements.bulkActions.firstElementChild.checked = true;
-		this.elements.bulkActions.firstElementChild.selected = true;
-		options.remove();
-
-	}
-
-	handleBulkControl() {
-		let action = this.elements.bulkActions.value;
-		switch (action) {
-			case 'delete':
-				if (confirm(`Hold up! Are you sure you want to permanently delete these ${this.selected.size} ${this.plural}?\n\nThis is a forever kind of deal - no takebacks!`)) {
-					this.handleBulkEdit('delete');
-				}
-				break;
-			case 'edit':
-				this.modals['bulkEdit'].handleOpen();
-				break;
-			case 'restore':
-				this.handleBulkEdit('draft');
-				break;
-			case 'trash':
-			case 'publish':
-			case 'draft':
-				this.handleBulkEdit(action);
-				break;
-
-		}
-	}
-
-	handleItemAction(action, item) {
-		const ID = item.dataset.id;
-		this.clearSelection();
-		switch (action) {
-			case 'restore':
-			case 'trash':
-				this.selected.add(ID);
-				this.handleBulkEdit(action);
-				break;
-			case 'delete':
-				if (confirm(`Hold up! Are you sure you want to permanently delete this ${this.single}?\n\nThis is a forever kind of deal - no taking it back.`)) {
-					this.selected.add(ID);
-					this.handleBulkEdit(action);
-				}
-				break;
-			case 'toggle-status':
-				const current = item.dataset.status;
-				const newStatus = current === 'publish' ? 'draft' : 'publish';
-				this.selected.add(ID);
-				this.handleBulkEdit(newStatus);
-				item.dataset.status = newStatus;
-				window.removeChildren(item.querySelector('[data-action="toggle-status"]'));
-				item.querySelector('[data-action="toggle-status"]').append(window.getIcon(newStatus));
-				break;
-
-		}
-	}
-
-	resetFilters(elements = false) {
-		this.filters = {
-			content: this.content,
-			status: 'all',
-			taxonomies: {},
-			page: 1,
-			order: 'DESC',
-			orderby: 'date',
-			...this.config.filters //additional filters can be added in constructor
-		}
-
-
-
-		if (elements) {
-			let checks = [this.filters.status, this.filters.order, this.filters.orderby];
-			checks.forEach((check) => {
-				let item = this.elements.filters.querySelector(`[data-filter][value="${check}"]`);
-				if (item) {
-					item.checked = true;
-				}
-			});
-
-			this.elements.filters.querySelectorAll('select').forEach(select => {
-				select.value = '';
-			});
-
-			this.updateClearFiltersButton();
-			this.hasMore = true;
-			this.loadContent(true);
-		}
-
-	}
-
-	async switchView(view) {
-		if (!this.views.has(view)) {
-			console.error(`View "${view}" not registered`);
-			return;
-		}
-
-		// Don't switch if already in this view
-		if (this.view === view) {
-			return;
-		}
-
-		try {
-			// Store current data if we have any
-			const hasData = this.posts.size > 0;
-
-			// Deactivate current view
-			if (this.view) {
-				const currentViewObj = this.views.get(this.view);
-				await currentViewObj.deactivate();
-			}
-
-			// Activate new view
-			const newView = this.views.get(view);
-			await newView.activate();
-
-			this.view = view;
-			this.elements.view.querySelector(`[data-view="${view}"]`).checked = true;
-
-			// Save view preference
-			this.saveViewSettings();
-
-			// If we already have data, just re-render it in the new view
-			if (hasData) {
-				// Convert Map to array for rendering
-				const items = Array.from(this.posts.values());
-
-				// Clear the display area first
-				this.clearContent();
-
-				// Render existing data in new view format
-				this.renderItems(items, false);
-
-				// Show loading state briefly for UX
-				window.jvbA11y?.announce(`Switched to ${view} view`);
-			} else {
-				// Only load data if we don't have any
-				this.hasMore = true;
-				this.loadContent(true);
-			}
-
-		} catch (error) {
-			console.error('Error switching view:', error);
-			window.showToast?.(`Failed to switch to ${view} view`, 'error');
-		}
-	}
-
-
-	/**
-	 * Updates the filters from the node element
-	 * HTML element MUST have: data-filter-by and data-filter
-	 * @param {string} filter
-	 * @param {string} value
-	 * @param {?string} taxonomy
-	 */
-	updateFilters(filter, value, taxonomy = null) {
-
-		// If the filter isn't defined in the filter object, we won't be able to filter by it
-		if (Object.hasOwn(this.filters, filter)) {
-			if (taxonomy !== null) {
-				if (!Object.hasOwn(this.filters[filter], taxonomy)) {
-					this.filters[filter][taxonomy] = [];
-				}
-				this.filters[filter][taxonomy].push(value);
-			} else {
-				this.filters[filter] = value;
-			}
-
-			this.saveViewSettings();
-			this.updateClearFiltersButton();
-		}
-		this.hasMore = true;
-		this.loadContent(true);
-	}
-
-	buildFilters() {
-		const temp = {};
-		for (const [name, value] of Object.entries(this.filters)) {
-			if (value) {
-				if (typeof value === 'object' && window.isEmptyObject(value)) {
-
-				} else if (typeof value === 'object') {
-					temp[name] = {};
-					for (let [key, v] of Object.entries(value)) {
-						temp[name][key] = v.join(',');
-					}
-
-					temp[name] = JSON.stringify(temp[name]);
-				} else {
-					temp[name] = value;
-				}
-			}
-		}
-		temp.user = jvbSettings.currentUser;
-
-		return new URLSearchParams(temp);
-	}
-
-	async loadContent(reset = false, force = false) {
-		if (this.isLoading || !this.hasMore) return;
-
-		try {
-			this.isLoading = true;
-			this.loading.showLoading();
-			if (reset) {
-				this.filters.page = 1;
-				this.clearContent();
-			}
-
-			const filters = this.buildFilters();
-			const data = await this.cache.fetchWithCache(
-				`${jvbSettings.api}${this.api}?${filters.toString()}`,
-				{
-					method: 'GET',
-					headers: {
-						'X-WP-Nonce': jvbSettings.nonce,
-						'action_nonce': jvbSettings.dash,
-					}
-				},
-				{
-					context: this.content,
-					forceRefresh: force,
-				}
-			);
-
-			console.log('Fetched data: ', data);
-
-			if (data.pagination) {
-				this.hasMore = data['has_more'];
-				this.totalItems = data.items;
-				this.pages = data.pages;
-			} else {
-				this.hasMore = false;
-				this.totalItems = 0;
-				this.pages = 0;
-			}
-
-			if (data.items && data.items.length > 0) {
-				this.renderItems(data.items, this.filters.page >1);
-				this.cacheItems(data.items);
-			} else if (reset) {
-				this.showEmptyState();
-			}
-
-			if (this.hasMore) {
-				this.filters.page++;
-			}
-		} catch (error) {
-			this.handleError(
-				error,
-				'loadContent'
-			);
-			throw error;
-		} finally {
-			this.isLoading = false;
-			this.loading.hideLoading();
-		}
-	}
-
-	clearContent() {
-		if (this.view === 'table') {
-			const tbody = document.querySelector('form.table tbody');
-			if (tbody) {
-				window.removeChildren(tbody);
-			}
-		} else {
-			const grid = document.querySelector(this.config.selectors.grid);
-			if (grid) {
-				window.removeChildren(grid);
-			}
-		}
-	}
-
-	/**
-	 * Cache items for later reference
-	 */
-	cacheItems(items) {
-		items.forEach(item => {
-			this.posts.set(item.id, item);
-		});
-	}
-
-	showEmptyState() {
-
-		if (this.view === 'table') {
-			const template = window.getTemplate('emptyState');
-			const table = document.querySelector('form.table tbody');
-			if (table) {
-				table.appendChild(template);
-			}
-		} else {
-			const template = window.getTemplate('emptyState');
-			const grid = document.querySelector(this.config.selectors.grid);
-			if (grid) {
-				grid.appendChild(template);
-			}
-		}
-	}
-
-	hideEmptyState() {
-		if (this.isTable) {
-			this.table.classList.remove('empty');
-		} else {
-			this.elements.grid.classList.remove('empty');
-		}
-
-		this.elements.container.querySelector('.empty-state')?.remove();
-	}
-
-	/**
-	 * Handle errors
-	 * @param {Error} error - Error object
-	 * @param {string} action - Action being performed when error occurred
-	 */
-	handleError(error, action) {
-		console.error(`CRUD error (${action}):`, error);
-
-		// Show toast notification
-		showToast(
-			`Error ${action}: ${error.message || 'Something went wrong'}`,
-			'error'
-		);
-
-		// Log with error handler if available
-		if (window.jvbError) {
-			window.jvbError.log(error, {
-				component: 'CRUD',
-				action: action
-			});
-		}
-
-		// Announce to screen readers
-		if (window.jvbA11y) {
-			window.jvbA11y.announce(`Error ${action}. ${error.message || 'Please try again.'}`);
-		}
-	}
-
-	/**
-	 * Modal Handlers
-	 */
-	renderEditModal(e, modal) {
-		let item = e.target.closest('.item');
-		this.editing = item.dataset.id;
-		let fields = JSON.parse(item.dataset.fields);
-		let images = JSON.parse(item.dataset.images);
-
-		modal.querySelector('h2').textContent = (fields['post_title'] !== '') ? `Editing "${fields['post_title']}"` : `Editing ${this.single}`;
-
-		if (item.dataset.status) {
-			modal.querySelector(`[name="status"][value="${item.dataset.status}"]`).checked = true;
-		}
-
-		window.jvbForm.removeChangeListener();
-		window.jvbForm.populateFormFields(modal.querySelector('form'), fields, images);
-		// window.jvbForm.processChanges(modal.form, {processSave: false});
-		window.jvbForm.addChangeListener();
-	}
-
-	closeEditModal() {
-		let modal = document.querySelector('dialog.edit-modal');
-		window.removeChildren(modal);
-		let inside = this.editModal.cloneNode(true);
-		modal.append(inside);
-	}
-
-	handleEditModalSave(changes) {
-		let data = {};
-		data[this.editing] = changes;
-
-		this.saveData(data);
-	}
-
-	openBulkEditModal(e, modal) {
-		let selected = modal.querySelector('.selected');
-		for (let item of this.selected) {
-			item = parseInt(item);
-			if (!this.posts.has(item)) {
-				let element = document.querySelector(`.item-grid .item[data-id="${item}"]`);
-				item = {
-					fields: JSON.parse(element.dataset.fields),
-					images: JSON.parse(element.dataset.images)
-				}
-
-			} else {
-				item = this.posts.get(item);
-			}
-
-			let img = window.getTemplate('bulkItem');
-			let check = img.querySelector('input[type=checkbox]');
-			let image = img.querySelector('img');
-			[
-				img.htmlFor,
-				check.name,
-				check.id,
-				check.checked,
-				image.src,
-				image.alt,
-			] = [
-				item.id,
-				item.id,
-				item.id,
-				true,
-				item.images[item.fields['post_thumbnail']].medium,
-				item.images[item.fields['post_thumbnail']].alt,
-			];
-			selected.append(img);
-		}
-	}
-
-	renderBulkEditModal(modal) {
-
-	}
-
-	closeBulkEditModal(e, changes) {
-		for (let id of this.selected) {
-			//Remove any selected that are not selected from the bulk editor
-			if (!changes[id])  {
-				this.selected.delete(id);
-			}
-			//Remove the selected from the changes, because we don't need to send that to the server
-			delete changes[id];
-		}
-		if (window.isEmptyObject(changes) || this.selected.size === 0) {
-			return;
-		}
-		let temp = {};
-		for (let key in changes) {
-
-			let value = changes[key];
-			key = key.replace('bulk-edit-', '');
-			temp[key] = value;
-		}
-
-		let data = {};
-		for (let id of this.selected){
-			data[id] = temp;
-		}
-		this.saveData(data);
-
-
-		let modal = document.querySelector('dialog.bulk-edit-modal');
-		window.removeChildren(modal);
-		modal.append(this.bulkEditModal);
-	}
-
-	handleBulkEditModalSave(changes) {
-
-	}
-
-	openCreateModal(modal) {
-		if (this.isTable) {
-			this.modals['create'].handleClose();
-		}
-	}
-
-	renderCreateModal(modal) {
-
-	}
-
-	closeCreateModal() {
-
-		let modal = document.querySelector('dialog.create-modal');
-		window.removeChildren(modal);
-		modal.append(this.createModal);
-	}
-
-	handleCreateModalSave(changes) {
-
-		let id = this.modals.create.modal.querySelector('input[name="form-id"]').value;
-		let data = {};
-		if (!Object.hasOwn(data, id)) {
-			data[id] = {content: this.content};
-		}
-
-		for (let [field, value] of Object.entries(changes)) {
-			if (field === 'image_temp') {
-				continue;
-			}
-			data[id][field] = value;
-		}
-
-		this.saveData(data);
-	}
-
-	saveData(data) {
-		if (data.length === 0 || window.isEmptyObject(data)) {
-			return;
-		}
-
-		for (var [id, value] of Object.entries(data)) {
-			data[id].content = this.content;
-		}
-		let title = (data.length > 1) ? this.plural : this.single;
-		let operation = {
-			endpoint: 'content',
-			headers: {
-				'action_nonce': jvbSettings.dash,
-			},
-			title: `Adding ${data.length} ${title} to Queue`,
-			popup: `Queuing ${title}...`,
-			data: {
-				posts: data,
-			}
-		};
-
-		this.queue.addToQueue(operation);
-	}
-
-	handleBulkEdit(status) {
-		this.loading.showLoading('Processing bulk changes...');
-		try {
-			let posts = {};
-			this.selected.forEach(postID => {
-
-				posts[postID] = {
-					content: this.content,
-					status: status
-				};
-				if (['delete', 'trash', 'restore'].includes(status)) {
-					this.elements.grid.querySelector(`[data-id="${postID}"]`).remove();
-				}
-			});
-			this.saveData(posts);
-			this.clearSelection();
-		} catch (error) {
-			console.error('Bulk operation failed: ', error);
-		} finally {
-			this.loading.hideLoading();
-		}
-	}
-
-	handleDateRangeOpen() {
-
-	}
-
-	handleDateRangeClose() {
-		let start = this.elements.dateRange.querySelector('.date-start');
-		let end = this.elements.dateRange.querySelector('.date-end');
-		let select = this.elements.dateRange.querySelector('.month-select');
-	}
-
-
-	handleMonthSelect(e) {
-		const [year, month] = e.target.value.split('-');
-		if (year && month) {
-			const start = new Date(year, month - 1, 1);
-			const end = new Date(year, month, 0);
-			end.setHours(23, 59, 59, 999);
-
-			this.setDateFilter('custom', start, end);
-			this.modals.dateRange.handleClose();
-		}
-	}
-
-	setDateFilter(type, startDate = null, endDate = null) {
-		const now = new Date();
-		now.setHours(23, 59, 59, 999);
-
-		let start = startDate;
-		let end = endDate || now;
-
-		if (!startDate && type !== '') {
-			start = new Date();
-			switch (type) {
-				case 'today':
-					start.setHours(0, 0, 0, 0);
-					break;
-				case 'week':
-					start.setDate(now.getDate() - 7);
-					break;
-				case 'month':
-					start.setMonth(now.getMonth() - 1);
-					break;
-				case 'year':
-					start.setFullYear(now.getFullYear() - 1);
-					break;
-			}
-		}
-
-		this.filters.date = type ? {
-			range: {
-				after: start.toISOString(),
-				before: end.toISOString()
-			},
-			custom: type === 'custom'
-		} : {
-			range: null,
-			custom: false
-		};
-
-		this.updateClearFiltersButton();
-		this.page = 1;
-		this.loadContent(true);
-	}
-
-	updateClearFiltersButton() {
-		const hasFilters =
-			Object.keys(this.filters.taxonomies).length > 0 ||
-			this.filters.date.range !== null;
-
-		this.elements.clearButton.hidden = !hasFilters;
-	}
-
-	handleClearFilters() {
-		this.resetFilters(true);
-	}
-
-	/*****************************************************
-	 *
-	 * VIEW CONTROLS
-	 *
-	 *****************************************************/
-	/**
-	 * Initialize visual view (grid/list)
-	 */
-	initVisualView(viewName) {
-		// Visual views are initialized on first load
-		const view = this.views.get(viewName);
-		view.initialized = true;
-	}
-
-	/**
-	 * Activate visual view
-	 */
-	activateVisualView(viewName) {
-		const view = this.views.get(viewName);
-
-		// Remove all view classes
-		this.elements.grid.classList.remove('grid-view', 'list-view', 'table');
-
-		// Add specific view class
-		this.elements.grid.classList.add(view.containerClass);
-
-		// Hide table-specific controls
-		if (this.elements.columnsSelect) {
-			this.elements.columnsSelect.hidden = true;
-		}
-
-		return Promise.resolve();
-	}
-
-	/**
-	 * Deactivate visual view
-	 */
-	deactivateVisualView(viewName) {
-		// Clear rendered items if needed
-		return Promise.resolve();
-	}
-
-	/**
-	 * Initialize table view
-	 */
-	initTableView() {
-		const view = this.views.get('table');
-
-		if (view.initialized) {
-			return;
-		}
-
-		// Create table container structure
-		const tableTemplate = window.getTemplate('contentTable');
-		if (!tableTemplate) {
-			throw new Error('Table template not found');
-		}
-
-		view.tableContainer = tableTemplate;
-		view.initialized = true;
-	}
-
-	/**
-	 * Activate table view
-	 */
-	async activateTableView() {
-		const view = this.views.get('table');
-
-		// Initialize if needed
-		if (!view.initialized) {
-			this.initTableView();
-		}
-
-		// Remove visual view classes
-		this.elements.grid.classList.remove('grid-view', 'list-view');
-		this.elements.grid.classList.add('table');
-
-		// Insert table before grid
-		if (!this.elements.gridWrap.querySelector('form.table')) {
-			this.elements.gridWrap.insertBefore(
-				view.tableContainer.cloneNode(true),
-				this.elements.grid
-			);
-		}
-
-		// Get table reference
-		const table = this.elements.gridWrap.querySelector('form.table');
-
-		// Initialize the vertical navigation checkbox
-		const verticalCheckbox = table.querySelector('input#vertical');
-		if (verticalCheckbox) {
-			// Load saved preference
-			const savedPref = localStorage.getItem('jvbTabNav');
-			this.viewSettings.tabNav = savedPref === 'vertical';
-			verticalCheckbox.checked = this.viewSettings.tabNav;
-		}
-
-		// Register form with jvbForm
-		if (window.jvbForm && !view.tableForm) {
-			view.tableForm = window.jvbForm.registerForm(table, {
-				onSave: (data) => this.handleTableSave(data),
-				isRow: true,
-				content: this.config.content,
-				autoSave: true,
-				saveDelay: 3000
-			});
-		}
-
-		// Show column controls
-		if (this.elements.columnsSelect) {
-			this.elements.columnsSelect.hidden = false;
-		}
-
-		// Load saved column preferences
-		await this.loadTableColumns();
-
-		// Add keyboard navigation listeners
-		this.addTableListeners();
-
-		// Mark table view as active
-		this.isTable = true;
-	}
-
-
-	/**
-	 * Deactivate table view
-	 */
-	async deactivateTableView() {
-		const view = this.views.get('table');
-
-		// Remove table listeners
-		this.removeTableListeners();
-
-		// Unregister form
-		if (view.tableForm && window.jvbForm) {
-			window.jvbForm.removeForm(view.tableForm.id);
-			view.tableForm = null;
-		}
-
-		// Remove table from DOM
-		const table = this.elements.gridWrap.querySelector('form.table');
-		if (table) {
-			table.remove();
-		}
-
-		// Hide column controls
-		if (this.elements.columnsSelect) {
-			this.elements.columnsSelect.hidden = true;
-		}
-
-		// Remove table class
-		this.elements.grid.classList.remove('table');
-
-		// Mark table view as inactive
-		this.isTable = false;
-	}
-
-	/**
-	 * Cleanup table view (full cleanup)
-	 */
-	cleanupTableView() {
-		const view = this.views.get('table');
-		view.initialized = false;
-		view.tableContainer = null;
-		view.tableForm = null;
-	}
-
-	/**
-	 * Render items for visual views
-	 */
-	renderVisualItems(items, viewName, append) {
-		const view = this.views.get(viewName);
-		const template = view.template;
-
-		if (!append){
-			// Clear existing items
-			window.removeChildren(this.elements.grid);
-		}
-
-		// Batch render items
-		const fragment = document.createDocumentFragment();
-
-		items.forEach(item => {
-			const element = this.createVisualElement(item, template);
-			fragment.appendChild(element);
-		});
-
-		this.elements.grid.appendChild(fragment);
-
-		// Make items keyboard navigable
-		if (window.jvbA11y) {
-			window.jvbA11y.makeNavigable(
-				this.elements.grid.querySelectorAll('.item:not([data-keyboard-nav])')
-			);
-		}
-	}
-
-	/**
-	 * Render items for table view
-	 */
-	renderTableItems(items, append) {
-		const table = this.elements.gridWrap.querySelector('form.table tbody');
-		if (!table) return;
-
-		if (!append) {
-			// Clear existing rows
-			window.removeChildren(table);
-		}
-
-		// Batch render rows
-		const fragment = document.createDocumentFragment();
-
-		items.forEach(item => {
-			const row = this.createTableRow(item);
-			fragment.appendChild(row);
-		});
-
-		table.appendChild(fragment);
-
-		window.loadTemplates();
-
-		// Process form changes
-		const view = this.views.get('table');
-		if (view.tableForm && window.jvbForm) {
-			window.jvbForm.processChanges(view.tableForm, {processChanges: false});
-
-			// Scan for selectors and uploaders
-			window.jvbSelector?.scanExistingFields();
-			window.jvbUploadManager?.scanExistingFields();
-		}
-	}
-
-	/**
-	 * Create visual element (grid/list item)
-	 */
-	createVisualElement(item, templateName) {
-		const template = window.getTemplate(templateName);
-
-		// Set basic attributes
-		template.dataset.id = item.id;
-		template.dataset.img = item.thumbnail || '';
-
-		if (item.fields) {
-			template.dataset.fields = JSON.stringify(item.fields);
-		}
-
-		if (item.images) {
-			template.dataset.images = JSON.stringify(item.images);
-		}
-
-		if (item.status) {
-			template.classList.add(item.status);
-			template.dataset.status = item.status;
-		}
-
-		// Populate fields
-		this.populateItemFields(template, item);
-
-		return template;
-	}
-
-	/**
-	 * Create table row
-	 */
-	createTableRow(item) {
-		const row = window.getTemplate('tableView');
-
-		row.dataset.id = item.id;
-
-		// Update IDs for form handling
-		row.querySelectorAll('[id]').forEach(element => {
-			const label = element.nextElementSibling?.tagName === 'LABEL'
-				? element.nextElementSibling
-				: element.previousElementSibling?.tagName === 'LABEL'
-					? element.previousElementSibling
-					: null;
-
-			element.id = `${item.id}|${element.id}`;
-			element.name = `${item.id}|${element.name}`;
-
-			// let templates = element.querySelectorAll('template');
-			// if (templates) {
-			// 	templates.forEach(template => {
-			// 		template.className = template.className+item.id;
-			// 	})
-			// }
-
-			if (label) {
-				label.htmlFor = element.id;
-			}
-		});
-
-		// Set status
-		if (item.status) {
-			const statusRadio = row.querySelector(`[name="status"][value="${item.status}"]`);
-			if (statusRadio) {
-				statusRadio.checked = true;
-			}
-		}
-
-		// Populate field values
-		if (item.fields) {
-			row.querySelectorAll('.field').forEach(field => {
-				const fieldName = field.dataset.field;
-				if (fieldName && item.fields[fieldName] !== undefined) {
-					window.jvbForm?.populateFieldValue(
-						field,
-						`${item.id}|${fieldName}`,
-						item.fields[fieldName],
-						item.images
-					);
-				}
-			});
-		}
-
-		return row;
-	}
-
-	/**
-	 * Populate item fields for visual views
-	 */
-	populateItemFields(element, item) {
-		// Populate image
-		const img = element.querySelector('img');
-		if (img && item.thumbnail) {
-			img.src = item.thumbnail;
-			img.alt = item.fields?.post_title || `${this.config.content} image`;
-		}
-
-		// Populate field values
-		if (item.fields) {
-			Object.entries(item.fields).forEach(([fieldName, value]) => {
-				const fieldElement = element.querySelector(`.${fieldName}`);
-				if (fieldElement) {
-					if (fieldElement.classList.contains('images')) {
-						window.handleGalleryField?.(fieldElement, value);
-					} else if (fieldElement.querySelector('li')) {
-						window.handleListField?.(fieldElement, value);
-					} else {
-						window.handleTextField?.(fieldElement, value);
-					}
-				}
-			});
-		}
-
-		// Set checkbox values
-		const checkbox = element.querySelector('.select-checkbox');
-		if (checkbox) {
-			checkbox.id = `select-${item.id}`;
-			checkbox.name = `select-${item.id}`;
-			checkbox.value = item.id;
-
-			const label = checkbox.nextElementSibling;
-			if (label) {
-				label.htmlFor = checkbox.id;
-			}
-		}
-	}
-
-	/**
-	 * Handle column visibility changes
-	 */
-	handleColumnVisibility(event) {
-		const columnClass = event.target.id;
-		const table = this.elements.gridWrap.querySelector('form.table');
-
-		if (table) {
-			table.querySelectorAll(`.${columnClass}`).forEach(cell => {
-				cell.hidden = !event.target.checked;
-			});
-		}
-
-		this.saveViewSettings();
-	}
-
-	/**
-	 * Add table-specific listeners
-	 */
-	addTableListeners() {
-
-
-		// Tab navigation listener
-		this.tabListener = (e) => {
-			// Only handle tab in table view
-			if (this.view !== 'table') return;
-			// Check if we're in a table input/select/textarea
-			const isInTable = e.target.closest('form.table tbody');
-			if (!isInTable) return;
-
-			if (e.key === 'Tab' && this.viewSettings.tabNav) {
-				this.handleTableTabNavigation(e);
-			} else if ((e.ctrlKey || e.metaKey) && e.key === 'ArrowUp') {
-				e.preventDefault();
-				this.toggleTableNavDirection();
-			}
-		};
-
-		document.addEventListener('keydown', this.tabListener);
-	}
-
-	/**
-	 * Remove table-specific listeners
-	 */
-	removeTableListeners() {
-		if (this.tabListener) {
-			document.removeEventListener('keydown', this.tabListener);
-			this.tabListener = null;
-		}
-	}
-
-	/**
-	 * Handle table tab navigation
-	 */
-	handleTableTabNavigation(event) {
-		const table = this.elements.gridWrap.querySelector('form.table');
-		if (!table) return;
-
-		const currentElement = document.activeElement;
-		const currentCell = currentElement.closest('td');
-		if (!currentCell) return;
-
-		const currentRow = currentCell.closest('tr');
-		const rows = Array.from(table.querySelectorAll('tbody tr'));
-		const currentRowIndex = rows.indexOf(currentRow);
-
-		if (currentRowIndex === -1) return;
-
-		// Determine next row (up or down based on shift key)
-		const nextRowIndex = event.shiftKey ? currentRowIndex - 1 : currentRowIndex + 1;
-
-		// Check bounds
-		if (nextRowIndex < 0 || nextRowIndex >= rows.length) {
-			// Optionally, you could wrap around or stop
-			return;
-		}
-
-		const nextRow = rows[nextRowIndex];
-		if (!nextRow) return;
-
-		// Find the corresponding cell in the next row
-		const cells = Array.from(currentRow.querySelectorAll('td'));
-		const currentCellIndex = cells.indexOf(currentCell);
-
-		const nextRowCells = Array.from(nextRow.querySelectorAll('td'));
-		const nextCell = nextRowCells[currentCellIndex - 1];
-
-		if (!nextCell) return;
-
-		// Find the first focusable element in the next cell
-		const focusable = nextCell.querySelector('input, select, textarea, button');
-
-		if (focusable) {
-			// Smooth scroll to the next row
-			nextRow.scrollIntoView({
-				behavior: 'smooth',
-				block: 'nearest',
-				inline: 'nearest'
-			});
-
-			// Focus the element
-			focusable.focus();
-
-			// If it's a text input, select all text for easy editing
-			if (focusable.type === 'text' || focusable.type === 'number') {
-				focusable.select();
-			}
-		}
-	}
-
-	/**
-	 * Toggle table navigation direction
-	 */
-	toggleTableNavDirection() {
-		this.viewSettings.tabNav = !this.viewSettings.tabNav;
-
-		const message = this.viewSettings.tabNav
-			? 'Changed to vertical navigation'
-			: 'Changed to horizontal navigation';
-
-		window.jvbA11y?.announce(message);
-		this.saveViewSettings();
-	}
-
-	/**
-	 * Save view settings to cache
-	 */
-	async saveViewSettings() {
-		const settings = {
-			view: this.view,
-			tabNav: this.viewSettings.tabNav || false,
-			columns: []
-		};
-
-		// Save column visibility for table view
-		if (this.view === 'table') {
-			const checkedColumns = document.querySelectorAll('.multi-select input:checked');
-			settings.columns = Array.from(checkedColumns).map(input => input.id);
-		}
-
-		// Save to cache
-		if (window.jvbCache) {
-			await window.jvbCache.setItem(`${this.config.content}_view_settings`, settings);
-		}
-
-		// Also save to localStorage for quick access
-		localStorage.setItem(`${this.config.content}_view`, this.view);
-	}
-
-	/**
-	 * Load view settings from cache
-	 */
-	async loadViewSettings() {
-		// Try cache first
-		if (window.jvbCache) {
-			const cached = await window.jvbCache.getItem(`${this.config.content}_view_settings`);
-			if (cached) {
-				this.viewSettings = cached;
-				return cached.view || 'grid';
-			}
-		}
-
-		// Fallback to localStorage
-		const savedView = localStorage.getItem(`${this.config.content}_view`);
-		return savedView || 'grid';
-	}
-
-	/**
-	 * Load table column preferences
-	 */
-	async loadTableColumns() {
-		if (!this.viewSettings.columns || !this.viewSettings.columns.length) {
-			return;
-		}
-
-		const table = this.elements.gridWrap.querySelector('form.table');
-		if (!table) return;
-
-		// Apply saved column visibility
-		document.querySelectorAll('.multi-select input[type="checkbox"]').forEach(input => {
-			const isVisible = this.viewSettings.columns.includes(input.id);
-			input.checked = isVisible;
-
-			// Update column visibility
-			table.querySelectorAll(`.${input.id}`).forEach(cell => {
-				cell.hidden = !isVisible;
-			});
-		});
-	}
-
-	/**
-	 * Handle table save
-	 */
-	handleTableSave(changes) {
-		this.saveData(changes);
-	}
-
-	/**
-	 * Callback for view changes
-	 */
-	onViewChange(viewName) {
-		if (this.config.onViewChange) {
-			this.config.onViewChange(viewName);
-		}
-	}
-
-	/**
-	 * Get current view
-	 */
-	getCurrentView() {
-		return this.view;
-	}
-
-	/**
-	 * Get view object
-	 */
-	getView(viewName) {
-		return this.views.get(viewName);
-	}
-
-	/**
-	 * Check if current view is editable
-	 */
-	isEditableView() {
-		const view = this.views.get(this.view);
-		return view && view.type === 'editable';
-	}
-
-	/**
-	 * Render items based on current view
-	 */
-	renderItems(items, append = false) {
-		const view = this.views.get(this.view);
-		if (view && view.render) {
-			view.render(items, append);
-		}
-	}
-
-	/**
-	 * Add empty row (for table view)
-	 */
-	addEmptyRow() {
-		if (this.view !== 'table') {
-			return;
-		}
-
-		const table = this.elements.gridWrap.querySelector('form.table tbody');
-		if (!table) return;
-
-		// Remove empty state if present
-		this.elements.container.querySelector('.empty-state')?.remove();
-
-		// Create new row
-		const template = window.getTemplate('tableView');
-		const timestamp = new Date().getTime().toString(36);
-
-		template.dataset.id = `new-${timestamp}`;
-
-		// Update IDs for new row
-		template.querySelectorAll('[id]').forEach(element => {
-			const label = element.nextElementSibling?.tagName === 'LABEL'
-				? element.nextElementSibling
-				: element.previousElementSibling?.tagName === 'LABEL'
-					? element.previousElementSibling
-					: null;
-
-			element.id = `${timestamp}|${element.id}`;
-			element.name = `${timestamp}|${element.name}`;
-
-			if (label) {
-				label.htmlFor = element.id;
-			}
-		});
-
-		// Handle selectors
-		template.querySelectorAll('.jvb-selector')?.forEach(selector => {
-			selector.id = `${timestamp}-${selector.id}`;
-			const toggle = selector.querySelector('.taxonomy-toggle');
-			if (toggle && window.jvbSelector) {
-				window.jvbSelector.handleToggleClick(toggle, false);
-			}
-		});
-
-		table.appendChild(template);
-
-		// Focus first input
-		const firstInput = template.querySelector('input:not([type="checkbox"]), select, textarea');
-		if (firstInput) {
-			firstInput.focus();
-		}
-	}
-
-	cleanup() {
-		// Remove listeners
-		this.removeTableListeners();
-
-		// Cleanup views
-		this.views.forEach((view, name) => {
-			if (view.cleanup) {
-				view.cleanup();
-			}
-		});
-
-		// Clear references
-		this.views.clear();
-		this.elements = null;
-		this.config = null;
-
-
-		this.posts.clear();
-		this.selected.clear();
-
-		document.removeEventListener('click', this.clickHandler);
-		document.removeEventListener('change', this.changeHandler);
-		document.removeEventListener('keydown', this.keyHandler);
-		document.removeEventListener('keydown', this.handleEscape);
-		document.removeEventListener('keydown', this.tabListener);
-	}
-}
-
-window.crud = CRUD;
-window.addEventListener('beforeunload', () => window.crud?.cleanup());
diff --git a/assets/js/dash/Cache.js b/assets/js/dash/Cache.js
deleted file mode 100644
index 705579b..0000000
--- a/assets/js/dash/Cache.js
+++ /dev/null
@@ -1,1910 +0,0 @@
-/**
- * Central Cache Class
- * Provides caching for taxonomy terms, API responses and other data
- * with support for Browser Cache API (preferred) or localStorage fallback
- */
-class Cache {
-    constructor(options = {}) {
-
-        this.options = {
-            defaultTTL: 3600000, // 1 hour in milliseconds
-            namespace: 'jvb_cache_',
-            maxSize: 100,
-            ...options
-        };
-
-        // Check if Cache API is available
-        this.cacheAvailable = 'caches' in window;
-
-        if(!this.cacheAvailable){
-            console.warn('Browser Cache API unavailable, reverting to LocalStorage');
-        }
-
-        // Initialize memory cache
-        this._memoryCache = new Map();
-
-        // Server timestamps from wp_localize_script
-        this.cachedContent = JSON.parse(cacheJVB.cache) || {};
-        this.lastTimestampUpdate = Date.now();
-
-        // Setup timestamp refresh interval (every 5 minutes)
-        this._timestampInterval = setInterval(() => this.refreshTimestamps(), 300000);
-
-		// HTTP caching metadata storage
-		this.httpHeaders = new Map(); // Stores ETags and Last-Modified per URL
-		this.httpHeadersTTL = 24 * 60 * 60 * 1000; // 24 hours
-		this.httpHeadersKey = 'http_headers_cache';
-		this.headerSaveDebounce = null;
-		this.headerSaveDelay = 2000; // 2 seconds
-
-		// Load persisted HTTP headers on initialization
-		this.loadHttpHeaders();
-
-		// Clean up old headers periodically
-		this.startHeaderCleanup();
-
-		//For Image Uploads
-		this.dbName= 'jvb_image_cache';
-		this.dbVersion = 1;
-		this.imageStoreName = 'pending_uploads';
-		this.initIndexedDB();
-    }
-
-	/**
-	 *
-	 * 		Header stuffs
-	 *
-	 */
-	/**
-	 * Load HTTP headers from cache on initialization
-	 */
-	async loadHttpHeaders() {
-		try {
-			const savedHeaders = await this.getItem(this.httpHeadersKey, 'system');
-
-			if (savedHeaders instanceof Map) {
-				// Clean expired headers while loading
-				const now = Date.now();
-				for (const [key, headerData] of savedHeaders) {
-					if (this.isHeaderDataValid(headerData, now)) {
-						this.httpHeaders.set(key, headerData);
-					}
-				}
-				console.debug(`Loaded ${this.httpHeaders.size} cached HTTP headers`);
-			}
-		} catch (error) {
-			console.warn('Failed to load HTTP headers from cache:', error);
-			this.httpHeaders = new Map();
-		}
-	}
-
-	/**
-	 * Save HTTP headers to cache with debouncing
-	 */
-	saveHttpHeaders() {
-		// Debounce saves to avoid excessive writes
-		if (this.headerSaveDebounce) {
-			clearTimeout(this.headerSaveDebounce);
-		}
-
-		this.headerSaveDebounce = setTimeout(async () => {
-			try {
-				await this.setItem(this.httpHeadersKey, this.httpHeaders, 'system');
-				console.debug(`Saved ${this.httpHeaders.size} HTTP headers to cache`);
-			} catch (error) {
-				console.warn('Failed to save HTTP headers to cache:', error);
-			}
-		}, this.headerSaveDelay);
-	}
-
-	/**
-	 * Enhanced store HTTP headers with automatic persistence
-	 */
-	storeHttpHeaders(urlHeaderKey, response) {
-		const etag = response.headers.get('ETag');
-		const lastModified = response.headers.get('Last-Modified');
-		const cacheControl = response.headers.get('Cache-Control');
-
-		if (etag || lastModified) {
-			const headerData = {
-				etag,
-				lastModified,
-				cacheControl,
-				storedAt: Date.now(),
-				url: this.extractBaseUrl(urlHeaderKey) // For debugging/cleanup
-			};
-
-			this.httpHeaders.set(urlHeaderKey, headerData);
-
-			// Automatically save to persistent cache
-			this.saveHttpHeaders();
-
-			console.debug(`Stored HTTP headers for ${urlHeaderKey}:`, {
-				etag: etag ? etag.substring(0, 20) + '...' : null,
-				lastModified
-			});
-		}
-	}
-
-	/**
-	 * Check if header data is still valid (not expired)
-	 */
-	isHeaderDataValid(headerData, currentTime = Date.now()) {
-		if (!headerData || !headerData.storedAt) {
-			return false;
-		}
-
-		const age = currentTime - headerData.storedAt;
-		return age < this.httpHeadersTTL;
-	}
-
-	/**
-	 * Clean up expired HTTP headers
-	 */
-	cleanupExpiredHeaders() {
-		const now = Date.now();
-		let removedCount = 0;
-
-		for (const [key, headerData] of this.httpHeaders) {
-			if (!this.isHeaderDataValid(headerData, now)) {
-				this.httpHeaders.delete(key);
-				removedCount++;
-			}
-		}
-
-		if (removedCount > 0) {
-			console.debug(`Cleaned up ${removedCount} expired HTTP headers`);
-			this.saveHttpHeaders(); // Persist the cleanup
-		}
-
-		return removedCount;
-	}
-
-	/**
-	 * Start periodic cleanup of old headers
-	 */
-	startHeaderCleanup() {
-		// Clean up every hour
-		this.headerCleanupInterval = setInterval(() => {
-			this.cleanupExpiredHeaders();
-		}, 60 * 60 * 1000);
-
-		// Clean up when page becomes visible (user returns)
-		document.addEventListener('visibilitychange', () => {
-			if (!document.hidden) {
-				this.cleanupExpiredHeaders();
-			}
-		});
-	}
-
-	/**
-	 * Extract base URL from header key for debugging
-	 */
-	extractBaseUrl(urlHeaderKey) {
-		const parts = urlHeaderKey.split('|');
-		return parts[1] || urlHeaderKey; // URL is usually second part
-	}
-
-	/**
-	 * Enhanced Cache Deletion Methods
-	 * Add these methods to your existing Cache class
-	 */
-
-	/**
-	 * Completely clear all IndexedDB data
-	 * @returns {Promise<boolean>} Success status
-	 */
-	async clearIndexedDB() {
-		try {
-			if (!this.imageDB) {
-				console.log('IndexedDB not initialized, nothing to clear');
-				return true;
-			}
-
-			// Close the database connection first
-			this.imageDB.close();
-
-			// Delete the entire database
-			await new Promise((resolve, reject) => {
-				const deleteRequest = indexedDB.deleteDatabase(this.dbName);
-
-				deleteRequest.onsuccess = () => {
-					console.log(`IndexedDB "${this.dbName}" deleted successfully`);
-					resolve();
-				};
-
-				deleteRequest.onerror = () => {
-					console.error('Failed to delete IndexedDB:', deleteRequest.error);
-					reject(deleteRequest.error);
-				};
-
-				deleteRequest.onblocked = () => {
-					console.warn('IndexedDB deletion blocked - other tabs may be using it');
-					// Try to resolve anyway after a timeout
-					setTimeout(resolve, 2000);
-				};
-			});
-
-			// Reset the database reference
-			this.imageDB = null;
-
-			// Reinitialize if needed (optional)
-			// await this.initIndexedDB();
-
-			return true;
-		} catch (error) {
-			console.error('Error clearing IndexedDB:', error);
-			return false;
-		}
-	}
-
-	/**
-	 * Clear all memory cache
-	 * @returns {number} Number of items cleared
-	 */
-	clearMemoryCache() {
-		const count = this._memoryCache.size;
-		this._memoryCache.clear();
-
-		console.log(`Cleared ${count} items from memory cache`);
-		return count;
-	}
-
-	/**
-	 * Comprehensive cache clear - everything
-	 * @param {Object} options - Options for what to clear
-	 * @returns {Promise<Object>} Summary of what was cleared
-	 */
-	async clearAllCache(options = {}) {
-		const {
-			includeBrowserCache = true,
-			includeLocalStorage = true,
-			includeIndexedDB = true,
-			includeMemoryCache = true,
-			includeHttpHeaders = true,
-			showProgress = false
-		} = options;
-
-		const results = {
-			browserCache: false,
-			localStorage: 0,
-			indexedDB: false,
-			memoryCache: 0,
-			httpHeaders: 0,
-			errors: []
-		};
-
-		if (showProgress) {
-			console.log('Starting comprehensive cache clear...');
-		}
-
-		// Clear Browser Cache API
-		if (includeBrowserCache) {
-			try {
-				if (this.cacheAvailable) {
-					await caches.delete(this.options.namespace);
-					results.browserCache = true;
-					if (showProgress) console.log('✓ Browser Cache API cleared');
-				}
-			} catch (error) {
-				results.errors.push(`Browser Cache: ${error.message}`);
-				if (showProgress) console.error('✗ Browser Cache API error:', error);
-			}
-		}
-
-		// Clear localStorage
-		if (includeLocalStorage) {
-			try {
-				let cleared = 0;
-				for (let i = localStorage.length - 1; i >= 0; i--) {
-					const key = localStorage.key(i);
-					if (key && key.startsWith(this.options.namespace)) {
-						localStorage.removeItem(key);
-						cleared++;
-					}
-				}
-				results.localStorage = cleared;
-				if (showProgress) console.log(`✓ LocalStorage cleared: ${cleared} items`);
-			} catch (error) {
-				results.errors.push(`LocalStorage: ${error.message}`);
-				if (showProgress) console.error('✗ LocalStorage error:', error);
-			}
-		}
-
-		// Clear IndexedDB
-		if (includeIndexedDB) {
-			try {
-				results.indexedDB = await this.clearIndexedDB();
-				if (showProgress) console.log('✓ IndexedDB cleared');
-			} catch (error) {
-				results.errors.push(`IndexedDB: ${error.message}`);
-				if (showProgress) console.error('✗ IndexedDB error:', error);
-			}
-		}
-
-		// Clear Memory Cache
-		if (includeMemoryCache) {
-			try {
-				results.memoryCache = this.clearMemoryCache();
-				if (showProgress) console.log(`✓ Memory cache cleared: ${results.memoryCache} items`);
-			} catch (error) {
-				results.errors.push(`Memory Cache: ${error.message}`);
-				if (showProgress) console.error('✗ Memory cache error:', error);
-			}
-		}
-
-		// Clear HTTP Headers
-		if (includeHttpHeaders) {
-			try {
-				results.httpHeaders = this.clearHttpHeaders();
-				if (showProgress) console.log(`✓ HTTP headers cleared: ${results.httpHeaders} items`);
-			} catch (error) {
-				results.errors.push(`HTTP Headers: ${error.message}`);
-				if (showProgress) console.error('✗ HTTP headers error:', error);
-			}
-		}
-
-		// Clean up intervals and timeouts
-		this.destroy();
-
-		if (showProgress) {
-			console.log('Cache clear completed:', results);
-		}
-
-		return results;
-	}
-
-	/**
-	 * Clear specific IndexedDB stores without deleting the entire database
-	 * @param {Array<string>} storeNames - Array of store names to clear
-	 * @returns {Promise<number>} Number of stores cleared
-	 */
-	async clearIndexedDBStores(storeNames = []) {
-		if (!this.imageDB) {
-			console.log('IndexedDB not available');
-			return 0;
-		}
-
-		// Default to all known stores if none specified
-		if (storeNames.length === 0) {
-			storeNames = [this.imageStoreName, 'groups'];
-		}
-
-		let clearedCount = 0;
-
-		try {
-			const transaction = this.imageDB.transaction(storeNames, 'readwrite');
-
-			for (const storeName of storeNames) {
-				try {
-					const store = transaction.objectStore(storeName);
-					await new Promise((resolve, reject) => {
-						const clearRequest = store.clear();
-						clearRequest.onsuccess = () => {
-							console.log(`Cleared IndexedDB store: ${storeName}`);
-							clearedCount++;
-							resolve();
-						};
-						clearRequest.onerror = () => reject(clearRequest.error);
-					});
-				} catch (error) {
-					console.warn(`Failed to clear store ${storeName}:`, error);
-				}
-			}
-		} catch (error) {
-			console.error('Error clearing IndexedDB stores:', error);
-		}
-
-		return clearedCount;
-	}
-	/**
-	 * Clear all HTTP headers cache
-	 * @returns {number} Number of headers cleared
-	 */
-	clearHttpHeaders() {
-		const count = this.httpHeaders.size;
-		this.httpHeaders.clear();
-
-		// Force save the empty headers cache
-		this.saveHttpHeaders();
-
-		console.log(`Cleared ${count} HTTP headers from cache`);
-		return count;
-	}
-
-	/**
-	 * Get HTTP cache statistics
-	 */
-	getHttpCacheStats() {
-		const now = Date.now();
-		let validHeaders = 0;
-		let expiredHeaders = 0;
-		const urlCounts = {};
-
-		for (const [key, headerData] of this.httpHeaders) {
-			if (this.isHeaderDataValid(headerData, now)) {
-				validHeaders++;
-				const baseUrl = this.extractBaseUrl(key);
-				urlCounts[baseUrl] = (urlCounts[baseUrl] || 0) + 1;
-			} else {
-				expiredHeaders++;
-			}
-		}
-
-		return {
-			total: this.httpHeaders.size,
-			valid: validHeaders,
-			expired: expiredHeaders,
-			urlBreakdown: urlCounts,
-			memoryUsage: this.estimateHttpHeadersMemoryUsage()
-		};
-	}
-
-	/**
-	 * Estimate memory usage of HTTP headers cache
-	 */
-	estimateHttpHeadersMemoryUsage() {
-		let totalSize = 0;
-
-		for (const [key, headerData] of this.httpHeaders) {
-			// Rough estimate: key + JSON representation of value
-			totalSize += key.length * 2; // Unicode characters
-			totalSize += JSON.stringify(headerData).length * 2;
-		}
-
-		return {
-			bytes: totalSize,
-			kb: Math.round(totalSize / 1024 * 100) / 100,
-			entries: this.httpHeaders.size
-		};
-	}
-
-	/**
-	 * Force save HTTP headers (useful for page unload)
-	 */
-	async forceHttpHeadersSave() {
-		if (this.headerSaveDebounce) {
-			clearTimeout(this.headerSaveDebounce);
-			this.headerSaveDebounce = null;
-		}
-
-		try {
-			await this.setItem(this.httpHeadersKey, this.httpHeaders, 'system');
-			console.debug('Force saved HTTP headers');
-		} catch (error) {
-			console.warn('Failed to force save HTTP headers:', error);
-		}
-	}
-
-	/**
-	 * Fetch with comprehensive HTTP caching support
-	 *
-	 * @param {string} url - The API URL to fetch
-	 * @param {object} fetchOptions - Options for the fetch request
-	 * @param {object} cacheOptions - Options for caching behavior
-	 * @returns {Promise<any>} - The response data
-	 */
-	async fetchWithCache(url, fetchOptions = {}, cacheOptions = {}) {
-
-		// Extract cache-specific options with defaults
-		let {
-			maxAge = this.options.defaultTTL,
-			forceRefresh = false,
-			content = '',
-			timeout = 30000,
-		} = cacheOptions;
-
-		// forceRefresh = true;
-		const cacheKey = this.generateCacheKey(url, fetchOptions);
-
-		if (content || (content = this.detectContentType(url)),
-		!forceRefresh && !this.cacheUpdated(content)) {
-
-			const cachedItem = await this.getCacheItem(cacheKey);
-			if (cachedItem && this.isCacheItemValid(cachedItem, maxAge)) {
-				console.debug(`Cache hit for ${url} (content: ${content})`);
-				return this.deserializeData(cachedItem.data, cachedItem.dataType);
-			}
-		}
-
-		// Prepare headers with caching directives
-		const headers = { ...fetchOptions.headers };
-		const headerKey = this.getUrlHeaderKey(url, fetchOptions);
-		const cachedHeaders = this.httpHeaders.get(headerKey);
-
-		if (!forceRefresh && cachedHeaders?.etag) {
-			headers["If-None-Match"] = cachedHeaders.etag;
-		}
-		if (!forceRefresh && cachedHeaders?.lastModified) {
-			headers["If-Modified-Since"] = cachedHeaders.lastModified;
-		}
-		if (!forceRefresh && this.lastTimestampUpdate) {
-			headers["X-Client-Cache-Timestamp"] = new Date(this.lastTimestampUpdate).toISOString();
-		}
-
-		// Create the fetch function that log can retry
-		const performFetch = async () => {
-			// Set up timeout
-			const controller = new AbortController();
-			const timeoutId = setTimeout(() => controller.abort(), timeout);
-
-			try {
-				const response = await fetch(url, {
-					...fetchOptions,
-					headers,
-					signal: controller.signal
-				});
-
-				clearTimeout(timeoutId);
-
-				// Handle 304 Not Modified
-				if (response.status === 304) {
-					console.debug(`304 Not Modified for ${url}`);
-					const cachedItem = await this.getCacheItem(cacheKey);
-					if (cachedItem) {
-						// Update timestamp but keep existing data
-						cachedItem.timestamp = Date.now();
-						await this.setCacheItem(cacheKey, cachedItem);
-						return this.deserializeData(cachedItem.data, cachedItem.dataType);
-					} else {
-						console.warn(`Received 304 but no cached data for ${url}`);
-						return null;
-					}
-				}
-
-				if (!response.ok) {
-					throw new Error(`HTTP ${response.status}: ${response.statusText}`);
-				}
-
-				// Store HTTP headers for future caching
-				this.storeHttpHeaders(headerKey, response);
-
-				// Parse and cache the response
-				const data = await response.json();
-				const serializedData = this.serializeData(data);
-
-				const cacheItem = {
-					data: serializedData.data,
-					dataType: serializedData.type,
-					timestamp: Date.now(),
-					content,
-					url,
-					httpStatus: response.status,
-					etag: response.headers.get("ETag"),
-					lastModified: response.headers.get("Last-Modified")
-				};
-
-				await this.setCacheItem(cacheKey, cacheItem);
-				if (content !== "") {
-					this.updateContentCache(content);
-				}
-
-				console.debug(`Fetched and cached ${url} (content: ${content})`);
-				return data;
-
-			} catch (error) {
-				clearTimeout(timeoutId);
-				throw error;
-			}
-		};
-
-		// Use the centralized error handling system
-		try {
-			return await performFetch();
-		} catch (error) {
-			// Check for stale data fallback before using error handler
-			if (error.name === "AbortError" || error.message?.includes("timeout")) {
-				const staleData = await this.getStaleDataFallback(cacheKey);
-				if (staleData) {
-					console.warn(`Returning stale data for ${url} due to timeout`);
-					return staleData;
-				}
-			}
-
-			// Use centralized error handling with retry capability
-			if (window.jvbError) {
-				const result = await window.jvbError.log(error, {
-					component: "CacheManager",
-					action: "fetchWithCache",
-					url: url,
-					content: content,
-					timeout: timeout
-				}, performFetch); // Pass the fetch function for retries
-
-				// If log returns a result from retry, use it
-				if (result && result.success !== false) {
-					return result;
-				}
-			}
-
-			// If error handling didn't resolve it, try stale data as final fallback
-			const staleData = await this.getStaleDataFallback(cacheKey);
-			if (staleData) {
-				console.warn(`Returning stale data for ${url} due to error:`, error.message);
-				return staleData;
-			}
-
-			// Re-throw the error if no fallbacks work
-			throw error;
-		}
-	}
-
-	detectContentType(url) {
-		const patterns = {
-			'queue': /\/queue/,
-			'notifications': /\/notifications/,
-			'posts': /\/(posts|content)/,
-			'users': /\/users/,
-			'taxonomy': /\/(terms|taxonomies)/,
-			'media': /\/(media|images|files)/,
-			'artists': /\/artists/,
-			'shops': /\/shops/,
-			'events': /\/events/
-		};
-
-		for (const [type, pattern] of Object.entries(patterns)) {
-			if (pattern.test(url)) {
-				return type;
-			}
-		}
-
-		return '';
-	}
-
-	/**
-	 * Generate a unique key for storing HTTP headers per URL/method combination
-	 */
-	getUrlHeaderKey(url, fetchOptions) {
-		const method = fetchOptions.method || 'GET';
-
-		// Include relevant parts of the request that affect caching
-		let keyComponents = [method, url];
-
-		// For POST/PUT requests, include body hash if it affects response
-		if (method !== 'GET' && fetchOptions.body) {
-			try {
-				const bodyHash = this.hashString(JSON.stringify(fetchOptions.body));
-				keyComponents.push(bodyHash);
-			} catch (e) {
-				// If body isn't JSON, use string representation
-				keyComponents.push(this.hashString(String(fetchOptions.body)));
-			}
-		}
-
-		return keyComponents.join('|');
-	}
-
-	/**
-	 * Check if cache item is still valid based on age
-	 */
-	isCacheItemValid(cacheItem, maxAge) {
-		if (!cacheItem || !cacheItem.timestamp) {
-			return false;
-		}
-
-		const age = Date.now() - cacheItem.timestamp;
-		return age < maxAge;
-	}
-
-
-	/**
-	 * Get stale cached data as fallback when fresh fetch fails
-	 */
-	async getStaleDataFallback(cacheKey) {
-		try {
-			const cachedItem = await this.getCacheItem(cacheKey);
-			if (cachedItem) {
-				console.debug('Using stale cache data as fallback');
-				return this.deserializeData(cachedItem.data, cachedItem.dataType);
-			}
-		} catch (error) {
-			console.warn('Failed to retrieve stale cache data:', error);
-		}
-		return null;
-	}
-
-	/**
-	 * Simple string hashing for cache keys
-	 */
-	hashString(str) {
-		let hash = 0;
-		if (str.length === 0) return hash;
-
-		for (let i = 0; i < str.length; i++) {
-			const char = str.charCodeAt(i);
-			hash = ((hash << 5) - hash) + char;
-			hash = hash & hash; // Convert to 32-bit integer
-		}
-
-		return hash.toString(36);
-	}
-
-    cacheUpdated(content){
-        if(!content || !this.cachedContent) return true;
-
-        if(!this.cachedContent[content]) return true;
-
-        const key = `${this.options.namespace}_cached_${content}`;
-        let localCache = null;
-
-        //Check memory cache first
-        if (this._memoryCache && this._memoryCache.has(key)) {
-            localCache = this._memoryCache.get(key);
-        } else {
-            // Check persistent storage
-            localCache = this.cacheAvailable
-                ? this.getBrowserCacheItem(key)
-                : this.getLocalStorageItem(key);
-        }
-
-        if(!localCache) return true;
-
-        return this.cachedContent[content] > localCache;
-    }
-
-    /**
-     * Update the timestamp for a content type
-     * @param {string} content - The content type to update
-     */
-    updateContentCache(content) {
-        if (!content) return;
-
-        const timestamp = Date.now();
-        const key = `${this.options.namespace}_cached_${content}`;
-
-        // Update memory cache
-        this._memoryCache.set(key, timestamp);
-
-        // Update persistent storage
-        if (this.cacheAvailable) {
-            this.setBrowserCacheItem(key, timestamp);
-        } else {
-            this.setLocalStorageItem(key, timestamp);
-        }
-    }
-
-	/**
-	 * Clear HTTP headers for specific URLs (useful for cache invalidation)
-	 */
-	clearHttpHeaderPattern(urlPattern) {
-		for (const [key, value] of this.httpHeaders.entries()) {
-			if (key.includes(urlPattern)) {
-				this.httpHeaders.delete(key);
-			}
-		}
-	}
-
-	/**
-	 *
-	 *
-	 * Custom caches
-	 *
-	 *
-	 */
-
-	/**
-	 * Enhanced setItem with automatic type preservation
-	 */
-	async setItem(key, data, content = '') {
-		const cacheKey = (content === '') ?
-			this.options.namespace + key :
-			this.options.namespace + content + ':' + key;
-
-		// Serialize data with type preservation
-		const serializedData = this.serializeData(data);
-
-		const cacheItem = {
-			data: serializedData.data,
-			dataType: serializedData.type,
-			timestamp: Date.now(),
-			content
-		};
-
-		await this.setCacheItem(cacheKey, cacheItem);
-
-		// Update timestamp for the content type
-		if (content !== '') {
-			this.updateContentCache(content);
-		}
-	}
-
-	/**
-	 * Enhanced getItem with automatic type restoration
-	 */
-	async getItem(key, content = '') {
-		const cacheKey = (content === '') ?
-			this.options.namespace + key :
-			this.options.namespace + content + ':' + key;
-
-		const cachedItem = await this.getCacheItem(cacheKey);
-
-		if (!cachedItem) {
-			return null;
-		}
-
-		// Deserialize data back to original type
-		return this.deserializeData(cachedItem.data, cachedItem.dataType);
-	}
-
-	serializeData(data) {
-		// Handle null/undefined
-		if (data === null || data === undefined) {
-			return { data, type: 'primitive' };
-		}
-
-		// Handle Maps
-		if (data instanceof Map) {
-			return {
-				data: Array.from(data.entries()),
-				type: 'Map'
-			};
-		}
-
-		// Handle Sets
-		if (data instanceof Set) {
-			return {
-				data: Array.from(data),
-				type: 'Set'
-			};
-		}
-
-		// Handle Dates
-		if (data instanceof Date) {
-			return {
-				data: data.toISOString(),
-				type: 'Date'
-			};
-		}
-
-		// Handle RegExp
-		if (data instanceof RegExp) {
-			return {
-				data: { source: data.source, flags: data.flags },
-				type: 'RegExp'
-			};
-		}
-
-		// Handle Arrays (check before general objects)
-		if (Array.isArray(data)) {
-			// Recursively serialize array elements that might contain Maps/Sets
-			return {
-				data: data.map(item => this.serializeData(item)),
-				type: 'Array'
-			};
-		}
-
-		// Handle plain objects
-		if (data && typeof data === 'object' && data.constructor === Object) {
-			const serializedObj = {};
-			for (const [key, value] of Object.entries(data)) {
-				serializedObj[key] = this.serializeData(value);
-			}
-			return {
-				data: serializedObj,
-				type: 'Object'
-			};
-		}
-
-		// Handle primitives (string, number, boolean)
-		return { data, type: 'primitive' };
-	}
-
-	deserializeData(data, type) {
-		if (!type || type === 'primitive') {
-			return data;
-		}
-
-		switch (type) {
-			case 'Map':
-				return new Map(data);
-
-			case 'Set':
-				return new Set(data);
-
-			case 'Date':
-				return new Date(data);
-
-			case 'RegExp':
-				return new RegExp(data.source, data.flags);
-
-			case 'Array':
-				// Recursively deserialize array elements
-				return data.map(item =>
-					this.deserializeData(item.data, item.type)
-				);
-
-			case 'Object':
-				const restoredObj = {};
-				for (const [key, value] of Object.entries(data)) {
-					restoredObj[key] = this.deserializeData(value.data, value.type);
-				}
-				return restoredObj;
-
-			default:
-				console.warn(`Unknown data type: ${type}, returning as-is`);
-				return data;
-		}
-	}
-
-	async setMap(key, map, content = '') {
-		if (!(map instanceof Map)) {
-			throw new Error('setMap requires a Map instance');
-		}
-		return this.setItem(key, map, content);
-	}
-
-	async getMap(key, content = '') {
-		const result = await this.getItem(key, content);
-
-		if (result === null) {
-			return null;
-		}
-
-		if (!(result instanceof Map)) {
-			console.warn(`Expected Map but got ${typeof result} for key: ${key}`);
-			return null;
-		}
-
-		return result;
-	}
-
-    /**
-     * Refreshes server timestamps from the API
-     *
-     * @returns {Promise<void>}
-     */
-    async refreshTimestamps() {
-        try {
-            const response = await fetch(`${jvbSettings.api}cachedContent`, {
-                method: 'GET',
-                headers: {
-                    'If-Modified-Since': this.lastTimestampUpdate ?
-                        new Date(this.lastTimestampUpdate).toUTCString() : ''
-                }
-            });
-
-            if (response.status === 304) {
-                // Not modified, nothing to do
-                return;
-            }
-
-            if (response.ok) {
-                this.cachedContent = await response.json();
-                this.lastTimestampUpdate = Date.now();
-
-                // Invalidate affected caches
-                await this.invalidateAffectedCaches();
-            }
-        } catch (error) {
-            console.warn('Failed to refresh cache timestamps:', error);
-        }
-    }
-
-    /**
-     * Main method to get an item from cache
-     * Tries Browser Cache API first, falls back to localStorage
-     *
-     * @param {string} key - Cache key
-     * @returns {Promise<any>} - Cached item or null
-     */
-    async getCacheItem(key) {
-        // Check memory cache first (ultra fast)
-        if (this._memoryCache.has(key)) {
-            return this._memoryCache.get(key);
-        }
-
-        // Then check persistent cache
-        const item = this.cacheAvailable ?
-            await this.getBrowserCacheItem(key) :
-            this.getLocalStorageItem(key);
-
-        // Store in memory cache if found
-        if (item) {
-            this._memoryCache.set(key, item);
-        }
-
-        return item;
-    }
-
-    /**
-     * Main method to set an item in cache
-     * Uses Browser Cache API if available, falls back to localStorage
-     *
-     * @param {string} key - Cache key
-     * @param {any} item - Item to cache
-     * @returns {Promise<void>}
-     */
-    async setCacheItem(key, item) {
-        // Always store in memory cache
-        this._memoryCache.set(key, item);
-
-        return this.cacheAvailable ?
-            await this.setBrowserCacheItem(key, item) :
-            this.setLocalStorageItem(key, item);
-    }
-
-    /**
-     * Remove an item from cache
-     *
-     * @param {string} key - Cache key
-     * @returns {Promise<void>}
-     */
-    async removeCacheItem(key) {
-        // Remove from memory cache
-        this._memoryCache.delete(key);
-
-        return this.cacheAvailable ?
-            await this.removeBrowserCacheItem(key) :
-            this.removeLocalStorageItem(key);
-    }
-
-    /**
-     * Get item from Browser Cache API
-     *
-     * @param {string} key - Cache key
-     * @returns {Promise<any>} - Cached item or null
-     */
-    async getBrowserCacheItem(key) {
-        try {
-            const cache = await caches.open(this.options.namespace);
-            const response = await cache.match(key);
-
-            if (!response) {
-                return null;
-            }
-
-            return await response.json();
-        } catch (error) {
-            console.warn('Error getting from Browser Cache API:', error);
-            return null;
-        }
-    }
-
-    /**
-     * Set item in Browser Cache API
-     *
-     * @param {string} key - Cache key
-     * @param {any} item - Item to cache
-     * @returns {Promise<void>}
-     */
-    async setBrowserCacheItem(key, item) {
-        try {
-            const cache = await caches.open(this.options.namespace);
-            const response = new Response(JSON.stringify(item), {
-                headers: { 'Content-Type': 'application/json' }
-            });
-
-            await cache.put(key, response);
-        } catch (error) {
-            console.warn('Error setting in Browser Cache API:', error);
-        }
-    }
-
-    /**
-	 * Remove item from Browser Cache API
-	 *
-	 * @param {string} key - Cache key
-	 * @returns {Promise<void>}
-	 */
-	async removeBrowserCacheItem(key) {
-		try {
-			const cache = await caches.open(this.options.namespace);
-			await cache.delete(key);
-		} catch (error) {
-			console.warn('Error removing from Browser Cache API:', error);
-		}
-	}
-
-	/**
-	 * Get item from localStorage
-	 *
-	 * @param {string} key - Cache key
-	 * @returns {object|null} - Cached item or null
-	 */
-	getLocalStorageItem(key) {
-		try {
-			const stored = localStorage.getItem(key);
-
-			if (!stored) {
-				return null;
-			}
-
-			return JSON.parse(stored);
-		} catch (error) {
-			console.warn('Error getting from localStorage:', error);
-			return null;
-		}
-	}
-
-	/**
-	 * Set item in localStorage
-	 *
-	 * @param {string} key - Cache key
-	 * @param {any} item - Item to cache
-	 */
-	setLocalStorageItem(key, item) {
-		try {
-			localStorage.setItem(key, JSON.stringify(item));
-		} catch (error) {
-			// Handle quota exceeded
-			if (error instanceof DOMException && error.code === 22) {
-				this.clearOldestLocalStorageItems();
-				try {
-					localStorage.setItem(key, JSON.stringify(item));
-				} catch (retryError) {
-					console.warn('Still failed to set localStorage item after cleanup:', retryError);
-				}
-			} else {
-				console.warn('Error setting localStorage item:', error);
-			}
-		}
-	}
-
-	/**
-	 * Remove item from localStorage
-	 *
-	 * @param {string} key - Cache key
-	 */
-	removeLocalStorageItem(key) {
-		try {
-			localStorage.removeItem(key);
-		} catch (error) {
-			console.warn('Error removing localStorage item:', error);
-		}
-	}
-
-	/**
-	 * Clear oldest items from localStorage when quota is exceeded
-	 */
-	clearOldestLocalStorageItems() {
-		try {
-			const keysToRemove = [];
-
-			// Find all our cache keys
-			for (let i = 0; i < localStorage.length; i++) {
-				const key = localStorage.key(i);
-				if (key.startsWith(this.options.namespace)) {
-					try {
-						const item = JSON.parse(localStorage.getItem(key));
-						keysToRemove.push({ key, timestamp: item.timestamp || 0 });
-					} catch (e) {
-						// If it's not valid JSON or doesn't have a timestamp, prioritize for removal
-						keysToRemove.push({ key, timestamp: 0 });
-					}
-				}
-			}
-
-			// Sort by timestamp (oldest first)
-			keysToRemove.sort((a, b) => a.timestamp - b.timestamp);
-
-			// Remove the oldest 20% of items
-			const removeCount = Math.max(1, Math.ceil(keysToRemove.length * 0.2));
-			for (let i = 0; i < removeCount; i++) {
-				if (keysToRemove[i]) {
-					localStorage.removeItem(keysToRemove[i].key);
-				}
-			}
-		} catch (error) {
-			console.warn('Error cleaning up localStorage:', error);
-		}
-	}
-
-	/**
-	 * Initialize IndexedDB for image storage
-	 */
-	async initIndexedDB() {
-		try {
-			this.imageDB = await new Promise((resolve, reject) => {
-				const request = indexedDB.open(this.dbName, 2); // Increment version
-
-				request.onerror = () => reject(request.error);
-				request.onsuccess = () => resolve(request.result);
-
-				request.onupgradeneeded = (event) => {
-					const db = event.target.result;
-
-					// Create pending uploads store
-					if (!db.objectStoreNames.contains(this.imageStoreName)) {
-						const store = db.createObjectStore(this.imageStoreName, { keyPath: 'id' });
-						store.createIndex('operationId', 'operationId', { unique: false });
-						store.createIndex('timestamp', 'timestamp', { unique: false });
-						store.createIndex('fieldId', 'fieldId', { unique: false });
-					}
-
-					// Create groups store
-					if (!db.objectStoreNames.contains('groups')) {
-						const groupStore = db.createObjectStore('groups', { keyPath: 'id' });
-						groupStore.createIndex('fieldId', 'fieldId', { unique: false });
-						groupStore.createIndex('timestamp', 'timestamp', { unique: false });
-					}
-				};
-			});
-
-			console.debug('IndexedDB initialized with group support');
-		} catch (error) {
-			console.warn('Failed to initialize IndexedDB:', error);
-			this.imageDB = null;
-		}
-	}
-
-	/**
-	 * Store group data in IndexedDB
-	 */
-	async storeGroupData(fieldId, groupId, groupData) {
-		if (!this.imageDB) {
-			// Fallback to memory cache
-			this._memoryCache.set(`group_${fieldId}_${groupId}`, groupData);
-			return true;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction(['groups'], 'readwrite');
-			const store = transaction.objectStore('groups');
-
-			const groupRecord = {
-				id: `${fieldId}_${groupId}`,
-				fieldId,
-				groupId,
-				...groupData,
-				timestamp: Date.now()
-			};
-
-			await new Promise((resolve, reject) => {
-				const request = store.put(groupRecord);
-				request.onsuccess = () => resolve();
-				request.onerror = () => reject(request.error);
-			});
-
-			return true;
-		} catch (error) {
-			console.error('Failed to store group data:', error);
-			return false;
-		}
-	}
-
-	/**
-	 * Retrieve group data for a field
-	 */
-	async getGroupsForField(fieldId) {
-		// Check memory cache first
-		const memoryResults = [];
-		for (const [key, value] of this._memoryCache.entries()) {
-			if (key.startsWith(`group_${fieldId}_`)) {
-				memoryResults.push(value);
-			}
-		}
-
-		if (memoryResults.length > 0 || !this.imageDB) {
-			return memoryResults;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction(['groups'], 'readonly');
-			const store = transaction.objectStore('groups');
-			const index = store.index('fieldId');
-
-			return await new Promise((resolve, reject) => {
-				const request = index.getAll(fieldId);
-				request.onsuccess = () => resolve(request.result || []);
-				request.onerror = () => reject(request.error);
-			});
-		} catch (error) {
-			console.error('Failed to retrieve groups for field:', error);
-			return [];
-		}
-	}
-
-	/**
-	 * Store pending image in IndexedDB
-	 * @param {string} id - Unique identifier for the image
-	 * @param {File|Blob} imageData - The image file/blob
-	 * @param {Object} metadata - Associated metadata
-	 * @param {string} operationId - Queue operation ID
-	 * @returns {Promise<boolean>} Success status
-	 */
-	async storeImagePending(id, imageData, metadata = {}, operationId = null) {
-		if (!this.imageDB) {
-			console.warn('IndexedDB not available, falling back to memory cache');
-			this._memoryCache.set(`pending_image_${id}`, {
-				imageData,
-				metadata,
-				operationId,
-				groupInfo: metadata.groupInfo || null
-			});
-			return true;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction([this.imageStoreName], 'readwrite');
-			const store = transaction.objectStore(this.imageStoreName);
-
-			const imageRecord = {
-				id,
-				imageData,
-				metadata,
-				operationId,
-				fieldId: metadata.uploadConfig?.fieldId,
-				groupInfo: metadata.groupInfo || null,
-				timestamp: Date.now(),
-				status: 'pending'
-			};
-
-			await new Promise((resolve, reject) => {
-				const request = store.put(imageRecord);
-				request.onsuccess = () => resolve();
-				request.onerror = () => reject(request.error);
-			});
-
-			return true;
-		} catch (error) {
-			console.error('Failed to store pending image:', error);
-			return false;
-		}
-	}
-
-
-
-	/**
-	 * Retrieve pending image from IndexedDB
-	 * @param {string} id - Image identifier
-	 * @returns {Promise<Object|null>} Image record or null
-	 */
-	async getImagePending(id) {
-		// Check memory cache first
-		const memoryResult = this._memoryCache.get(`pending_image_${id}`);
-		if (memoryResult) {
-			return { id, ...memoryResult };
-		}
-
-		if (!this.imageDB) {
-			return null;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction([this.imageStoreName], 'readonly');
-			const store = transaction.objectStore(this.imageStoreName);
-
-			return await new Promise((resolve, reject) => {
-				const request = store.get(id);
-				request.onsuccess = () => resolve(request.result || null);
-				request.onerror = () => reject(request.error);
-			});
-		} catch (error) {
-			console.error('Failed to retrieve pending image:', error);
-			return null;
-		}
-	}
-
-	/**
-	 * Get all pending images for an operation
-	 * @param {string} operationId - Queue operation ID
-	 * @returns {Promise<Array>} Array of image records
-	 */
-	async getImagesPendingByOperation() {
-		const results = [];
-
-		// Check memory cache first
-		for (const [key, value] of this._memoryCache.entries()) {
-			if (key.startsWith('pending_image_')) {
-				results.push({
-					id: key.replace('pending_image_', ''),
-					...value
-				});
-			}
-		}
-
-		if (results.length > 0 || !this.imageDB) {
-			return results;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction([this.imageStoreName], 'readonly');
-			const store = transaction.objectStore(this.imageStoreName);
-
-			return await new Promise((resolve, reject) => {
-				const request = store.getAll();
-				request.onsuccess = () => resolve(request.result || []);
-				request.onerror = () => reject(request.error);
-			});
-		} catch (error) {
-			console.error('Failed to retrieve pending images:', error);
-			return [];
-		}
-	}
-
-	async getImagesPendingByField(fieldId) {
-		if (!this.imageDB) {
-			// Fallback to memory cache
-			const results = [];
-			for (const [key, value] of this._memoryCache.entries()) {
-				if (key.startsWith('pending_image_') &&
-					value.metadata?.uploadConfig?.fieldId === fieldId) {
-					results.push({
-						id: key.replace('pending_image_', ''),
-						...value
-					});
-				}
-			}
-			return results;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction([this.imageStoreName], 'readonly');
-			const store = transaction.objectStore(this.imageStoreName);
-			const index = store.index('fieldId');
-
-			return await new Promise((resolve, reject) => {
-				const request = index.getAll(fieldId);
-				request.onsuccess = () => resolve(request.result || []);
-				request.onerror = () => reject(request.error);
-			});
-		} catch (error) {
-			console.error('Failed to retrieve pending images by field:', error);
-			return [];
-		}
-	}
-
-	/**
-	 * Clear group data for a field
-	 */
-	async clearGroupsForField(fieldId) {
-		// Clear from memory cache
-		for (const key of this._memoryCache.keys()) {
-			if (key.startsWith(`group_${fieldId}_`)) {
-				this._memoryCache.delete(key);
-			}
-		}
-
-		if (!this.imageDB) return;
-
-		try {
-			const transaction = this.imageDB.transaction(['groups'], 'readwrite');
-			const store = transaction.objectStore('groups');
-			const index = store.index('fieldId');
-
-			const range = IDBKeyRange.only(fieldId);
-			const request = index.openCursor(range);
-
-			await new Promise((resolve, reject) => {
-				request.onsuccess = (event) => {
-					const cursor = event.target.result;
-					if (cursor) {
-						cursor.delete();
-						cursor.continue();
-					} else {
-						resolve();
-					}
-				};
-				request.onerror = () => reject(request.error);
-			});
-		} catch (error) {
-			console.error('Failed to clear groups for field:', error);
-		}
-	}
-
-	/**
-	 * Remove pending image from storage
-	 * @param {string} id - Image identifier
-	 * @returns {Promise<boolean>} Success status
-	 */
-	async removeImagePending(id) {
-		// Remove from memory cache
-		this._memoryCache.delete(`pending_image_${id}`);
-
-		if (!this.imageDB) {
-			return true;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction([this.imageStoreName], 'readwrite');
-			const store = transaction.objectStore(this.imageStoreName);
-
-			await new Promise((resolve, reject) => {
-				const request = store.delete(id);
-				request.onsuccess = () => resolve();
-				request.onerror = () => reject(request.error);
-			});
-
-			return true;
-		} catch (error) {
-			console.error('Failed to remove pending image:', error);
-			return false;
-		}
-	}
-
-	async updateGroupData(fieldId, groupId, updates) {
-		const groupKey = `group_${fieldId}_${groupId}`;
-
-		// Update memory cache
-		const existing = this._memoryCache.get(groupKey);
-		if (existing) {
-			this._memoryCache.set(groupKey, { ...existing, ...updates });
-		}
-
-		// Update IndexedDB
-		if (this.imageDB) {
-			return this.storeGroupData(fieldId, groupId, updates);
-		}
-
-		return true;
-	}
-
-	/**
-	 * Clear all pending images for an operation
-	 * @param {string} operationId - Queue operation ID
-	 * @returns {Promise<number>} Number of images removed
-	 */
-	async clearImagesPendingByOperation(operationId) {
-		const images = await this.getImagesPendingByOperation(operationId);
-		let removedCount = 0;
-		const fieldsToCleanup = new Set();
-
-		for (const image of images) {
-			if (await this.removeImagePending(image.id)) {
-				removedCount++;
-				if (image.fieldId) {
-					fieldsToCleanup.add(image.fieldId);
-				}
-			}
-		}
-
-		// Clear group data for affected fields
-		for (const fieldId of fieldsToCleanup) {
-			await this.clearGroupsForField(fieldId);
-		}
-
-		return removedCount;
-	}
-
-	/**
-	 * Clean up old pending images (older than 24 hours)
-	 * @returns {Promise<number>} Number of images cleaned up
-	 */
-	async cleanupOldPendingImages() {
-		// const cutoffTime = Date.now() - (24 * 60 * 60 * 1000); // 24 hours
-		const cutoffTime = Date.now(); // 24 hours
-		let cleanedCount = 0;
-
-		if (!this.imageDB) {
-			// Clean memory cache
-			for (const [key, value] of this._memoryCache.entries()) {
-				if (key.startsWith('pending_image_') && value.timestamp < cutoffTime) {
-					this._memoryCache.delete(key);
-					cleanedCount++;
-				}
-			}
-			return cleanedCount;
-		}
-
-		try {
-			const transaction = this.imageDB.transaction([this.imageStoreName], 'readwrite');
-			const store = transaction.objectStore(this.imageStoreName);
-			const index = store.index('timestamp');
-
-			const range = IDBKeyRange.upperBound(cutoffTime);
-			const request = index.openCursor(range);
-
-			await new Promise((resolve, reject) => {
-				request.onsuccess = (event) => {
-					const cursor = event.target.result;
-					if (cursor) {
-						cursor.delete();
-						cleanedCount++;
-						cursor.continue();
-					} else {
-						resolve();
-					}
-				};
-				request.onerror = () => reject(request.error);
-			});
-		} catch (error) {
-			console.error('Failed to cleanup old pending images:', error);
-		}
-
-		return cleanedCount;
-	}
-
-	/**
-	 * Generate a cache key from URL and fetch options
-	 *
-	 * @param {string} url - The fetch URL
-	 * @param {object} options - The fetch options
-	 * @returns {string} - The cache key
-	 */
-	generateCacheKey(url, options = {}) {
-		let body = '';
-		if (options.method === 'POST' && options.body) {
-			try {
-				body = typeof options.body === 'string'
-					? JSON.stringify(JSON.parse(options.body))
-					: JSON.stringify(options.body);
-			} catch (e) {
-				body = String(options.body);
-			}
-		}
-
-		// For GET requests, use URL as is
-		if (!options.method || options.method === 'GET') {
-			return url;
-		}
-
-		// For other methods, include method and body in key
-		return `${options.method}:${url}:${body}`;
-	}
-
-	/**
-	 * Invalidate caches affected by timestamp updates
-	 *
-	 * @returns {Promise<void>}
-	 */
-	async invalidateAffectedCaches() {
-		// Get all content types that need invalidation
-		const contentToInvalidate = [];
-
-		for (const [content, timestamp] of Object.entries(this.cachedContent)) {
-			// Skip if no timestamp or invalid value
-			if (!timestamp) continue;
-
-			// Compare with local timestamp
-			if (this.cacheUpdated(content)) {
-				contentToInvalidate.push(content);
-			}
-		}
-
-		// If no invalidations needed, return early
-		if (contentToInvalidate.length === 0) return;
-
-		// Clear cache for affected content types
-		await this.clearByContent(contentToInvalidate);
-		console.log(`Invalidated caches for content types: ${contentToInvalidate.join(', ')}`);
-	}
-
-	/**
-	 * Clear all cache entries for a specific content type
-	 * @param {string|array} content - Content type(s) to clear
-	 * @returns {Promise<void>}
-	 */
-	async clearByContent(content) {
-		// Convert to array if string is passed
-		const typesToClear = Array.isArray(content) ? content : [content];
-
-		if (this.cacheAvailable) {
-			try {
-				const cache = await caches.open(this.options.namespace);
-				const keys = await cache.keys();
-
-				for (const request of keys) {
-					try {
-						const response = await cache.match(request);
-						const item = await response.json();
-
-						// Check if this item belongs to a content type we need to clear
-						if (item.content && typesToClear.includes(item.content)) {
-							await cache.delete(request);
-						}
-					} catch (e) {
-						// Skip if we can't parse the item
-					}
-				}
-			} catch (error) {
-				console.warn('Error clearing browser cache by content type:', error);
-			}
-		} else {
-			// Clear localStorage by content type
-			try {
-				for (let i = localStorage.length - 1; i >= 0; i--) {
-					const key = localStorage.key(i);
-					if (key && key.startsWith(this.options.namespace)) {
-						try {
-							const item = JSON.parse(localStorage.getItem(key));
-							if (item.content && typesToClear.includes(item.content)) {
-								localStorage.removeItem(key);
-							}
-						} catch (e) {
-							// Skip invalid items
-						}
-					}
-				}
-			} catch (error) {
-				console.warn('Error clearing localStorage by content type:', error);
-			}
-		}
-
-		// Also clear from memory cache for these content types
-		for (const [key, item] of this._memoryCache.entries()) {
-			if (item.content && typesToClear.includes(item.content)) {
-				this._memoryCache.delete(key);
-			}
-		}
-
-		console.log(`Cache cleared for content types: ${typesToClear.join(', ')}`);
-	}
-
-	/**
-	 * Clean expired items from cache
-	 *
-	 * @returns {Promise<void>}
-	 */
-	async cleanExpired() {
-		const now = Date.now();
-		const maxAge = this.options.defaultTTL;
-
-		if (this.cacheAvailable) {
-			try {
-				const cache = await caches.open(this.options.namespace);
-				const keys = await cache.keys();
-
-				for (const request of keys) {
-					const response = await cache.match(request);
-
-					try {
-						const cacheItem = await response.json();
-						if (now - cacheItem.timestamp > maxAge) {
-							await cache.delete(request);
-						}
-					} catch (e) {
-						// If we can't parse it, just leave it alone
-					}
-				}
-			} catch (error) {
-				console.warn('Error cleaning browser cache:', error);
-			}
-		} else {
-			// Clean localStorage
-			try {
-				for (let i = 0; i < localStorage.length; i++) {
-					const key = localStorage.key(i);
-
-					if (key && key.startsWith(this.options.namespace)) {
-						try {
-							const item = JSON.parse(localStorage.getItem(key));
-
-							if (now - item.timestamp > maxAge) {
-								localStorage.removeItem(key);
-							}
-						} catch (e) {
-							// Skip invalid items
-						}
-					}
-				}
-			} catch (error) {
-				console.warn('Error cleaning localStorage cache:', error);
-			}
-		}
-
-		// Clean memory cache
-		for (const [key, item] of this._memoryCache.entries()) {
-			if (now - item.timestamp > maxAge) {
-				this._memoryCache.delete(key);
-			}
-		}
-	}
-
-	/**
-	 * Clear all cache
-	 *
-	 * @returns {Promise<void>}
-	 */
-	async clear() {
-		// Clear memory cache
-		this._memoryCache.clear();
-
-		if (this.cacheAvailable) {
-			try {
-				await caches.delete(this.options.namespace);
-			} catch (error) {
-				console.warn('Error clearing browser cache:', error);
-			}
-		}
-
-		// Also clear localStorage in case we've used it as fallback
-		this.clearLocalStorage();
-	}
-
-	/**
-	 * Clear just localStorage cache
-	 */
-	clearLocalStorage() {
-		try {
-			for (let i = localStorage.length - 1; i >= 0; i--) {
-				const key = localStorage.key(i);
-				if (key && key.startsWith(this.options.namespace)) {
-					localStorage.removeItem(key);
-				}
-			}
-		} catch (error) {
-			console.warn('Error clearing localStorage cache:', error);
-		}
-	}
-
-	/**
-	 * Clean up
-	 */
-	destroy() {
-		// Clear intervals
-		if (this.headerCleanupInterval) {
-			clearInterval(this.headerCleanupInterval);
-		}
-
-		if (this._timestampInterval) {
-			clearInterval(this._timestampInterval);
-		}
-
-		// Force save any pending HTTP headers
-		this.forceHttpHeadersSave();
-
-		// Clear debounce timeout
-		if (this.headerSaveDebounce) {
-			clearTimeout(this.headerSaveDebounce);
-		}
-	}
-}
-
-window.RequestManager = {
-	controllers: new Map(),
-	timeouts: new Map(),
-
-	// Get or create a controller for a specific context
-	getController(context = 'default') {
-		if (!this.controllers.has(context)) {
-			this.controllers.set(context, new AbortController());
-		}
-		return this.controllers.get(context);
-	},
-
-	// Set a timeout that will abort the controller after the specified time
-	setTimeout(context = 'default', ms = 30000) {
-		// Clear any existing timeout for this context
-		this.clearTimeout(context);
-
-		const controller = this.getController(context);
-		const timeoutId = setTimeout(() => {
-			controller.abort();
-			this.controllers.set(context, new AbortController()); // Create a new controller
-		}, ms);
-
-		this.timeouts.set(context, timeoutId);
-		return controller.signal;
-	},
-
-	// Clear timeout for a context
-	clearTimeout(context = 'default') {
-		if (this.timeouts.has(context)) {
-			clearTimeout(this.timeouts.get(context));
-			this.timeouts.delete(context);
-		}
-	},
-
-	// Abort a specific context manually
-	abort(context = 'default') {
-		if (this.controllers.has(context)) {
-			this.controllers.get(context).abort();
-			this.controllers.set(context, new AbortController());
-		}
-		this.clearTimeout(context);
-	}
-
-
-};
-
-// Initialize global cache instance
-window.jvbCache = new Cache();
-// Check cache effectiveness
-const stats = window.jvbCache.getHttpCacheStats();
-console.log('HTTP Cache Stats:', stats);
-/**
- * Enhanced page lifecycle handling
- */
-window.addEventListener('beforeunload', () => {
-	if (window.jvbCache) {
-		window.jvbCache.forceHttpHeadersSave();
-	}
-});
-
-window.addEventListener('pagehide', () => {
-	if (window.jvbCache) {
-		window.jvbCache.forceHttpHeadersSave();
-	}
-});
-
-
diff --git a/assets/js/dash/FormHandler.js b/assets/js/dash/FormHandler.js
deleted file mode 100644
index c540e10..0000000
--- a/assets/js/dash/FormHandler.js
+++ /dev/null
@@ -1,2834 +0,0 @@
-class FormFields {
-	constructor() {
-		this.forms = new Map(); // Store form configurations
-		this.initialized = false;
-		this.stepMultiplier = 1;
-
-		this.cache = window.jvbCache;
-		this.queue = window.jvbQueue;
-		this.debouncer = window.debouncer;
-		this.activeOperations = new Map(); // operationId -> operation details
-		this.pendingForms = new Map(); // formId -> pending operation data
-
-		this.activeRepeaters = new Map();
-		this.repeaterDelays = {
-			change: 6000,
-			typing: 3000,
-			blur: 1500,
-			add: 500,
-			remove: 800,
-			reorder: 1000
-		};
-
-		this.ignore = [];
-
-		// Bind methods for event delegation
-		this.submitHandler = this.handleSubmit.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-		this.clickHandler = this.handleClick.bind(this);
-		this.focusHandler = this.handleFocus.bind(this);
-		this.keyHandler = this.handleKeys.bind(this);
-		this.blurHandler = this.handleBlur.bind(this);
-
-		this.init();
-	}
-
-	/**
-	 * Initialize the form manager
-	 */
-	async init() {
-		this.scanExistingForms();
-		this.initListeners();
-		await this.resumePendingOperations();
-	}
-
-
-	/**
-	 * Scan page for existing forms and register them automatically
-	 */
-	scanExistingForms() {
-		//look for forms with the save endpoint
-		const managedForms = document.querySelectorAll('form[data-form-id]');
-
-		managedForms.forEach(form => {
-			try {
-				this.registerForm(form);
-			} catch (error) {
-				this.handleError('Failed to register form:', form, error);
-			}
-		});
-	}
-
-	/**
-	 * Register a form with configuration from data attributes
-	 * @param {HTMLFormElement} formElement
-	 * @param {Object} options - Override options (optional)
-	 * @returns {Object} Form configuration
-	 */
-	registerForm(formElement, options = {}) {
-		options = {
-			...this.parseFormDataAttributes(formElement),
-			...options
-		};
-
-		// Use existing addForm method with parsed config
-		return this.addForm(formElement, options);
-	}
-
-	/**
-	 * Parse form configuration from data attributes
-	 * @param {HTMLFormElement} formElement
-	 * @returns {Object} Configuration object
-	 */
-	parseFormDataAttributes(formElement) {
-		const dataset = formElement.dataset;
-		const config = {};
-
-		// Basic options
-		if ('autoSave' in dataset) {
-			config.autoSave = dataset.autoSave !== 'false';
-		}
-
-		if ('noCache' in dataset) {
-			config.noCache = true;
-		}
-		if ('saveDelay' in dataset) {
-			config.saveDelay = parseInt(dataset.saveDelay) || 2000;
-		}
-
-		if ('save' in dataset) {
-			config.api = dataset.save;
-		}
-
-		if ('itemId' in dataset) {
-			config.itemID = dataset.itemId;
-		}
-
-		if ('content' in dataset) {
-			config.content = dataset.content;
-		}
-
-		if ('title' in dataset) {
-			config.title = dataset.title;
-		}
-
-		// Advanced options
-		if ('noAutosave' in dataset) {
-			config.autoSave = false;
-		}
-
-		if ('onChange' in dataset) {
-			// Look for global callback function
-			const callbackName = dataset.onChange;
-			if (window[callbackName] && typeof window[callbackName] === 'function') {
-				config.onChange = window[callbackName];
-			}
-		}
-
-		if ('onSave' in dataset) {
-			const callbackName = dataset.onSave;
-			if (window[callbackName] && typeof window[callbackName] === 'function') {
-				config.onSave = window[callbackName];
-			}
-		}
-
-		if ('onSubmit' in dataset) {
-			const callbackName = dataset.onSubmit;
-			if (window[callbackName] && typeof window[callbackName] === 'function') {
-				config.onSubmit = window[callbackName];
-			}
-		}
-
-		// Headers
-		config.headers = { 'action_nonce': jvbSettings?.dash };
-		if ('headers' in dataset) {
-			try {
-				const additionalHeaders = JSON.parse(dataset.headers);
-				config.headers = { ...config.headers, ...additionalHeaders };
-			} catch (e) {
-				console.warn('Invalid headers JSON in data-headers:', dataset.headers);
-			}
-		}
-
-		return config;
-	}
-
-	/**
-	 * Add a form to be managed by this instance
-	 * @param {HTMLFormElement} formElement
-	 * @param {Object} options
-	 * @returns {Object} Form configuration
-	 */
-	addForm(formElement, options = {}) {
-
-		const formConfig = {
-			element: formElement,
-			id: formElement.dataset.formId,
-			data: this.collectFormData(formElement),
-			initialized: false,
-
-			// Default options with overrides
-			options: {
-				onSave: false,
-				onChange: null,
-				onSubmit: null,
-				itemID: null,
-				content: null,
-				saveDelay: 2000,
-				autoSave: true,
-				api: formElement.dataset.save || null,
-				headers: { 'action_nonce': jvbSettings?.dash },
-				...options
-			},
-
-			// Form-specific state
-			state: {
-				saveTimeout: null,
-				lastData: null,
-				isDirty: false,
-				operationId: null,
-				cached: false
-			},
-			dependencies: new Map(),
-		};
-
-
-		// Store the configuration
-		this.forms.set(formConfig.id, formConfig);
-
-		// Initialize form-specific features
-		this.initFormFeatures(formConfig);
-		return formConfig;
-	}
-
-	initFormFeatures(formConfig) {
-		if (formConfig.initialized) {
-			return;
-		}
-		formConfig.initialized = true;
-		const form = formConfig.element;
-
-
-		// Only initialize what exists
-		const featureChecks = [
-			{ selector: '[data-tab]', init: () => this.initTabs(formConfig) },
-			{ selector: '.repeater', init: () => this.initRepeaterFields(form) },
-			{ selector: '[data-editor="true"]', init: () => this.initQuillEditor(formConfig) },
-			{ selector: '.gallery', init: () => this.initGalleryFields(formConfig) },
-			{ selector: '.image', init: () => this.initImageFields(formConfig) },
-			{ selector: '[data-limit]', init: () => this.initCharacterLimits(formConfig) },
-			{ selector: '[data-depends-on]', init: () => this.initConditionalFields(formConfig)}
-		];
-
-		requestAnimationFrame(() => {
-			featureChecks.forEach(({selector, init} ) => {
-				if (form.querySelector(selector)) {
-					init();
-				}
-			});
-		});
-	}
-
-	initTabs(formConfig) {
-		if (!window.jvbTabs) {
-			console.warn('jvbTabs not available');
-			return;
-		}
-		let form = formConfig.element;
-		// Check if form is within a tabs container
-		const tabsContainer = form.closest('[data-tab]') ||
-			form.closest('.tabs-container') ||
-			form.querySelector('.tabs:not(.icon)');
-
-		if (tabsContainer) {
-			// Initialize tabs with form-specific callbacks
-			formConfig.tabs = new window.jvbTabs(form, {
-				updateURL: false, // Disable URL updates for form tabs,
-			});
-
-			this.addTabNavigationButtons(formConfig);
-		}
-	}
-
-	addTabNavigationButtons(formConfig) {
-		const form = formConfig.element;
-		const sections = form.querySelectorAll('section[id]');
-
-		if (sections.length <= 1) {
-			return; // No need for navigation if only one section
-		}
-
-		sections.forEach((section, index) => {
-			let isLast = index === sections.length - 1;
-			// Check if navigation buttons already exist
-			if (section.querySelector('.tab-navigation')) {
-				return;
-			}
-
-			let buttons = {
-				previous: sections[index - 1]?.id ?? null,
-				next: (isLast) ? null : sections[index + 1]?.id ?? null
-			};
-
-			for (var [direction, id] of Object.entries(buttons)) {
-				if (id){
-					let button = form.querySelector('button[type=submit]').cloneNode(true);
-
-					button.type = 'button';
-					button.className = `tab-navigation ${direction}`;
-					button.innerText = window.uppercaseFirst(direction);
-
-					let icon = (direction === 'previous') ? 'left' : 'right';
-					button.prepend(window.getIcon(icon));
-					button.dataset.navigateTo = id;
-
-					if (isLast) {
-						section.querySelector('.form-actions').prepend(button);
-					} else {
-						section.append(button);
-					}
-				}
-			}
-		});
-	}
-
-	/*********************************************************
-	 *
-	 * CACHE
-	 *
-	 *********************************************************/
-	/**
-	 * Cache form data with operation tracking
-	 */
-	async cacheFormData(formConfig) {
-		if (!this.cache || !formConfig.options.cache) return false;
-
-		try {
-			const cacheData = {
-				formId: formConfig.id,
-				formData: this.collectFormData(formConfig.element),
-				formConfig: {
-					endpoint: formConfig.element.dataset.save,
-					content: formConfig.options.content,
-					itemID: formConfig.options.itemID,
-					headers: formConfig.options.headers
-				},
-				timestamp: Date.now(),
-				status: 'pending',
-				operationId: formConfig.state.operationId
-			};
-
-			const cacheKey = `form_${formConfig.id}`;
-			await this.cache.setItem(cacheKey, cacheData);
-
-			return true;
-
-		} catch (error) {
-			this.handleError('Failed to cache form data:', error);
-			return false;
-		}
-	}
-
-	/**
-	 * Update cached form with operation ID
-	 */
-	async updateCachedFormOperation(formId, operationId) {
-		if (!this.cache || this.forms.get(formId).options.noCache) return;
-
-		try {
-			const cacheKey = `form_${formId}`;
-			const cachedData = await this.cache.getItem(cacheKey);
-
-			if (cachedData) {
-				cachedData.operationId = operationId;
-				cachedData.status = 'queued';
-				await this.cache.setItem(cacheKey, cachedData);
-			}
-		} catch (error) {
-			this.handleError('Failed to update cached form operation:', error);
-		}
-	}
-
-	/**
-	 * Resume pending operations from cache
-	 */
-	async resumePendingOperations() {
-		if (!this.cache) return;
-
-		try {
-			// Get all cached form operations for current page
-			const pendingForms = await this.getCachedFormsForCurrentPage();
-
-			if (pendingForms.length > 0) {
-				for (const cachedForm of pendingForms) {
-					await this.resumeFormOperation(cachedForm);
-				}
-			}
-		} catch (error) {
-			this.handleError('Failed to resume pending operations:', error);
-		}
-	}
-
-	/**
-	 * Get cached forms for current page
-	 */
-	async getCachedFormsForCurrentPage() {
-		if (!this.cache) return [];
-		try {
-			const pendingForms = [];
-			for (let [key, value] of this.forms) {
-				if (!value.options.cache){
-					continue;
-				}
-				const cachedData = await this.cache.getItem(`form_${key}`);
-				if (cachedData &&
-					cachedData.operationId &&
-					['queued', 'pending', 'processing'].includes(cachedData.status)) {
-					pendingForms.push(cachedData);
-				}
-			}
-
-			return pendingForms;
-		} catch (error) {
-			this.handleError('Failed to get cached forms:', error);
-			return [];
-		}
-	}
-
-	/**
-	 * Resume a specific form operation
-	 */
-	async resumeFormOperation(cachedForm) {
-		try {
-			// Check if operation still exists in queue
-			const operationStatus = await this.queue.getOperationStatus(cachedForm.operationId);
-
-			if (operationStatus) {
-				// Re-track the operation
-				this.activeOperations.set(cachedForm.operationId, {
-					formId: cachedForm.formId,
-					status: operationStatus.status,
-					startTime: cachedForm.timestamp,
-					data: cachedForm.formConfig
-				});
-
-				// Update form config if form is registered
-				const formConfig = this.forms.get(cachedForm.formId);
-				if (formConfig) {
-					formConfig.state.operationId = cachedForm.operationId;
-					formConfig.state.cached = true;
-
-					// Show resumption notification
-					this.showFormResumptionNotification(formConfig, operationStatus.status);
-				}
-
-				// Set up completion handler
-				this.queue.onOperationComplete(cachedForm.operationId, (operation) => {
-					this.handleOperationComplete(operation);
-				});
-
-			} else {
-				// Operation no longer exists, clean up cache
-				await this.removeCachedForm(cachedForm.formId);
-			}
-		} catch (error) {
-			this.handleError('Failed to resume form operation:', error);
-			// Clean up on error
-			await this.removeCachedForm(cachedForm.formId);
-		}
-	}
-
-	/**
-	 * Show form resumption notification
-	 */
-	showFormResumptionNotification(formConfig, status) {
-		const form = formConfig.element;
-		const notification = document.createElement('div');
-		notification.className = 'form-resumption-notification';
-		notification.innerHTML = `
-			<div class="notification-content">
-				<span class="icon">🔄</span>
-				<span class="message">Resumed: ${this.getStatusMessage(status)}</span>
-				<button type="button" class="dismiss">×</button>
-			</div>
-		`;
-
-		// Insert notification
-		form.insertBefore(notification, form.firstChild);
-
-		// Auto-dismiss
-		setTimeout(() => {
-			notification.remove();
-		}, 5000);
-
-		// Manual dismiss
-		notification.querySelector('.dismiss').addEventListener('click', () => {
-			notification.remove();
-		});
-	}
-
-	/**
-	 * Remove cached form data
-	 */
-	async removeCachedForm(formId) {
-		if (!this.cache || !this.forms.get(formId).options.cache) return;
-
-		try {
-			const cacheKey = `form_${formId}`;
-			await this.cache.removeItem(cacheKey);
-		} catch (error) {
-			this.handleError('Failed to remove cached form:', error);
-		}
-	}
-
-
-	/**
-	 * Get form cache status
-	 */
-	async getFormCacheStatus(formId) {
-		if (!this.cache) return null;
-
-		try {
-			const cacheKey = `form_${formId}`;
-			const cachedData = await this.cache.getItem(cacheKey);
-
-			return cachedData ? {
-				formId: cachedData.formId,
-				status: cachedData.status,
-				operationId: cachedData.operationId,
-				timestamp: cachedData.timestamp,
-				hasData: !!cachedData.formData
-			} : null;
-		} catch (error) {
-			this.handleError('Failed to get form cache status:', error);
-			return null;
-		}
-	}
-	/**
-	 * Track form operation
-	 */
-	trackOperation(formId, operationId, operationData) {
-		this.activeOperations.set(operationId, {
-			formId: formId,
-			status: 'queued',
-			startTime: Date.now(),
-			data: operationData
-		});
-
-		// Update form config
-		const formConfig = this.forms.get(formId);
-		if (formConfig) {
-			formConfig.state.operationId = operationId;
-			formConfig.state.cached = true;
-		}
-	}
-
-	/**
-	 * Handle operation updates from queue
-	 */
-	handleOperationUpdate(operation) {
-		const activeOp = this.activeOperations.get(operation.id);
-		if (!activeOp) return;
-
-		// Update operation status
-		activeOp.status = operation.status;
-		this.activeOperations.set(operation.id, activeOp);
-
-		// Update form UI if needed
-		const formConfig = this.forms.get(activeOp.formId);
-		if (formConfig) {
-			// Show progress or status updates in form UI
-			this.updateFormStatus(formConfig, operation.status);
-		}
-	}
-
-	/**
-	 * Handle operation completion
-	 */
-	async handleOperationComplete(operation) {
-		const activeOp = this.activeOperations.get(operation.id);
-		if (!activeOp) return;
-
-		const formId = activeOp.formId;
-		const formConfig = this.forms.get(formId);
-
-		if (formConfig) {
-			// Clear operation tracking
-			formConfig.state.operationId = null;
-			formConfig.state.cached = false;
-			formConfig.state.isDirty = false;
-
-			// Update last saved data
-			formConfig.data = this.collectFormData(formConfig.element);
-
-			// Update form UI
-			this.updateFormStatus(formConfig, 'completed');
-		}
-
-		// Clean up
-		this.activeOperations.delete(operation.id);
-		await this.removeCachedForm(formId);
-	}
-	/**
-	 * Update form status in UI
-	 */
-	updateFormStatus(formConfig, status) {
-		// Add status indicator to form or show notification
-		const statusElement = formConfig.element.querySelector('.form-status');
-		if (statusElement) {
-			statusElement.className = `form-status ${status}`;
-			statusElement.textContent = this.getStatusMessage(status);
-		}
-	}
-
-	/**
-	 * Get human-readable status message
-	 */
-	getStatusMessage(status) {
-		const messages = {
-			'queued': 'Queued for saving...',
-			'pending': 'Waiting to save...',
-			'processing': 'Saving...',
-			'completed': 'Saved successfully',
-			'failed': 'Save failed'
-		};
-		return messages[status] || status;
-	}
-	/*********************************************************
-	 *
-	 *   EVENT HANDLERS
-	 *
-	 *********************************************************/
-	handleSubmit(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.dataset.formId)) return;
-
-		this.checkHiddenTabs(this.forms.get(form.dataset.formId));
-		event.preventDefault();
-		const formConfig = this.forms.get(form.dataset.formId);
-
-		// Call form-specific submit callback if provided
-		if (formConfig.options.onSubmit) {
-			formConfig.options.onSubmit(event, this.collectFormData(formConfig.element));
-		} else {
-			// Default submit behavior
-			this.handleSave(this.collectFormData(formConfig.element), formConfig);
-		}
-	}
-
-	checkHiddenTabs(formConfig) {
-		if (!('tabs' in formConfig)) {
-			return;
-		}
-		let sections = formConfig.element.querySelectorAll('section');
-		for (let section of sections) {
-			let requiredInputs = section.querySelector('[required]');
-			if (requiredInputs) {
-				formConfig.tabs.switchTab(section.id);
-				return;
-			}
-		}
-	}
-
-	handleChange(event) {
-		// Cache target checks
-		const target = event.target;
-		const form = target.form || target.closest('form');
-		console.log('[form]Handling change');
-
-		// Early return if not a managed form
-		if (!form?.dataset.formId || !this.forms.has(form.dataset.formId)) return;
-		console.log('[form]Managing this form');
-		// Image fields handled separately
-		if (window.targetCheck(event, '.image.field')) {
-			return;
-		}
-		console.log('[form] Not image field');
-
-		const formConfig = this.forms.get(form.dataset.formId);
-		const fieldName = event.target.name;
-
-		// Check conditionals immediately for all fields
-		this.checkConditionals(fieldName, formConfig);
-		console.log('[form]Conditionals checked');
-
-		// Handle special field types
-		this.handleSpecialFields(event, formConfig);
-
-
-		// Skip autosave if disabled
-		if ('noautosave' in form.dataset) {
-			console.log('No Autosave set');
-			return;
-		}
-
-		if (fieldName.includes('::')) {
-			const groupName = fieldName.split('::')[0];
-			this.scheduleSave(formConfig, {
-				type: 'field',
-				fieldName: groupName,
-				reason: event.type,
-				delay: 3000
-			});
-		} else if (fieldName.includes(':')) {
-			const row = target.closest('.repeater-row');
-			if (row) {
-				const repeater = row.closest('.field.repeater');
-				const repeaterName = repeater.dataset.field;
-				const delay = this.getRepeaterChangeDelay(event.target, event.type);
-
-				this.scheduleSave(formConfig, {
-					type: 'field',
-					fieldName: repeaterName,
-					reason: event.type,
-					delay: delay
-				});
-			}
-		} else {
-			// Schedule form-wide save
-			this.scheduleSave(formConfig, {
-				type: 'form',
-				reason: 'change'
-			});
-		}
-	}
-
-	handleClick(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.dataset.formId)) return;
-
-		const formConfig = this.forms.get(form.dataset.formId);
-
-		if (window.targetCheck(event, '.add-repeater-row')) {
-			const repeater = event.target.closest('.repeater');
-			this.addRepeaterRow(repeater, formConfig);
-
-			// Schedule save after add
-			this.scheduleSave(formConfig, {
-				type: 'field',
-				fieldName: repeater.dataset.field,
-				reason: 'add',
-				delay: this.repeaterDelays.add
-			});
-
-		} else if (window.targetCheck(event, '.remove-row')) {
-			const repeater = event.target.closest('.repeater');
-			this.removeRepeaterRow(event.target, formConfig);
-
-			// Schedule save after remove
-			this.scheduleSave(formConfig, {
-				type: 'field',
-				fieldName: repeater.dataset.field,
-				reason: 'remove',
-				delay: this.repeaterDelays.remove
-			});
-
-		} else if (window.targetCheck(event, '.remove-image')) {
-			this.handleImageRemove(event.target.closest('.field'));
-		} else if (window.targetCheck(event, '.replace-image')) {
-			let fileInput = event.closest('.image').querySelector('input[type="file"]');
-			fileInput.click();
-		} else if (window.targetCheck(event, 'div.quantity')) {
-			let quantity = window.targetCheck(event, 'div.quantity');
-			this.handleNumberClick(event, quantity);
-		} else if (window.targetCheck(event, '.tab-navigation')) {
-			formConfig.tabs.switchTab(event.target.dataset.navigateTo);
-		}
-	}
-
-	handleFocus(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.dataset.formId)) return;
-
-		const repeaterRow = event.target.closest('.repeater-row');
-		if (!repeaterRow) return;
-
-		const formConfig = this.forms.get(form.dataset.formId);
-		const rowId = repeaterRow.id || this.generateRowId(repeaterRow);
-
-		// Store the currently active repeater field
-		this.activeRepeaters.set(form.dataset.formId, {
-			rowId: rowId,
-			element: event.target,
-			formConfig: formConfig
-		});
-	}
-
-	handleBlur(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.dataset.formId)) return;
-
-		// Handle repeater field blur with shorter delay
-		const repeaterRow = event.target.closest('.repeater-row');
-		if (repeaterRow) {
-			const repeater = repeaterRow.closest('.field.repeater');
-
-			this.scheduleSave(this.forms.get(form.dataset.formId), {
-				type: 'field',
-				fieldName: repeater.dataset.field,
-				reason: 'blur',
-				delay: this.repeaterDelays.blur
-			});
-		}
-	}
-
-	handleKeys(e) {
-		const MAX_MULTIPLIER = 10000;
-
-		if (e.ctrlKey && e.shiftKey) {
-			this.stepMultiplier = Math.min(
-				Math.max(this.stepMultiplier * 100, 1000),
-				MAX_MULTIPLIER
-			);
-		} else if (e.shiftKey) {
-			this.stepMultiplier = Math.min(
-				Math.max(this.stepMultiplier * 10, 100),
-				MAX_MULTIPLIER
-			);
-		} else if (e.key === 'Escape') {
-			this.stepMultiplier = 1;
-		}
-	}
-	/**
-	 * Initialize global event listeners (single delegation point)
-	 */
-	initListeners() {
-		if (this.initialized) return;
-
-		// Use event delegation for all forms
-		document.addEventListener('submit', this.submitHandler);
-		document.addEventListener('change', this.changeHandler);
-		document.addEventListener('click', this.clickHandler);
-		document.addEventListener('keydown', this.keyHandler);
-		document.addEventListener('focusin', this.focusHandler);
-		document.addEventListener('focusout', this.blurHandler);
-
-		this.initialized = true;
-	}
-
-	removeChangeListener() {
-		document.removeEventListener('change', this.changeHandler);
-	}
-	addChangeListener() {
-		document.addEventListener('change', this.changeHandler);
-	}
-
-	/*******************************************************************
-	 *
-	 * 	Data Processing
-	 *
-	 *******************************************************************/
-	// collectFormData(form) {
-	// 	const formData = new FormData(form);
-	// 	let data = {};
-	// 	//push any fields that need all parts to a 'sendAll' property
-	// 	let repeaterData = {};
-	//
-	// 	for (let [key, value] of formData.entries()) {
-	// 		if (this.ignore.includes(key) || key.endsWith('_temp')) {
-	// 			continue;
-	// 		}
-	//
-	// 		let post = null;
-	// 		let original = null;
-	//
-	// 		//If we're on a table view, we need to organize this by post ID
-	// 		if (key.includes('|')) {
-	// 			[post, key] = key.split('|');
-	// 			//Temporarily store data to merge later
-	// 			original = data;
-	// 			data = original[post] ?? {};
-	// 		}
-	//
-	// 		// Check if this is a repeater field (3 parts: field:index:name)
-	// 		const keyParts = key.split(':');
-	// 		if (keyParts.length === 3) {
-	// 			// Handle repeater fields (field:index:name)
-	// 			let [fieldName, index, rawSubField] = keyParts;
-	//
-	// 			// Handle array fields (remove [] brackets)
-	// 			const isArrayField = rawSubField.endsWith('[]');
-	// 			const subField = rawSubField.replace('[]', '');
-	//
-	// 			// Initialize repeater structure
-	// 			if (!repeaterData[fieldName]) {
-	// 				repeaterData[fieldName] = {};
-	// 			}
-	// 			if (!repeaterData[fieldName][index]) {
-	// 				repeaterData[fieldName][index] = {};
-	// 			}
-	//
-	// 			// Handle different field types for repeater
-	// 			let fieldValue = value;
-	//
-	// 			// Only check radio buttons since FormData handles checkboxes correctly
-	// 			const fieldElement = form.querySelector(`[name="${key}"]`);
-	// 			if (fieldElement && fieldElement.type === 'radio' && !fieldElement.checked) {
-	// 				continue; // Skip unchecked radios
-	// 			}
-	//
-	// 			// Handle array fields (like checkbox groups)
-	// 			if (isArrayField || repeaterData[fieldName][index][subField]) {
-	// 				// Initialize as array if not already
-	// 				if (!repeaterData[fieldName][index][subField]) {
-	// 					repeaterData[fieldName][index][subField] = [];
-	// 				} else if (!Array.isArray(repeaterData[fieldName][index][subField])) {
-	// 					repeaterData[fieldName][index][subField] = [repeaterData[fieldName][index][subField]];
-	// 				}
-	// 				repeaterData[fieldName][index][subField].push(fieldValue);
-	// 			} else {
-	// 				// Single value field
-	// 				repeaterData[fieldName][index][subField] = fieldValue;
-	// 			}
-	//
-	// 		} else if (keyParts.length === 2) {
-	// 			// Handle group fields (group:field)
-	// 			let [groupName, fieldName] = keyParts;
-	//
-	// 			// Initialize group structure if needed
-	// 			if (!data[groupName]) {
-	// 				data[groupName] = {};
-	// 			}
-	//
-	// 			// Handle array values for group fields (checkboxes, etc.)
-	// 			if (data[groupName][fieldName]) {
-	// 				if (!Array.isArray(data[groupName][fieldName])) {
-	// 					data[groupName][fieldName] = [data[groupName][fieldName]];
-	// 				}
-	// 				data[groupName][fieldName].push(value);
-	// 			} else {
-	// 				data[groupName][fieldName] = value;
-	// 			}
-	//
-	// 		} else if (key.includes('[')) {
-	// 			let [fieldKey, v ] = key.split('[');
-	// 			v = v.replace(']','');
-	// 			if (!Object.hasOwn(data, fieldKey)) {
-	// 				data[fieldKey] = {};
-	//
-	// 				if (!Object.hasOwn(data, 'sendAll')) {
-	// 					data['sendAll'] = [fieldKey];
-	// 				} else if (!data['sendAll'].includes(fieldKey)) {
-	// 					data['sendAll'].push(fieldKey);
-	// 				}
-	// 			}
-	// 			data[fieldKey][v] = value;
-	//
-	// 		} else {
-	// 			// Handle regular fields (no colons or single field name)
-	// 			// Handle array values (multiple checkboxes/selects)
-	// 			if (data[key]) {
-	// 				if (!Array.isArray(data[key])) {
-	// 					data[key] = [data[key]];
-	// 				}
-	// 				data[key].push(value);
-	// 			} else {
-	// 				data[key] = value;
-	// 			}
-	// 		}
-	//
-	// 		if (post) {
-	// 			//Merge back with original
-	// 			original[post] = data;
-	// 			data = original;
-	// 		}
-	// 	}
-	//
-	// 	return data;
-	// }
-
-	collectFormData(form) {
-		const formData = new FormData(form);
-		let data = {};
-		const repeaterData = {};
-		const postData = {};
-
-		for (let [key, value] of formData.entries()) {
-			if (this.ignore.includes(key) || key.endsWith('_temp')) continue;
-
-			const processor = this.getFieldProcessor(key);
-			processor(key, value, data, repeaterData, postData, form);
-		}
-		if (!window.isEmptyObject(postData)) {
-			data = this.mergeRepeaterData(data, repeaterData);
-			return this.mergePostData(data, postData);
-		}
-
-		return this.mergeRepeaterData(data, repeaterData);
-	}
-
-	getFieldProcessor(key) {
-		if (key.includes('|')) return this.processTableField;
-		if (key.includes('::')) return this.processGroupField;
-		if (key.includes(':')) return this.processRepeaterField;
-		if (key.includes('[')) return this.processLocationField;
-		return this.processRegularField;
-	}
-
-	mergeRepeaterData(data, repeaterData) {
-		Object.keys(repeaterData).forEach(fieldName => {
-			// Clean up empty rows and convert to array format
-			const cleanedRows = {};
-			Object.keys(repeaterData[fieldName]).forEach(index => {
-				const rowData = repeaterData[fieldName][index];
-				if (Object.keys(rowData).length > 0) {
-					cleanedRows[index] = rowData;
-				}
-			});
-
-			// Convert to sequential array
-			data[fieldName] = Object.values(cleanedRows);
-		});
-		return data;
-	}
-
-	mergePostData(data, postData) {
-		for (let [postId, postData] in Object.entries(postData)) {
-			data[postId] = postData;
-		}
-		return data;
-	}
-
-	processTableField(key, value, data, repeaterData, postData, form) {
-		/***
-		 * Table forms are a huge form containing multiple posts and their data
-		 * Field names are prepended with  `${postID}|`
-		 * Goal:
-		 * 1) Separate out the post id from the field name
-		 * 2) store the original data in a temporary 'original' variable
-		 * 3) Process the field as normal
-		 * 4) return the original data, as PostID: {$field data}
-		 * Final format:
-		 * {
-		 *     id1: {
-		 *         field1: "A title",
-		 *         field3: 32
-		 *     },
-		 *     id2: {
-		 *         field1: "Another title",
-		 *         field2: "122,21,32"
-		 *     }
-		 * }
-		 **/
-		let [post, fieldKey] = key.split('|');
-		if (!post in postData) {
-			postData[post] = {};
-		}
-
-		const processor = this.getFieldProcessor(fieldKey);
-		processor(fieldKey, value, postData, repeaterData, postData, form);
-
-	}
-	processRepeaterField(key, value, data, repeaterData, postData, form) {
-		let [fieldName, index, subField] = key.split(':');
-
-		const isArray = subField.endsWith('[]');
-		subField = subField.replace('[]', '');
-
-		//Ensure this repeater and row is in repeaterData
-		if (!repeaterData[fieldName]) {
-			repeaterData[fieldName] = {};
-		}
-		if (!repeaterData[fieldName][index]) {
-			repeaterData[fieldName][index] = {};
-		}
-
-		if (isArray || repeaterData[fieldName][index][subField]) {
-			// Initialize as array if not already
-			if (!repeaterData[fieldName][index][subField]) {
-				repeaterData[fieldName][index][subField] = [];
-			} else if (!Array.isArray(repeaterData[fieldName][index][subField])) {
-				repeaterData[fieldName][index][subField] = [repeaterData[fieldName][index][subField]];
-			}
-			repeaterData[fieldName][index][subField].push(value);
-		} else {
-			// Single value field
-			repeaterData[fieldName][index][subField] = value;
-		}
-	}
-	processGroupField(key, value, data, repeaterData, postData, form) {
-		const keys = key.split('::');
-		const rootGroup = keys[0];
-
-		// Initialize root group if it doesn't exist
-		if (!data[rootGroup]) {
-			data[rootGroup] = {};
-		}
-
-		// Build nested structure step by step
-		let current = data[rootGroup];
-		for (let i = 1; i < keys.length - 1; i++) {
-			const groupKey = keys[i];
-			if (!current[groupKey]) {
-				current[groupKey] = {};
-			}
-			current = current[groupKey];
-		}
-
-		// Set the final field value
-		const fieldKey = keys[keys.length - 1];
-
-		// Handle array values (checkboxes, multi-selects)
-		if (current[fieldKey] !== undefined) {
-			if (!Array.isArray(current[fieldKey])) {
-				current[fieldKey] = [current[fieldKey]];
-			}
-			current[fieldKey].push(value);
-		} else {
-			current[fieldKey] = value;
-		}
-	}
-	processLocationField(key, value, data, repeaterData, postData, form) {
-		let [fieldKey, v ] = key.split('[');
-		v = v.replace(']','');
-		if (!Object.hasOwn(data, fieldKey)) {
-			data[fieldKey] = {};
-
-			if (!Object.hasOwn(data, 'sendAll')) {
-				data['sendAll'] = [fieldKey];
-			} else if (!data['sendAll'].includes(fieldKey)) {
-				data['sendAll'].push(fieldKey);
-			}
-		}
-		data[fieldKey][v] = value;
-	}
-
-	processRegularField(key, value, data, repeaterData, postData, form) {
-		//handle array values (like checkboxes/selects)
-		if (data[key]) {
-			if (!Array.isArray(data[key])) {
-				data[key] = [data[key]];
-			}
-			data[key].push(value);
-		} else {
-			data[key] = value;
-		}
-	}
-
-	/**
-	 * Get differences between old and new data
-	 */
-	getDataChanges(newData, oldData, deep = false) {
-		return window.getDifferences?.map(oldData, newData) || {};
-	}
-
-	async handleSave(changes, formConfig) {
-		console.log(changes);
-		if (changes.length === 0 || window.isEmptyObject(changes)) {
-			return;
-		}
-
-		// Cache the current form state before operation
-		await this.cacheFormData(formConfig);
-
-		if (typeof formConfig.options.onSave === "function") {
-			formConfig.options.onSave(changes, formConfig);
-		} else if (formConfig.element.dataset.save) {
-			let endpoint = formConfig.element.dataset.save;
-			let title = formConfig.element.dataset.title ?? endpoint;
-
-			const operation = {
-				endpoint: endpoint,
-				headers: {
-					'action_nonce': jvbSettings.dash
-				},
-				title: `Saving ${title}`,
-				popup: `Saving ${title}...`,
-				data: changes,
-				formId: formConfig.id,
-				onComplete: (operation) => this.handleOperationComplete(operation),
-				onUpdate: (operation) => this.handleOperationUpdate(operation)
-			};
-
-			try {
-				const operationId = await window.jvbQueue.addToQueue(operation);
-
-				// Track the operation
-				this.trackOperation(formConfig.id, operationId, operation);
-
-				// Update cached data with operation ID
-				await this.updateCachedFormOperation(formConfig.id, operationId);
-			} catch (error) {
-				this.handleError('Failed to queue form operation:', error);
-				// Remove from cache if queueing failed
-				await this.removeCachedForm(formConfig.id);
-			}
-		}
-	}
-
-	/**
-	 * Unified save scheduling for both forms and specific fields
-	 * @param {Object} formConfig - Form configuration
-	 * @param {Object} options - Save options
-	 */
-	scheduleSave(formConfig, options = {}) {
-		console.log('Scheduling save...');
-		const opts = {
-			type: 'form',           // 'form' or 'field'
-			fieldName: null,        // Required for field saves
-			reason: 'change',       // 'change', 'blur', 'add', 'remove', 'reorder'
-			delay: null,          	// Uses form saveDelay if null
-			...options
-		};
-
-		// Generate timeout key
-		const timeoutKey = opts.type === 'field'
-			? `${formConfig.id}-${opts.fieldName}-${opts.type}`
-			: `${formConfig.id}-${opts.type}`;
-
-		// Determine delay
-		const delay = opts.delay ?? this.getSaveDelay(formConfig, opts);
-		console.log(timeoutKey);
-		console.log(delay);
-		this.debouncer.schedule(
-			timeoutKey,
-			() => { this.processChanges(formConfig, opts)},
-			delay
-		);
-	}
-
-	/**
-	 * Unified change processing for both forms and fields
-	 * @param {Object} formConfig - Form configuration
-	 * @param {Object} options - What to process
-	 */
-	processChanges(formConfig, options = {}) {
-		const opts = {
-			type: 'form',
-			fieldName: null,
-			processSave: true,
-			... options
-		};
-
-		let changes;
-
-		if (opts.type === 'field' && opts.fieldName) {
-			// Process specific field changes
-			changes = this.collectFieldChanges(formConfig, opts.fieldName);
-		} else {
-			console.log('formConfig: ', formConfig);
-			// Process entire form changes
-			const newData = this.collectFormData(formConfig.element);
-			changes = this.getDataChanges(newData, formConfig.data);
-			//Check for any fields that are flagged to send all data (mainly location fields)
-			if (newData.sendAll) {
-				newData.sendAll.forEach(key => {
-					changes[key] = newData[key];
-				});
-			}
-			formConfig.data = newData;
-		}
-
-		if (Object.keys(changes).length > 0) {
-			formConfig.state.isDirty = true;
-
-			if (opts.processSave) {
-				console.log('Handling Save....');
-				this.handleSave(changes, formConfig);
-			}
-		}
-	}
-
-	hasUploadsBlocking(formConfig) {
-		return formConfig.state.uploadPending || this.hasActiveUploads(formConfig);
-	}
-
-	getSaveDelay(formConfig, options) {
-		if (options.type === 'field') {
-			// Use repeater delays for field-level saves
-			switch (options.reason) {
-				case 'typing': return this.repeaterDelays.typing;
-				case 'blur': return this.repeaterDelays.blur;
-				case 'add': return this.repeaterDelays.add;
-				case 'remove': return this.repeaterDelays.remove;
-				case 'reorder': return this.repeaterDelays.reorder;
-				case 'change':
-				default: return this.repeaterDelays.change;
-			}
-		}
-
-		// Use form-level delay
-		return formConfig.options.saveDelay;
-	}
-
-	/**
-	 * Collect changes for a specific field (used for repeaters)
-	 */
-	collectFieldChanges(formConfig, fieldName) {
-		const currentData = this.collectFormData(formConfig.element);
-
-		// Return only the specified field data
-		const fieldChanges = {
-			[fieldName]: currentData[fieldName] || []
-		};
-
-		// Update stored data for this field
-		if (!formConfig.data) {
-			formConfig.data = {};
-		}
-		formConfig.data[fieldName] = currentData[fieldName];
-
-		return fieldChanges;
-	}
-	/************************************************************
-	 *
-	 * CONDITIONAL LOGIC
-	 *
-	 ************************************************************/
-	initConditionalFields(formConfig) {
-		const form = formConfig.element;
-
-		form.querySelectorAll('[data-depends-on]').forEach(field => {
-			const dependsOn = field.dataset.dependsOn;
-			if (!formConfig.dependencies.has(dependsOn)) {
-				formConfig.dependencies.set(dependsOn, []);
-			}
-			formConfig.dependencies.get(dependsOn).push(field);
-			const triggerValue = this.getFieldValue(form, dependsOn);
-
-			const requiredValue = field.dataset.dependsValue;
-			const operator = field.dataset.dependsOperator || '==';
-			const shouldShow = this.evaluateCondition(triggerValue, requiredValue, operator);
-
-			this.toggleFieldVisibility(field, shouldShow);
-		});
-
-	}
-
-	handleSpecialFields(event, formConfig) {
-		// Handle repeater field changes
-		if (event.target.closest('.repeater-row')) {
-			this.updateRepeaterOrder(event.target.closest('.repeater'));
-		}
-	}
-	checkConditionals(changed, formConfig) {
-		const dependencies = formConfig.dependencies.get(changed);
-		if (!dependencies) return;
-		this.updateConditionalFields(changed, dependencies, formConfig);
-	}
-	updateConditionalFields(changed, dependencies, formConfig) {
-
-		const triggerValue = this.getFieldValue(formConfig.element, changed);
-
-		dependencies.forEach(field => {
-			this.checkDependencies(field, triggerValue);
-		});
-	}
-
-	checkDependencies(field, value) {
-		const requiredValue = field.dataset.dependsValue;
-		const operator = field.dataset.dependsOperator || '==';
-		const shouldShow = this.evaluateCondition(value, requiredValue, operator);
-		this.toggleFieldVisibility(field, shouldShow);
-	}
-
-	evaluateCondition(fieldValue, requiredValue, operator) {
-		// Handle null/undefined values
-		if (fieldValue === null || fieldValue === undefined) {
-			fieldValue = '';
-		}
-		if (requiredValue === null || requiredValue === undefined) {
-			requiredValue = '';
-		}
-
-		// Convert both to strings for consistent comparison (since form values are always strings)
-		const fieldStr = String(fieldValue);
-		const requiredStr = String(requiredValue);
-
-		switch (operator) {
-			case '==':
-				return fieldStr == requiredStr; // Loose equality
-			case '!=':
-				return fieldStr != requiredStr; // Loose inequality
-			case '===':
-				return fieldStr === requiredStr; // Strict equality (if needed)
-			case '!==':
-				return fieldStr !== requiredStr; // Strict inequality (if needed)
-			case '>':
-				return Number(fieldValue) > Number(requiredValue);
-			case '>=':
-				return Number(fieldValue) >= Number(requiredValue);
-			case '<':
-				return Number(fieldValue) < Number(requiredValue);
-			case '<=':
-				return Number(fieldValue) <= Number(requiredValue);
-			case 'contains':
-				return fieldStr.toLowerCase().includes(requiredStr.toLowerCase());
-			case 'not_contains':
-				return !fieldStr.toLowerCase().includes(requiredStr.toLowerCase());
-			case 'starts_with':
-				return fieldStr.toLowerCase().startsWith(requiredStr.toLowerCase());
-			case 'ends_with':
-				return fieldStr.toLowerCase().endsWith(requiredStr.toLowerCase());
-			case 'empty':
-				return fieldStr.trim() === '';
-			case 'not_empty':
-				return fieldStr.trim() !== '';
-			case 'in':
-				// For array/comma-separated values: requiredValue = "option1,option2,option3"
-				const options = requiredStr.split(',').map(opt => opt.trim());
-				return options.includes(fieldStr);
-			case 'not_in':
-				const notOptions = requiredStr.split(',').map(opt => opt.trim());
-				return !notOptions.includes(fieldStr);
-			default:
-				return false;
-		}
-	}
-	getFieldValue(form, fieldName) {
-		const field = form.querySelector(`[name="${fieldName}"]`);
-		if (!field) return null;
-
-		if (field.type === 'radio') {
-			const checked = form.querySelector(`[name="${fieldName}"]:checked`);
-			return checked ? checked.value : null;
-		} else if (field.type === 'checkbox') {
-			return field.checked ? (field.value || '1') : '';
-		}
-
-		return field.value;
-	}
-
-	toggleFieldVisibility(field, show) {
-		const wrapper = field.closest('.field, fieldset');
-		if (!wrapper) return;
-
-		wrapper.hidden = !show;
-		wrapper.querySelectorAll('input, select, textarea').forEach(control => {
-			control.hidden = !show;
-			control.disabled = !show;
-		});
-	}
-	/**************************************************
-	 *
-	 * REPEATER FUNCTIONALITY
-	 *
-	 **************************************************/
-	initRepeaterFields(form) {
-		form.querySelectorAll('.repeater').forEach(repeater => {
-			this.hasRepeaters = true;
-			const addButton = repeater.querySelector('.add-repeater-row');
-			let temp = repeater.querySelector('template').className;
-			let template = window.getTemplate(temp);
-			const container = repeater.querySelector('.repeater-items');
-
-			if (!addButton || !template || !container) {
-				console.warn('Missing required repeater elements:',
-					{addButton, template, container});
-				return;
-			}
-
-			// Initialize Sortable
-			new Sortable(container, {
-				handle: '.repeater-row-header',
-				animation: 150,
-				onEnd: () => {
-					this.updateRepeaterOrder(repeater);
-				}
-			});
-
-		});
-	}
-	addRepeaterRow(repeater, formConfig) {
-		let rows = repeater.querySelector('.repeater-items');
-		let index = rows.children.length;
-
-		let row = window.getTemplate(repeater.querySelector('template').className);
-
-		// Set a proper ID for the row
-		row.id = `${formConfig.id}-${repeater.dataset.field}-row-${index}`;
-
-		let base = repeater.dataset.field;
-		row.querySelectorAll('input, select, textarea').forEach((field) => {
-			let label = field.nextElementSibling;
-			if (!label || label.tagName !== 'LABEL') {
-				label = field.closest('.field')?.querySelector(`label`);
-			}
-			let id = `${base}:${index}:${field.name}-${field.value}`;
-			let name = `${base}:${index}:${field.name}`;
-
-			[field.name, field.id, label.htmlFor] = [name, id, id];
-		});
-
-		[row.dataset.index, row.querySelector('.row-number').textContent] = [index, `#${index + 1}`];
-		rows.append(row);
-
-		// Focus the first input in the new row
-		const firstInput = row.querySelector('input, select, textarea');
-		if (firstInput) {
-			firstInput.focus();
-		}
-	}
-
-	removeRepeaterRow(removeButton, formConfig) {
-		let repeater = removeButton.closest('.repeater');
-		removeButton.closest('.repeater-row').remove();
-		this.updateRepeaterOrder(repeater);
-	}
-	updateRepeaterOrder(repeater) {
-		requestAnimationFrame(() => {
-			let items = repeater.querySelector('.repeater-items');
-			let updates = [];
-
-			let base = repeater.dataset.field;
-			const form = repeater.closest('form');
-			const formConfig = this.forms.get(form.dataset.formId);
-
-			// Update field names and row numbers
-			items.querySelectorAll('.repeater-row').forEach((row, index) => {
-				updates.push(() => {
-					[
-						row.dataset.index,
-						row.querySelector('.row-number').textContent
-					] = [
-						index,
-						`#${index + 1}`
-					];
-
-					row.querySelectorAll('[name]').forEach(field => {
-						let name = field.name.split(':').pop();
-						let newName = `${base}:${index}:${name}`;
-						let newId = `${base}-${index}-${name}-${field.value}`;
-						[
-							field.name,
-							field.id
-						] = [
-							newName,
-							newId
-						];
-						let label = field.closest('.field').querySelector('label');
-						if (label) {
-							label.htmlFor = newId;
-						}
-					});
-				});
-			});
-
-			updates.forEach(update => update());
-
-			// Schedule save after reorder
-			this.scheduleSave(formConfig, {
-				type: 'field',
-				fieldName: base,
-				reason: 'reorder',
-				delay: this.repeaterDelays.reorder
-			});
-		});
-	}
-
-	getRepeaterChangeDelay(field, eventType) {
-		// Text inputs get longer delays to allow for typing
-		if ((field.type === 'text' || field.type === 'textarea') && eventType === 'input') {
-			return this.repeaterDelays.typing;
-		}
-
-		// Other input types get standard change delay
-		if (eventType === 'change') {
-			return this.repeaterDelays.change;
-		}
-
-		// Blur events get shorter delay
-		if (eventType === 'blur') {
-			return this.repeaterDelays.blur;
-		}
-
-		return this.repeaterDelays.change;
-	}
-
-	generateRowId(repeaterRow) {
-		if (repeaterRow.id) return repeaterRow.id;
-
-		// Generate ID based on form and row position
-		const form = repeaterRow.closest('form');
-		const repeater = repeaterRow.closest('.repeater');
-		const index = Array.from(repeater.querySelectorAll('.repeater-row')).indexOf(repeaterRow);
-		const repeaterId = repeater.dataset.field || 'repeater';
-
-		const generatedId = `${form.dataset.formId}-${repeaterId}-row-${index}`;
-		repeaterRow.id = generatedId;
-		return generatedId;
-	}
-	/*****************************************************
-	 *
-	 * FIELD POPULATION (used by CRUD edit/bulk-edit modals and table view)
-	 *
-	 *****************************************************/
-	/**
-	 * Populate form fields with data values
-	 * @param {HTMLFormElement} form - The form element
-	 * @param {Object} fieldsData - Field values from API
-	 * @param {Object} imagesData - Image metadata (optional)
-	 * @param {Object} options - Additional options
-	 */
-	populateFormFields(form, fieldsData, imagesData = {}, options = {}) {
-		const formConfig = this.forms.get(form.dataset.formId);
-
-		// Build field type cache once
-		if (!formConfig.fieldTypeCache) {
-			formConfig.fieldTypeCache = new Map();
-			form.querySelectorAll('.field').forEach(field => {
-				const fieldName = field.dataset.field;
-				if (fieldName) {
-					formConfig.fieldTypeCache.set(fieldName, this.getFieldType(field));
-				}
-			});
-		}
-
-		// Use cached types
-		for (let [fieldName, fieldValue] of Object.entries(fieldsData)) {
-			let fieldWrapper = form.querySelector(`[data-field=${fieldName}]`);
-			if (fieldWrapper) {
-				this.populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData, options);
-			}
-		}
-	}
-
-	/**
-	 * Populate a single field with its value
-	 * @param {HTMLElement} fieldWrapper - The field wrapper element
-	 * @param {string} fieldName - Field name
-	 * @param {*} fieldValue - Field value
-	 * @param {Object} imagesData - Image metadata
-	 * @param {Object} options - Additional options
-	 */
-	populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData = {}, options = {}) {
-		if (!fieldWrapper || fieldValue === undefined || fieldValue === null) {
-			return;
-		}
-
-		// Determine field type from classes or data attributes
-		const fieldType = this.getFieldType(fieldWrapper);
-
-		switch (fieldType) {
-			case 'image':
-				this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
-				break;
-
-			case 'gallery':
-				this.populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData);
-				break;
-
-			case 'repeater':
-				this.populateRepeaterField(fieldWrapper, fieldName, fieldValue, options);
-				break;
-
-			case 'taxonomy':
-				this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'user':
-				this.populateUserField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'location':
-				this.populateLocationField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'set':
-			case 'checkbox':
-				this.populateSetField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'select':
-			case 'radio':
-				this.populateSelectField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'true_false':
-				this.populateBooleanField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'date':
-			case 'time':
-			case 'datetime':
-				this.populateDateField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'number':
-				this.populateNumberField(fieldWrapper, fieldName, fieldValue);
-				break;
-			case 'textarea':
-				if (fieldWrapper.querySelector('.editor-container')) {
-					this.populateEditorField(fieldWrapper, fieldName, fieldValue);
-				} else {
-					this.populateTextareaField(fieldWrapper, fieldName, fieldValue);
-				}
-				break;
-
-			case 'text':
-			case 'email':
-			case 'url':
-			case 'tel':
-			case 'phone':
-			default:
-				this.populateTextField(fieldWrapper, fieldName, fieldValue);
-				break;
-		}
-	}
-
-	/**
-	 * Determine field type from wrapper element
-	 * @param {HTMLElement} fieldWrapper - Field wrapper element
-	 * @returns {string} Field type
-	 */
-	getFieldType(fieldWrapper) {
-		// Check for specific field classes
-		const typeClasses = [
-			'image', 'gallery', 'repeater', 'taxonomy', 'user', 'location',
-			'set', 'checkbox', 'select', 'radio', 'true_false', 'date',
-			'time', 'datetime', 'editor', 'number', 'text', 'textarea',
-			'email', 'url', 'tel', 'phone'
-		];
-
-		for (const type of typeClasses) {
-			if (fieldWrapper.classList.contains(type)) {
-				return type;
-			}
-		}
-
-		// Check for data attribute
-		if (fieldWrapper.dataset.type) {
-			return fieldWrapper.dataset.type;
-		}
-
-		// Check input type
-		const input = fieldWrapper.querySelector('input, select, textarea');
-		if (input) {
-			if (input.tagName === 'TEXTAREA') {
-				return input.dataset.editor === 'true' ? 'editor' : 'textarea';
-			}
-			if (input.type) {
-				return input.type === 'checkbox' && !fieldWrapper.classList.contains('true_false') ? 'set' : input.type;
-			}
-		}
-
-		return 'text';
-	}
-
-	/**
-	 * Populate text-based fields
-	 */
-	populateTextField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input, textarea`);
-		if (input) {
-			input.value = String(fieldValue || '');
-
-			// Update character counter if present
-			if (input.dataset.limit) {
-				const counter = fieldWrapper.querySelector('.char-count .current');
-				if (counter) {
-					counter.textContent = input.value.length;
-				}
-			}
-		}
-	}
-
-	/**
-	 * Populate textarea fields
-	 */
-	populateTextareaField(fieldWrapper, fieldName, fieldValue) {
-		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
-			fieldWrapper.querySelector('textarea:not([data-editor="true"])');
-
-		if (textarea) {
-			const oldValue = textarea.value;
-			textarea.value = String(fieldValue || '');
-
-			// Trigger change event to update any dependencies
-			textarea.dispatchEvent(new Event('change', { bubbles: true }));
-
-			// Update character counter if present
-			if (textarea.dataset.limit) {
-				const counter = fieldWrapper.querySelector('.char-count .current');
-				if (counter) {
-					counter.textContent = textarea.value.length;
-
-					// Check if limit is reached
-					const limit = parseInt(textarea.dataset.limit, 10);
-					if (textarea.value.length >= limit) {
-						fieldWrapper.classList.add('reached');
-					} else {
-						fieldWrapper.classList.remove('reached');
-					}
-				}
-			}
-		} else {
-			console.warn(`No textarea found for field ${fieldName} in wrapper:`, fieldWrapper);
-		}
-	}
-
-
-
-	/**
-	 * Populate number fields
-	 */
-	populateNumberField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="number"]`);
-		if (input) {
-			input.value = Number(fieldValue) || 0;
-		}
-	}
-
-	/**
-	 * Populate boolean/true_false fields
-	 */
-	populateBooleanField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="checkbox"]`);
-		if (input) {
-			input.checked = Boolean(fieldValue);
-		}
-	}
-
-	/**
-	 * Populate select/radio fields
-	 */
-	populateSelectField(fieldWrapper, fieldName, fieldValue) {
-		const value = String(fieldValue || '');
-
-		// Try select first
-		const select = fieldWrapper.querySelector(`select[name="${fieldName}"]`);
-		if (select) {
-			select.value = value;
-			return;
-		}
-
-		// Try radio buttons
-		const radio = fieldWrapper.querySelector(`input[type="radio"][name="${fieldName}"][value="${value}"]`);
-		if (radio) {
-			radio.checked = true;
-		}
-	}
-
-	/**
-	 * Populate set/checkbox fields (multiple selections)
-	 */
-	populateSetField(fieldWrapper, fieldName, fieldValue) {
-		// Parse value if it's a string
-		let values = fieldValue;
-		if (typeof fieldValue === 'string') {
-			try {
-				values = JSON.parse(fieldValue);
-			} catch (e) {
-				values = fieldValue.split(',').map(v => v.trim());
-			}
-		}
-
-		if (!Array.isArray(values)) {
-			values = [String(values)];
-		}
-
-		// Update checkboxes
-		fieldWrapper.querySelectorAll(`input[type="checkbox"][name*="${fieldName}"]`).forEach(checkbox => {
-			checkbox.checked = values.includes(checkbox.value);
-		});
-	}
-
-	/**
-	 * Populate date/time fields
-	 */
-	populateDateField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input`);
-		if (input && fieldValue) {
-			// Handle different date formats
-			let dateValue = fieldValue;
-			if (typeof fieldValue === 'object' && fieldValue.date) {
-				dateValue = fieldValue.date;
-			}
-
-			// Convert to appropriate format for input type
-			try {
-				const date = new Date(dateValue);
-				if (!isNaN(date.getTime())) {
-					switch (input.type) {
-						case 'date':
-							input.value = date.toISOString().split('T')[0];
-							break;
-						case 'time':
-							input.value = date.toTimeString().slice(0, 5);
-							break;
-						case 'datetime-local':
-							input.value = date.toISOString().slice(0, 16);
-							break;
-						default:
-							input.value = dateValue;
-					}
-				}
-			} catch (e) {
-				input.value = dateValue;
-			}
-		}
-	}
-
-	/**
-	 * Populate editor fields (Quill)
-	 */
-	populateEditorField(fieldWrapper, fieldName, fieldValue) {
-		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
-			fieldWrapper.querySelector('textarea[data-editor="true"]') ||
-			fieldWrapper.querySelector('textarea');
-
-		if (!textarea) {
-			console.warn(`Editor field ${fieldName}: textarea not found`);
-			return;
-		}
-
-		const content = String(fieldValue || '');
-
-		// Update the textarea value
-		textarea.value = content;
-
-		// Try to find and update Quill editor
-		const editorContainer = fieldWrapper.querySelector('.editor');
-		if (editorContainer) {
-			// Try different ways to access the Quill instance
-			let quillInstance = null;
-
-			// Method 1: Check if Quill is stored on the editor element
-			if (editorContainer.__quill) {
-				quillInstance = editorContainer.__quill;
-			}
-			// Method 2: Check if Quill is stored as quill property
-			else if (editorContainer.quill) {
-				quillInstance = editorContainer.quill;
-			}
-			// Method 3: Try to find Quill in the global registry (if you have one)
-			else if (window.Quill && window.Quill.find) {
-				quillInstance = window.Quill.find(editorContainer);
-			}
-			// Method 4: Check all Quill instances if available
-			else if (window.Quill && window.Quill.instances) {
-				// Some setups store instances in a registry
-				for (let instance of window.Quill.instances) {
-					if (instance.container === editorContainer) {
-						quillInstance = instance;
-						break;
-					}
-				}
-			}
-
-			if (quillInstance) {
-				// Set the content in Quill
-				quillInstance.root.innerHTML = content;
-				// Store the instance reference for future use
-				editorContainer.__quill = quillInstance;
-			} else {
-				console.warn(`Quill instance not found for ${fieldName}, setting HTML directly`);
-				// Fallback: set HTML directly
-				editorContainer.innerHTML = content;
-			}
-		} else {
-			console.warn(`Editor container not found for ${fieldName}`);
-		}
-
-		// Trigger change event on textarea
-		textarea.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	/**
-	 * Populate location fields
-	 */
-	populateLocationField(fieldWrapper, fieldName, fieldValue) {
-		if (!fieldValue || typeof fieldValue !== 'object') {
-			return;
-		}
-
-		// Location fields typically have sub-fields
-		const subFields = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country'];
-
-		subFields.forEach(subField => {
-			if (fieldValue[subField] !== undefined) {
-				const input = fieldWrapper.querySelector(`[name="${fieldName}_${subField}"], [name="${subField}"]`);
-				if (input) {
-					input.value = String(fieldValue[subField] || '');
-				}
-			}
-		});
-	}
-
-	/**
-	 * Populate taxonomy fields
-	 */
-	populateTaxonomyField(fieldWrapper, fieldName, fieldValue) {
-		// Handle different value formats
-		let termIds = [];
-
-		if (Array.isArray(fieldValue)) {
-			termIds = fieldValue.map(v => String(v));
-		} else if (typeof fieldValue === 'string') {
-			try {
-				const parsed = JSON.parse(fieldValue);
-				termIds = Array.isArray(parsed) ? parsed.map(v => String(v)) : [String(parsed)];
-			} catch (e) {
-				termIds = fieldValue.split(',').map(v => v.trim());
-			}
-		} else if (fieldValue) {
-			termIds = [String(fieldValue)];
-		}
-
-		if (termIds.length === 0) {
-			return;
-		}
-
-		// Update hidden input
-		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
-		if (hiddenInput) {
-			hiddenInput.value = termIds.join(',');
-		}
-
-		// Update taxonomy selector if present
-		if (fieldWrapper.dataset.fieldId && window.jvbSelector) {
-			window.jvbSelector.updateFieldFromInput(fieldWrapper.dataset.fieldId);
-		}
-	}
-
-	/**
-	 * Populate user fields (similar to taxonomy)
-	 */
-	populateUserField(fieldWrapper, fieldName, fieldValue) {
-		// Similar logic to taxonomy fields
-		this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
-	}
-
-	/**
-	 * Populate image fields
-	 */
-	populateImageField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
-		if (!fieldValue) {
-			return;
-		}
-
-		// Handle comma-separated IDs or single ID
-		const imageIds = String(fieldValue).split(',').filter(id => parseInt(id.trim()));
-		if (imageIds.length === 0) {
-			return;
-		}
-
-		// Update hidden input
-		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
-		if (hiddenInput) {
-			hiddenInput.value = imageIds.join(',');
-		}
-
-		// Update image display
-		const grid = fieldWrapper.querySelector('.item-grid');
-		const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
-
-		if (grid) {
-			imageIds.forEach(imageId => {
-				let image = window.getTemplate('uploadItem');
-				let img = image.querySelector('img');
-
-				let details = image.querySelector('details');
-				let meta = window.getTemplate('uploadMeta')
-				details.append(meta);
-				[
-					img.src,
-					img.alt,
-					image.querySelector('[name="image-title"]').value,
-					image.querySelector('[name="image-alt-text"]').value,
-					image.querySelector('[name="image-caption"]').value,
-				] = [
-					imagesData[imageId].medium,
-					imagesData[imageId].alt,
-					imagesData[imageId].title,
-					imagesData[imageId].alt,
-					imagesData[imageId].caption,
-				];
-
-				details.querySelector('.upload-meta > .hint')?.remove();
-
-				grid.append(image);
-			});
-
-			if (imageIds.length > 0) {
-				if (uploadContainer) {
-					uploadContainer.hidden = true;
-				}
-			}
-		}
-	}
-
-	/**
-	 * Populate gallery fields
-	 */
-	populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
-		this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
-	}
-
-	/**
-	 * Populate repeater fields
-	 */
-	populateRepeaterField(fieldWrapper, fieldName, fieldValue, options = {}) {
-		if (!fieldValue || !Array.isArray(fieldValue)) {
-			return;
-		}
-
-		const container = fieldWrapper.querySelector('.repeater-items');
-		const template = fieldWrapper.querySelector('template');
-
-		if (!container || !template) {
-			console.warn(`Repeater field ${fieldName}: missing container or template`);
-			return;
-		}
-
-		// Clear existing rows
-		window.removeChildren(container);
-
-		// Create rows for each data item
-		fieldValue.forEach((rowData, index) => {
-			if (!rowData || typeof rowData !== 'object') {
-				return;
-			}
-
-			const row = window.getTemplate(template.className);
-			if (!row) {
-				console.warn(`Repeater field ${fieldName}: template not found`);
-				return;
-			}
-
-			// Set row ID and update row number
-			row.id = `${fieldWrapper.closest('form').id}-${fieldName}-row-${index}`;
-			row.dataset.index = index;
-
-			const rowNumber = row.querySelector('.row-number');
-			if (rowNumber) {
-				rowNumber.textContent = `#${index + 1}`;
-			}
-
-			// Update field names and populate values
-			row.querySelectorAll('input, select, textarea').forEach(field => {
-				const originalName = field.name;
-				const newName = `${fieldName}:${index}:${originalName}`;
-				const newId = `${fieldName}-${index}-${originalName}-${field.value}`;
-
-				// Update field identifiers
-				field.name = newName;
-				field.id = newId;
-
-				// Update label
-				const label = field.nextElementSibling;
-				if (label && label.tagName === 'LABEL') {
-					label.htmlFor = newId;
-				}
-
-				// Populate field value
-				if (rowData[originalName] !== undefined) {
-					this.populateRepeaterFieldValue(field, originalName, rowData[originalName]);
-				}
-			});
-
-			container.appendChild(row);
-		});
-	}
-
-	/**
-	 * Populate individual repeater field value
-	 */
-	populateRepeaterFieldValue(field, fieldName, fieldValue) {
-		switch (field.type) {
-			case 'checkbox':
-				field.checked = Boolean(fieldValue);
-				break;
-			case 'radio':
-				field.checked = field.value === String(fieldValue);
-				break;
-			case 'select-one':
-			case 'select-multiple':
-				field.value = String(fieldValue || '');
-				break;
-			default:
-				field.value = String(fieldValue || '');
-		}
-	}
-	/*****************************************************
-	 *
-	 * QUILL
-	 *
-	 *****************************************************/
-	initQuillEditor(formConfig) {
-		let form = formConfig.element;
-		const textareas = form.querySelectorAll('textarea[data-editor=true]');
-
-		textareas.forEach(textarea => {
-			let container, editor, toolbar;
-			//create it if it doesn't exist
-			if(!textarea.parentNode.querySelector('.editor-container')){
-				container = document.createElement('div');
-				container.className = 'editor-container';
-
-				editor = document.createElement('div');
-				editor.className = 'editor';
-				toolbar = document.createElement('div');
-				toolbar.className = 'toolbar';
-				const image = textarea.dataset.allowimage === true ? `<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>` : '';
-				toolbar.id = `toolbar-${textarea.id}`;
-				toolbar.innerHTML = `
-                <span class="ql-formats">
-                    <button type="button" class="ql-p">
-                        ${jvbSettings.icons.paragraph}
-                    </button>
-                    <button type="button" class="ql-h1">
-                        ${jvbSettings.icons.h1}
-                    </button>
-                    <button type="button" class="ql-h2">
-                        ${jvbSettings.icons.h2}
-                    </button>
-                    <button type="button" class="ql-h3">
-                        ${jvbSettings.icons.h3}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                    <button type="button" class="ql-jvb_bold">
-                        ${jvbSettings.icons['bold']}
-                    </button>
-                    <button type="button" class="ql-jvb_italic">
-                        ${jvbSettings.icons['italic']}
-                    </button>
-                    <button type="button" class="ql-jvb_underline">
-                        ${jvbSettings.icons['underline']}
-                    </button>
-                    <button type="button" class="ql-jvb_strike">
-                        ${jvbSettings.icons['strike']}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                     <button type="button" class="ql-jvb_list" value="bullet">
-                        ${jvbSettings.icons['list-bullets']}
-                    </button>
-                    <button type="button" class="ql-jvb_list" value="ordered">
-                        ${jvbSettings.icons['list-numbers']}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                     <button type="button" class="ql-jvb_align" value="left">
-                        ${jvbSettings.icons['align-left']}
-                    </button>
-                     <button type="button" class="ql-jvb_align" value="center">
-                        ${jvbSettings.icons['align-center']}
-                    </button>
-                     <button type="button" class="ql-jvb_align" value="right">
-                        ${jvbSettings.icons['align-right']}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                     <button type="button" class="ql-jvb_link">
-                        ${jvbSettings.icons.link}
-                    </button>
-                    ${image}
-                </span>
-            `;
-
-				container.appendChild(toolbar);
-				container.appendChild(editor);
-				textarea.parentNode.insertBefore(container, textarea);
-				textarea.style.display = 'none';
-				editor.innerHTML = textarea.value;
-			}else{
-				container = textarea.parentNode.querySelector('.editor-container');
-				editor = container.querySelector('.editor');
-				toolbar = container.querySelector('.toolbar');
-			}
-
-
-
-			const quill = new Quill(editor, {
-				theme: 'snow',
-				modules: {
-					toolbar: {
-						container: toolbar,
-						handlers: {
-							p: function() { this.quill.format('header', false); },
-							h1: function() { this.quill.format('header', 1); },
-							h2: function() { this.quill.format('header', 2); },
-							h3: function() { this.quill.format('header', 3); },
-							jvb_bold: function() {this.quill.format('bold', true)},
-							jvb_italic: function() {this.quill.format('italic', true)},
-							jvb_strike: function() {this.quill.format('strike', true)},
-							jvb_underline: function() {this.quill.format('underline', true)},
-							'jvb_align': function(value) {
-								this.quill.format('align', value === this.quill.getFormat().list ? false : value);
-							},
-							'jvb_list': function(value) {
-								this.quill.format('list', value === this.quill.getFormat().list ? false : value);
-							},
-							'jvb_link': function(value) {
-								if (value) {
-									const range = this.quill.getSelection();
-									if (range == null || range.length === 0) return;
-									// Get the existing link if any
-									const preview = this.quill.getText(range.index, range.length);
-									const existingLink = this.quill.getFormat(range).link;
-
-									// Create modal for link input
-									const modal = document.createElement('dialog');
-									modal.className = 'quill-link-modal';
-									modal.innerHTML = `
-                                    <div class="quill-link-modal-content ">
-                                        <label for="link">Enter URL</label>
-                                        <input type="url" id="link" placeholder="Enter URL" value="${existingLink || ''}" />
-                                        <div class="buttons">
-                                            <button type="button" class="save">Save</button>
-                                            ${existingLink ? '<button type="button" class="remove">Remove</button>' : ''}
-                                            <button type="button" class="cancel">Cancel</button>
-                                        </div>
-                                    </div>
-                                `;
-
-									document.body.appendChild(modal);
-									modal.showModal();
-									const input = modal.querySelector('input');
-									input.focus();
-
-									// Handle save
-									modal.querySelector('.save').addEventListener('click', () => {
-										const url = input.value;
-										if (url) {
-											this.quill.format('link', url);
-										}
-										modal.remove();
-									});
-
-									// Handle remove if link exists
-									const removeBtn = modal.querySelector('.remove');
-									if (removeBtn) {
-										removeBtn.addEventListener('click', () => {
-											this.quill.format('link', false);
-											modal.remove();
-										});
-									}
-
-									// Handle cancel
-									modal.querySelector('.cancel').addEventListener('click', () => {
-										modal.remove();
-									});
-
-									// Handle Enter key
-									input.addEventListener('keyup', (e) => {
-										if (e.key === 'Enter') {
-											const url = input.value;
-											if (url) {
-												this.quill.format('link', url);
-											}
-											modal.remove();
-										}
-									});
-								}
-							},
-							'jvb_image': function() {
-								const input = document.createElement('input');
-								input.setAttribute('type', 'file');
-								input.setAttribute('accept', 'image/jpeg,image/png,image/gif,image/webp');
-								input.style.display = 'none';
-								document.body.appendChild(input);
-
-								input.onchange = async (e) => {
-									const file = e.target.files?.[0];
-									if (!file) return;
-
-									// Validate file
-									const maxSize = 5242880; // 5MB
-									if (file.size > maxSize) {
-										this.quill.insertText(range.index, 'File too large. Maximum size is 5MB', {
-											'color': '#f00',
-											'italic': true
-										}, true);
-										input.remove();
-										return;
-									}
-
-									const range = this.quill.getSelection(true);
-									const formData = new FormData();
-									formData.append('image', file);
-
-									if (objectID) {
-										formData.append('post_id', objectID);
-									}
-
-									// Show loading state
-									if (window.jvbLoading) {
-										window.jvbLoading.showLoading('Uploading image...', 'Processing Upload');
-									}
-
-									try {
-										const response = await fetch(
-											`${jvbSettings.api}uploads/`,
-											{
-												method: 'POST',
-												headers: {
-													'X-WP-Nonce': jvbSettings.nonce
-												},
-												body: formData
-											}
-										);
-
-										if (!response.ok) {
-											throw new Error('Upload failed');
-										}
-
-										const result = await response.json();
-
-										// Insert the image at cursor position
-										this.quill.insertEmbed(range.index, 'image', result.url);
-
-									} catch (error) {
-										this.handleError('Upload error:', error);
-										this.quill.insertText(range.index, 'Failed to upload image. Please try again.', {
-											'color': '#f00',
-											'italic': true
-										}, true);
-									} finally {
-										if (window.jvbLoading) {
-											window.jvbLoading.hide();
-										}
-										input.remove();
-									}
-								};
-
-								input.click();
-							}
-						}
-					},
-					history: {
-						delay: 2000,
-						maxStack: 500
-					},
-					clipboard: {
-						matchVisual: false
-					}
-				}
-			});
-
-			quill.on('selection-change', function(range) {
-				const alignmentTools = toolbar.querySelector('.ql-align');
-				if (alignmentTools) {
-					if (range && range.length === 0) {
-						// Get the focused element
-						const [leaf] = this.quill.getLeaf(range.index);
-						if (leaf && leaf.domNode && leaf.domNode.tagName === 'IMG') {
-							alignmentTools.style.display = 'inline-block';
-							return;
-						}
-					}
-					alignmentTools.style.display = 'none';
-				}
-			});
-			// Update hidden textarea and trigger form change
-			quill.on('text-change', () => {
-				textarea.value = quill.root.innerHTML;
-				textarea.dispatchEvent(new Event('change', { bubbles: true }));
-			});
-		});
-	}
-
-	/*****************************************************
-	 *
-	 * CHARACTER LIMITS
-	 *
-	 *****************************************************/
-	initCharacterLimits(formConfig) {
-		formConfig.element.querySelectorAll('input[data-limit], textarea[data-limit]').forEach(input => {
-			const counter = input.closest('.field').querySelector('.char-count .current');
-			if (counter) {
-				const updateCount = () => {
-					const limit = parseInt(input.dataset.limit, 10);
-
-					// Update the counter
-					counter.textContent = input.value.length;
-
-					// If length exceeds limit, truncate the input value
-					if (input.value.length > limit) {
-						input.closest('.field').classList.add('reached');
-						input.value = input.value.substring(0, limit);
-						counter.textContent = limit; // Update counter after truncation
-					}else {
-						input.closest('.field').classList.remove('reached');
-					}
-				};
-
-				input.addEventListener('input', updateCount);
-				updateCount(); // Initial count
-			}
-		})
-	}
-	/*****************************************************
-	 *
-	 * NUMBER FIELDS
-	 *
-	 *****************************************************/
-	handleNumberClick(e, container) {
-		let change = 0;
-		if (e.target.closest('.increase')) {
-			change += 1;
-		} else if (e.target.closest('.decrease')) {
-			change -=1;
-		}
-		if (change !== 0) {
-			let [
-				step,
-				input
-			] = [
-				parseInt(container.dataset.step),
-				container.querySelector('input'),
-			];
-
-			let value = (input.value === '') ? 0 : parseInt(input.value);
-
-			input.value = (value + (step * change * this.stepMultiplier));
-			this.handleNumberLimits(container);
-		}
-	}
-
-	handleNumberLimits(container) {
-		let [
-			min,
-			max,
-			input,
-			increase,
-			decrease
-		] = [
-			container.dataset.min,
-			container.dataset.max,
-			container.querySelector('input'),
-			container.querySelector('.increase'),
-			container.querySelector('.decrease')
-		];
-		let value = parseInt(input.value);
-		if (value < min) {
-			input.value = min;
-			decrease.disabled = true;
-		} else if (value > max) {
-			input.value = max;
-			increase.disabled = false;
-		} else if (increase.disabled) {
-			increase.disabled = false;
-		} else if (decrease.disabled) {
-			decrease.disabled = false;
-		}
-	}
-	/*****************************************************
-	 *
-	 * GALLERY
-	 * TODO: Does the uploader handle this on its own now?
-	 *
-	 *****************************************************/
-	initImageFields(formConfig) {
-		formConfig.element.querySelectorAll('.image').forEach(field => {
-			const fieldName = field.querySelector('input[type="hidden"]').name;
-			const fileInput = field.querySelector('input[type="file"]');
-
-			if (!fileInput) return;
-
-			// Register with centralized upload manager
-			const fieldId = window.jvbUploadManager.registerUploader(field, {
-				content: formConfig.options.content,
-				postId: formConfig.options.itemID,
-				mode: 'direct',
-				uploadType: 'image_upload',
-				fieldName: fieldName,
-				maxFiles: 1,
-				autoSubmit: true,
-
-				// Custom callbacks for this field type
-				onUploadStart: () => {
-					formConfig.state.uploadPending = true;
-				},
-
-				onUploadComplete: (result) => {
-					formConfig.state.uploadPending = false;
-					this.handleImageUploadSuccess(result, field);
-				},
-
-				onUploadError: (error) => {
-					formConfig.state.uploadPending = false;
-					this.handleImageUploadError(error, field);
-				}
-			});
-
-			// Store field reference for cleanup
-			if (!formConfig.uploadFields) {
-				formConfig.uploadFields = new Set();
-			}
-			formConfig.uploadFields.add(fieldId);
-		});
-	}
-	handleImageRemove(field) {
-		const imageDisplay = field.querySelector('.image-display');
-		const img = imageDisplay.querySelector('img');
-		const hiddenInput = field.querySelector('input[type="hidden"]');
-		const uploadContainer = field.querySelector('.file-upload-container');
-
-		// Clear the hidden input
-		hiddenInput.value = '';
-
-		// Reset UI
-		img.src = '';
-		imageDisplay.classList.remove('has-image');
-		uploadContainer.hidden = false;
-
-		// Show notification
-		this.showNotification('Image removed');
-	}
-	handleImageUploadSuccess(result, field) {
-		if (!result.data || !result.data.length) return;
-
-		const imageDisplay = field.querySelector('.image-display');
-		removeChildren(imageDisplay);
-		imageDisplay.classList.add('has-image');
-
-		let ids = [];
-		result.data.forEach(file => {
-			let img = new Image();
-			img.src = file.url;
-			ids.push(file.attachment_id);
-			imageDisplay.appendChild(img);
-		});
-
-		const hiddenInput = field.querySelector('input[type="hidden"]');
-		hiddenInput.value = ids.join(',');
-
-		const uploadContainer = field.querySelector('.file-upload-container');
-		uploadContainer.hidden = true;
-
-		// Trigger form change event
-		hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
-
-		this.showNotification('Image updated successfully');
-	}
-	handleImageUploadError(error, field) {
-		this.handleError('Upload error:', error);
-
-		// Clear upload pending state
-		const form = field.closest('form');
-		if (form && form.dataset.formId) {
-			const formConfig = this.forms.get(form.dataset.formId);
-			if (formConfig) {
-				formConfig.state.uploadPending = false;
-			}
-		}
-
-		this.showNotification('Failed to upload image', 'error');
-
-		// Reset field if needed
-		const uploadContainer = field.querySelector('.file-upload-container');
-		uploadContainer.hidden = false;
-
-		// Clear any error states
-		const errorElement = field.querySelector('.file-error');
-		if (errorElement) {
-			errorElement.textContent = '';
-		}
-	}
-	initGalleryFields(formConfig) {
-		formConfig.element.querySelectorAll('.gallery').forEach(field => {
-			const fieldName = field.querySelector('input[type="hidden"]').name;
-			const previewGrid = field.querySelector('.gallery-preview');
-
-			// Pre-populate existing images
-			if (field.dataset.images) {
-				const urls = field.dataset.images.split(',');
-				urls.forEach(url => {
-					this.addToGalleryPreview(url, previewGrid);
-				});
-			}
-
-			// Register with centralized upload manager
-			const fieldId = window.jvbUploadManager.registerUploader(field, {
-				content: formConfig.options.content,
-				postId: formConfig.options.itemID,
-				mode: 'gallery',
-				uploadType: 'image_upload',
-				fieldName: fieldName,
-				maxFiles: 20,
-				allowMultiple: true,
-				groupable: true,
-
-				onUploadComplete: (result) => {
-					this.handleGalleryUploadSuccess(result, field);
-				},
-			});
-
-			// Store field reference
-			if (!formConfig.uploadFields) {
-				formConfig.uploadFields = new Set();
-			}
-			formConfig.uploadFields.add(fieldId);
-		});
-	}
-
-	handleGalleryUploadSuccess(result, field) {
-		if (!result.data || !result.data.length) return;
-
-		const hiddenInput = field.querySelector('input[type="hidden"]');
-		const previewGrid = field.querySelector('.gallery-preview');
-		const currentIds = hiddenInput.value ? hiddenInput.value.split(',') : [];
-
-		result.data.forEach(file => {
-			currentIds.push(file.attachment_id);
-			this.addToGalleryPreview(file.url, previewGrid);
-		});
-
-		hiddenInput.value = currentIds.join(',');
-		hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
-
-		this.showNotification(`Added ${result.data.length} image(s) to gallery`);
-	}
-	addToGalleryPreview(url, grid) {
-		let preview = window.getTemplate('galleryPreview');
-		let img = preview.querySelector('img');
-		img.src = url;
-
-		grid.appendChild(preview);
-		return preview;
-	}
-	/*****************************************************
-	 *
-	 * 	UPLOAD INTEGRATION
-	 * 	TODO: This may not be necessary. I believe the uploads handles any form changes in the backend
-	 *
-	 *****************************************************/
-	hasActiveUploads(formConfig) {
-		if (!formConfig.uploadFields || !window.jvbUploadManager) {
-			return false;
-		}
-
-		// Check each upload field for active uploads
-		for (const fieldId of formConfig.uploadFields) {
-			const status = window.jvbUploadManager.getFieldStatus(fieldId);
-			if (status && (status.uploading > 0 || status.ready > 0)) {
-				return true;
-			}
-		}
-
-		return false;
-	}
-
-	/***************************************************
-	 *
-	 *
-	 *
-	 ***************************************************/
-	showNotification(msg, type){
-		window.jvbNotifications.showToast(msg, type);
-	}
-
-	getForm(formId) {
-		return this.forms.get(formId);
-	}
-
-	processForm(formId) {
-		const formConfig = this.forms.get(formId);
-		if (formConfig) {
-			this.processChanges(formConfig);
-		}
-	}
-	isFormReadyToSave(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return true;
-
-		return !formConfig.state.uploadPending && !this.hasActiveUploads(formConfig);
-	}
-	getFormUploadProgress(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig || !formConfig.uploadFields) return null;
-
-		let totalUploads = 0;
-		let completedUploads = 0;
-		let failedUploads = 0;
-
-		formConfig.uploadFields.forEach(fieldId => {
-			const status = window.jvbUploadManager.getFieldStatus(fieldId);
-			if (status) {
-				totalUploads += status.uploadCount;
-				completedUploads += status.completed;
-				failedUploads += status.failed;
-			}
-		});
-
-		return {
-			total: totalUploads,
-			completed: completedUploads,
-			failed: failedUploads,
-			pending: totalUploads - completedUploads - failedUploads,
-			progress: totalUploads > 0 ? (completedUploads / totalUploads) * 100 : 0
-		};
-	}
-
-	// Remove form from management
-	removeForm(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		// Clean up regular timeouts
-		if (formConfig.state.saveTimeout) {
-			clearTimeout(formConfig.state.saveTimeout);
-		}
-
-
-		// Clean up upload fields
-		if (formConfig.uploadFields && window.jvbUploadManager) {
-			formConfig.uploadFields.forEach(fieldId => {
-				console.log(`Cleaning up upload field: ${fieldId}`);
-			});
-		}
-
-		// Clean up cached data
-		this.removeCachedForm(formId);
-
-		// Clean up active operations
-		if (formConfig.state.operationId) {
-			this.activeOperations.delete(formConfig.state.operationId);
-		}
-
-		this.forms.delete(formId);
-	}
-
-	cleanup() {
-		// Clean up debouncer
-		if (this.debouncer) {
-			this.debouncer.cleanup();
-		}
-
-		// Clear all maps
-		this.activeRepeaters.clear();
-		this.activeOperations.clear();
-		this.pendingForms.clear();
-
-		// Clean up all forms
-		for (let [formId, formConfig] of this.forms) {
-			this.removeForm(formId);
-		}
-
-		// Remove event listeners
-		if (this.initialized) {
-			document.removeEventListener('submit', this.submitHandler);
-			document.removeEventListener('change', this.changeHandler);
-			document.removeEventListener('click', this.clickHandler);
-			document.removeEventListener('keydown', this.keyHandler);
-			document.removeEventListener('focusin', this.focusHandler);
-			document.removeEventListener('focusout', this.blurHandler);
-			this.initialized = false;
-		}
-	}
-
-	handleError(error, context) {
-		// In production, send to error tracking service
-		if (window.jvbError) {
-			window.jvbError.log(error, context);
-		}
-
-		// In development only
-		if (jvbSettings.debug) {
-			console.error(context, error);
-		}
-	}
-}
-
-// Initialize singleton with auto-scanning
-document.addEventListener('DOMContentLoaded', () => {
-	if (!window.jvbForm) {
-		window.jvbForm = new FormFields();
-	}
-});
-
-
-window.addEventListener('beforeunload', () => window.jvbForm?.cleanup());
diff --git a/assets/js/dash/FormHandlerOld.js b/assets/js/dash/FormHandlerOld.js
deleted file mode 100644
index 3e97702..0000000
--- a/assets/js/dash/FormHandlerOld.js
+++ /dev/null
@@ -1,2406 +0,0 @@
-
-class FormFieldsOld {
-	constructor() {
-		this.forms = new Map(); // Store form configurations
-		this.formIndex = 0;     // Auto-generate IDs
-		this.initialized = false;
-		this.hasRepeaters = false;
-		this.hasNumberFields = false;
-		this.stepMultiplier = 1;
-
-		console.log(jvbSettings.icons);
-		this.timeouts = new Map();
-		this.activeRepeaters = new Map();
-		this.repeaterTimeouts = new Map(); // Form-level repeater timeouts
-		this.repeaterDelays = {
-			change: 6000,      // Delay after field change (longer than current 4250ms)
-			typing: 3000,      // Delay for text inputs while typing
-			blur: 1500,        // Delay after field loses focus
-			add: 500,          // Delay after adding row
-			remove: 800,       // Delay after removing row
-			reorder: 1000      // Delay after reordering rows
-		};
-
-		this.ignore = [
-			'image_temp',
-			'post_thumbnail_temp'
-		];
-
-		// Bind methods for event delegation
-		this.submitHandler = this.handleSubmit.bind(this);
-		this.changeHandler = this.handleChange.bind(this);
-		this.clickHandler = this.handleClick.bind(this);
-		this.focusHandler = this.handleFocus.bind(this);
-		this.keyHandler = this.handleKeys.bind(this);
-		this.blurHandler = this.handleBlur.bind(this);
-
-		this.initListeners();
-	}
-
-	/**
-	 * Add a form to be managed by this instance
-	 * @param {HTMLFormElement} formElement
-	 * @param {Object} options
-	 * @returns {Object} Form configuration
-	 */
-	addForm(formElement, options = {}) {
-		console.log('addingForm:', formElement);
-		// Ensure form has an ID
-		if (!formElement.id) {
-			formElement.id = `form-${this.formIndex++}`;
-		}
-
-		const formConfig = {
-			element: formElement,
-			id: formElement.id,
-			data: this.collectFormData(formElement),
-
-			// Default options with overrides
-			options: {
-				onSave: false,
-				onChange: null,           // Optional change callback
-				onSubmit: null,          // Optional submit callback
-				itemID: null,
-				content: null,
-				saveDelay: 2000,
-				autoSave: true,
-				api: formElement.dataset.save || null,
-				headers: { 'action_nonce': jvbSettings?.dash },
-				...options
-			},
-
-			// Form-specific state
-			state: {
-				saveTimeout: null,
-				lastData: null,
-				isDirty: false
-			},
-			dependencies: new Map(),
-		};
-
-		// Store the configuration
-		this.forms.set(formElement.id, formConfig);
-
-		// Initialize form-specific features
-		this.initFormFeatures(formConfig);
-		return formConfig;
-	}
-
-	/**
-	 * Initialize global event listeners (single delegation point)
-	 */
-	initListeners() {
-		if (this.initialized) return;
-
-		// Use event delegation for all forms
-		document.addEventListener('submit', this.submitHandler);
-		document.addEventListener('change', this.changeHandler);
-
-
-		document.addEventListener('click', this.clickHandler);
-
-		document.addEventListener('keydown', this.keyHandler);
-
-		document.addEventListener('focusin', this.focusHandler);
-		document.addEventListener('focousout', this.blurHandler);
-
-		this.initialized = true;
-	}
-
-	handleKeys(e) {
-		if (!this.hasNumberFields) {
-			return;
-		}
-		if (e.ctrlKey && e.shiftKey) {
-			this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 100, 1000);
-		} else if (e.shiftKey) {
-			this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 10, 1000);
-		} else if (e.key === 'Escape') {
-			this.stepMultiplier = 1;
-		}
-
-	}
-
-	/**
-	 * Global submit handler - routes to appropriate form
-	 */
-	handleSubmit(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.id)) return;
-
-		event.preventDefault();
-		const formConfig = this.forms.get(form.id);
-
-		// Call form-specific submit callback if provided
-		if (formConfig.options.onSubmit) {
-			console.log('sending data back to onSubmit...');
-			formConfig.options.onSubmit(event, this.collectFormData(formConfig.element));
-		} else {
-			console.log('processing Form Changes on submit...');
-			// Default submit behavior
-			this.processFormChanges(formConfig);
-		}
-	}
-
-	/**
-	 * Global change handler - routes to appropriate form
-	 */
-	handleChange(event) {
-		//Image fields handled separately
-		if (window.targetCheck(event, '.image.field')) {
-			return;
-		}
-
-		if (event.target.closest('.repeater-row')) {
-			this.handleRepeaterChange(event);
-			return;
-		}
-		// if (event.target.closest('.repeater-row')) {
-		// 	const repeaterRow = event.target.closest('.repeater-row');
-		// 	const row = repeaterRow.closest('.field.repeater');
-		// 	const form = event.target.closest('form');
-		// 	const formConfig = this.forms.get(form.id);
-		// 	const rowId = repeaterRow.id || this.generateRowId(repeaterRow);
-		//
-		// 	// Clear existing timeout for this row
-		// 	if (this.timeouts.has(rowId)) {
-		// 		clearTimeout(this.timeouts.get(rowId));
-		// 	}
-		//
-		// 	// Set longer timeout as fallback
-		// 	this.timeouts.set(rowId, setTimeout(() => {
-		// 		this.saveRepeaterChanges(form.id, row, 'timeout');
-		// 		this.timeouts.delete(rowId);
-		// 	}, 4250));
-		//
-		// 	// Check conditionals immediately
-		// 	let changed = event.target.name;
-		// 	this.checkConditionals(changed, formConfig);
-		//
-		// 	return;
-		// }
-		// if (!event.isTrusted) {
-		// 	return;
-		// }
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.id)) return;
-
-		const formConfig = this.forms.get(form.id);
-
-		let changed = event.target.name;
-		this.checkConditionals(changed, formConfig);
-
-		// Handle specific field types
-		this.handleSpecialFields(event, formConfig);
-
-		if ('noautosave' in form.dataset) {
-			return;
-		}
-
-		// Auto-save if enabled
-		if (formConfig.options.autoSave) {
-			this.scheduleAutoSave(formConfig);
-		}
-	}
-
-	handleRepeaterChange(event) {
-		const repeaterRow = event.target.closest('.repeater-row');
-		const repeater = repeaterRow.closest('.field.repeater');
-		const form = event.target.closest('form');
-		const formConfig = this.forms.get(form.id);
-
-		if (!formConfig) return;
-
-		const fieldName = repeater.dataset.field;
-		const formId = form.id;
-
-		// Check conditionals immediately
-		const changed = event.target.name;
-		this.checkConditionals(changed, formConfig);
-
-		// Determine appropriate delay based on field type and interaction
-		let delay = this.getRepeaterDelay(event.target, event.type);
-
-		// Clear existing timeout for this repeater field in this form
-		const timeoutKey = `${formId}-${fieldName}`;
-		if (this.repeaterTimeouts.has(timeoutKey)) {
-			clearTimeout(this.repeaterTimeouts.get(timeoutKey));
-		}
-
-		// Set new timeout with appropriate delay
-		this.repeaterTimeouts.set(timeoutKey, setTimeout(() => {
-			this.saveRepeaterChanges(formId, repeater, `${event.type}-timeout`);
-			this.repeaterTimeouts.delete(timeoutKey);
-		}, delay));
-
-		console.log(`Scheduled repeater save for ${fieldName} in ${delay}ms (reason: ${event.type})`);
-	}
-
-	/**
-	 * Force immediate save of all repeater changes for a form
-	 */
-	forceRepeaterSave(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		const form = formConfig.element;
-		const repeaters = form.querySelectorAll('.field.repeater');
-
-		// Clear all pending timeouts
-		this.clearFormRepeaterTimeouts(formId);
-
-		// Process each repeater immediately
-		repeaters.forEach(repeater => {
-			this.processRepeaterChanges(formConfig, repeater);
-		});
-	}
-
-	/**
-	 * Determine appropriate delay for repeater changes
-	 */
-	getRepeaterDelay(field, eventType) {
-		// Text inputs get longer delays to allow for typing
-		if ((field.type === 'text' || field.type === 'textarea') && eventType === 'input') {
-			return this.repeaterDelays.typing;
-		}
-
-		// Other input types get standard change delay
-		if (eventType === 'change') {
-			return this.repeaterDelays.change;
-		}
-
-		// Blur events (when user leaves field) get shorter delay
-		if (eventType === 'blur') {
-			return this.repeaterDelays.blur;
-		}
-
-		// Default to change delay
-		return this.repeaterDelays.change;
-	}
-
-	/**
-	 * Global click handler - routes to appropriate form
-	 */
-	handleClick(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.id)) return;
-
-		const formConfig = this.forms.get(form.id);
-
-		// Handle repeater actions
-		if (event.target.matches('.add-repeater-row')) {
-			const repeater = event.target.closest('.repeater');
-			this.addRepeaterRow(repeater, formConfig);
-
-			// Schedule save after add
-			this.scheduleRepeaterSave(form.id, repeater, 'add', this.repeaterDelays.add);
-
-		} else if (event.target.matches('.remove-row')) {
-			const repeater = event.target.closest('.repeater');
-			this.removeRepeaterRow(event.target, formConfig);
-
-			// Schedule save after remove
-			this.scheduleRepeaterSave(form.id, repeater, 'remove', this.repeaterDelays.remove);
-
-		}  else if (event.target.matches('.remove-image')) {
-			this.handleImageRemove(event.target.closest('.field'));
-		} else if (event.target.matches('.replace-image')) {
-			let fileInput = event.closest('.image').querySelector('input[type="file"]');
-			fileInput.click();
-		} else if (window.targetCheck(event, 'div.quantity')) {
-			let quantity = window.targetCheck(event, 'div.quantity');
-			this.handleNumberClick(event, quantity);
-		}
-		// Add other click handlers as needed
-	}
-
-	/**
-	 * Schedule repeater save with specific timing
-	 */
-	scheduleRepeaterSave(formId, repeater, reason, delay) {
-		const fieldName = repeater.dataset.field;
-		const timeoutKey = `${formId}-${fieldName}`;
-
-		// Clear existing timeout
-		if (this.repeaterTimeouts.has(timeoutKey)) {
-			clearTimeout(this.repeaterTimeouts.get(timeoutKey));
-		}
-
-		// Set new timeout
-		this.repeaterTimeouts.set(timeoutKey, setTimeout(() => {
-			this.saveRepeaterChanges(formId, repeater, reason);
-			this.repeaterTimeouts.delete(timeoutKey);
-		}, delay));
-
-		console.log(`Scheduled repeater save for ${fieldName} in ${delay}ms (reason: ${reason})`);
-	}
-
-	/**
-	 * Handle focus events - track when we enter repeater fields
-	 */
-	handleFocus(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.id)) return;
-
-		const repeaterRow = event.target.closest('.repeater-row');
-		if (!repeaterRow) return;
-
-		const formConfig = this.forms.get(form.id);
-		const rowId = repeaterRow.id || this.generateRowId(repeaterRow);
-
-		// Store the currently active repeater field
-		this.activeRepeaters.set(form.id, {
-			rowId: rowId,
-			element: event.target,
-			formConfig: formConfig
-		});
-	}
-
-	handleBlur(event) {
-		const form = event.target.closest('form');
-		if (!form || !this.forms.has(form.id)) return;
-
-		// Handle repeater field blur with shorter delay
-		if (event.target.closest('.repeater-row')) {
-			const repeater = event.target.closest('.field.repeater');
-			this.scheduleRepeaterSave(form.id, repeater, 'blur', this.repeaterDelays.blur);
-		}
-	}
-
-
-	/**
-	 * Schedule auto-save with debouncing per form
-	 */
-	scheduleAutoSave(formConfig) {
-		// Don't autosave if uploads are pending
-		if (formConfig.state.uploadPending) {
-			console.log('Skipping autosave - uploads pending');
-			return;
-		}
-
-		// Also check global upload status
-		if (this.hasActiveUploads(formConfig)) {
-			console.log('Skipping autosave - active uploads detected');
-			return;
-		}
-
-		// Clear existing timeout for this specific form
-		if (formConfig.state.saveTimeout) {
-			clearTimeout(formConfig.state.saveTimeout);
-		}
-
-		// Set new timeout for this form
-		formConfig.state.saveTimeout = setTimeout(() => {
-			// Double-check upload status before saving
-			if (!formConfig.state.uploadPending && !this.hasActiveUploads(formConfig)) {
-				this.processFormChanges(formConfig);
-			}
-		}, formConfig.options.saveDelay);
-	}
-
-	hasActiveUploads(formConfig) {
-		if (!formConfig.uploadFields || !window.jvbUploadManager) {
-			return false;
-		}
-
-		// Check each upload field for active uploads
-		for (const fieldId of formConfig.uploadFields) {
-			const status = window.jvbUploadManager.getFieldStatus(fieldId);
-			if (status && (status.uploading > 0 || status.ready > 0)) {
-				return true;
-			}
-		}
-
-		return false;
-	}
-
-	/**
-	 * Process changes for a specific form
-	 */
-	processFormChanges(formConfig, processSave = true) {
-		console.log('Processing changes...');
-		// Skip if uploads are pending
-		if (formConfig.state.uploadPending || this.hasActiveUploads(formConfig)) {
-			console.log('Skipping form changes processing - uploads pending');
-
-			// Schedule a retry
-			setTimeout(() => {
-				this.processFormChanges(formConfig, processSave);
-			}, 2000);
-
-			return;
-		}
-
-		const newData = this.collectFormData(formConfig.element);
-		const changes = this.getDataChanges(newData, formConfig.data);
-
-		console.log(newData);
-		console.log(changes);
-
-		if (Object.keys(changes).length > 0) {
-			// Update stored data
-			formConfig.data = newData;
-			formConfig.state.isDirty = true;
-
-			// Call the form's save callback
-			if (processSave) {
-				this.handleSave(changes, formConfig);
-			}
-		}
-	}
-
-
-	handleSave(changes, formConfig) {
-		console.log(changes);
-		if (changes.length === 0 || window.isEmptyObject(changes)) {
-			return;
-		}
-		if (typeof formConfig.options.onSave === "function") {
-			formConfig.options.onSave(changes, formConfig);
-		} else if (formConfig.element.dataset.save) {
-			let endpoint = formConfig.element.dataset.save;
-			let title = formConfig.element.dataset.title??endpoint;
-			let operation = {
-				endpoint: endpoint,
-				headers: {
-					'action_nonce': jvbSettings.dash
-				},
-				title: `Adding ${title} to Queue`,
-				popup: `Queueing ${title}...`,
-				data: this.collectFormData(formConfig.element)
-			}
-			window.jvbQueue.addToQueue(operation);
-		}
-	}
-
-	/**
-	 * Initialize form-specific features
-	 */
-	initFormFeatures(formConfig) {
-		const form = formConfig.element;
-
-		// Initialize conditional fields
-		this.initConditionalFields(formConfig);
-
-		// Initialize repeater fields
-		this.initRepeaterFields(form);
-
-		// Initialize special field types
-		if (form.querySelector('[data-editor="true"]')) {
-			this.initQuillEditor(formConfig, formConfig.options.itemID);
-		}
-
-		if (form.querySelector('.gallery')) {
-			this.initGalleryFields(formConfig);
-		}
-
-		if (form.querySelector('.image')) {
-			this.initImageFields(formConfig);
-		}
-
-		if (form.querySelector('[data-limit]')) {
-			this.initCharacterLimits(formConfig);
-		}
-
-		if (form.querySelector('input[type="number"]')) {
-			this.initNumberFields(formConfig);
-		}
-	}
-
-	/**
-	 * Collect form data into a structured object
-	 */
-	collectFormData(form) {
-		const formData = new FormData(form);
-		let data = {};
-		let repeaterData = {};
-
-
-		for (let [key, value] of formData.entries()) {
-			if (this.ignore.includes(key) || key.endsWith('_temp')) {
-				continue;
-			}
-
-			let post = null;
-			let original = null;
-
-			//If we're on a table view, we need to organize this by post ID
-			if (key.includes('|')) {
-				[post, key] = key.split('|');
-				//Temporarily store data to merge later
-				original = data;
-				data = original[post]??{};
-			}
-			if (key.includes(':')) {
-				// Handle repeater fields (field:index:name)
-				let [fieldName, index, rawSubField] = key.split(':');
-
-				// Handle array fields (remove [] brackets)
-				const isArrayField = rawSubField.endsWith('[]');
-				const subField = rawSubField.replace('[]', '');
-
-				// Initialize repeater structure
-				if (!repeaterData[fieldName]) {
-					repeaterData[fieldName] = {};
-				}
-				if (!repeaterData[fieldName][index]) {
-					repeaterData[fieldName][index] = {};
-				}
-
-				// Handle different field types for repeater
-				let fieldValue = value;
-
-				// Only check radio buttons since FormData handles checkboxes correctly
-				const fieldElement = form.querySelector(`[name="${key}"]`);
-				if (fieldElement && fieldElement.type === 'radio' && !fieldElement.checked) {
-					continue; // Skip unchecked radios
-				}
-
-				// Handle array fields (like checkbox groups)
-				if (isArrayField || repeaterData[fieldName][index][subField]) {
-					// Initialize as array if not already
-					if (!repeaterData[fieldName][index][subField]) {
-						repeaterData[fieldName][index][subField] = [];
-					} else if (!Array.isArray(repeaterData[fieldName][index][subField])) {
-						repeaterData[fieldName][index][subField] = [repeaterData[fieldName][index][subField]];
-					}
-					repeaterData[fieldName][index][subField].push(fieldValue);
-				} else {
-					// Single value field
-					repeaterData[fieldName][index][subField] = fieldValue;
-				}
-
-			}  else {
-				// Handle array values (multiple checkboxes/selects)
-				if (data[key]) {
-					if (!Array.isArray(data[key])) {
-						data[key] = [data[key]];
-					}
-					data[key].push(value);
-				} else {
-					data[key] = value;
-				}
-			}
-			if (post) {
-				//Merge back with original
-				original[post] = data;
-				data = original;
-			}
-		}
-
-		// Merge repeater data into main data structure
-		Object.keys(repeaterData).forEach(fieldName => {
-			// Clean up empty rows and convert to array format
-			const cleanedRows = {};
-			Object.keys(repeaterData[fieldName]).forEach(index => {
-				const rowData = repeaterData[fieldName][index];
-				if (Object.keys(rowData).length > 0) {
-					cleanedRows[index] = rowData;
-				}
-			});
-
-			// Convert to sequential array
-			data[fieldName] = Object.values(cleanedRows);
-		});
-
-		return data;
-	}
-
-	/**
-	 * Get differences between old and new data
-	 */
-	getDataChanges(newData, oldData, deep = false) {
-		// Use your existing getDifferences utility or implement comparison logic
-		return window.getDifferences?.map(oldData, newData) || {};
-	}
-
-	/**
-	 * Handle special field types (repeaters, conditionals, etc.)
-	 */
-	handleSpecialFields(event, formConfig) {
-		// Handle repeater field changes
-		if (event.target.closest('.repeater-row')) {
-			this.updateRepeaterOrder(event.target.closest('.repeater'));
-		}
-
-		// Handle conditional field triggers
-		const triggerName = event.target.name;
-		this.checkConditionals(triggerName, formConfig);
-	}
-
-	/**
-	 * Initialize conditional fields for a specific form
-	 */
-	initConditionalFields(formConfig) {
-		const form = formConfig.element;
-		const conditionalFields = form.querySelectorAll('[data-depends-on]');
-
-		conditionalFields.forEach(field => {
-			const changed = field.dataset.dependsOn;
-			const triggerValue = this.getFieldValue(form, changed);
-
-			const requiredValue = field.dataset.dependsValue;
-			const operator = field.dataset.dependsOperator || '==';
-			const shouldShow = this.evaluateCondition(triggerValue, requiredValue, operator);
-
-			this.toggleFieldVisibility(field, shouldShow);
-		});
-	}
-
-	checkConditionals(changed, formConfig) {
-		if (formConfig.dependencies.has(changed) && formConfig.dependencies.get(changed)=== false) {
-			return;
-		}
-		let dependencies = formConfig.element.querySelectorAll(`[data-depends-on="${changed}"]`);
-		formConfig.dependencies.set(changed, (dependencies.length > 0) ? dependencies : false);
-		if (dependencies.length === 0) {
-			return;
-		}
-		this.updateConditionalFields(changed, dependencies, formConfig);
-	}
-	/**
-	 * Update conditional fields based on trigger values
-	 */
-	updateConditionalFields(changed, dependencies, formConfig) {
-
-		const triggerValue = this.getFieldValue(formConfig.element, changed);
-
-		dependencies.forEach(field => {
-			this.checkDependencies(field, triggerValue);
-		});
-	}
-
-	checkDependencies(field, value) {
-		const requiredValue = field.dataset.dependsValue;
-		const operator = field.dataset.dependsOperator || '==';
-		const shouldShow = this.evaluateCondition(value, requiredValue, operator);
-		this.toggleFieldVisibility(field, shouldShow);
-	}
-
-	/**
-	 * Get field value considering different input types
-	 */
-	getFieldValue(form, fieldName) {
-		const field = form.querySelector(`[name="${fieldName}"]`);
-		if (!field) return null;
-
-		if (field.type === 'radio') {
-			const checked = form.querySelector(`[name="${fieldName}"]:checked`);
-			return checked ? checked.value : null;
-		} else if (field.type === 'checkbox') {
-			return field.checked ? (field.value || '1') : '';
-		}
-
-		return field.value;
-	}
-
-	/**
-	 * Evaluate conditional logic
-	 */
-	evaluateCondition(fieldValue, requiredValue, operator) {
-		// Handle null/undefined values
-		if (fieldValue === null || fieldValue === undefined) {
-			fieldValue = '';
-		}
-		if (requiredValue === null || requiredValue === undefined) {
-			requiredValue = '';
-		}
-
-		// Convert both to strings for consistent comparison (since form values are always strings)
-		const fieldStr = String(fieldValue);
-		const requiredStr = String(requiredValue);
-
-		switch (operator) {
-			case '==':
-				return fieldStr == requiredStr; // Loose equality
-			case '!=':
-				return fieldStr != requiredStr; // Loose inequality
-			case '===':
-				return fieldStr === requiredStr; // Strict equality (if needed)
-			case '!==':
-				return fieldStr !== requiredStr; // Strict inequality (if needed)
-			case '>':
-				return Number(fieldValue) > Number(requiredValue);
-			case '>=':
-				return Number(fieldValue) >= Number(requiredValue);
-			case '<':
-				return Number(fieldValue) < Number(requiredValue);
-			case '<=':
-				return Number(fieldValue) <= Number(requiredValue);
-			case 'contains':
-				return fieldStr.toLowerCase().includes(requiredStr.toLowerCase());
-			case 'not_contains':
-				return !fieldStr.toLowerCase().includes(requiredStr.toLowerCase());
-			case 'starts_with':
-				return fieldStr.toLowerCase().startsWith(requiredStr.toLowerCase());
-			case 'ends_with':
-				return fieldStr.toLowerCase().endsWith(requiredStr.toLowerCase());
-			case 'empty':
-				return fieldStr.trim() === '';
-			case 'not_empty':
-				return fieldStr.trim() !== '';
-			case 'in':
-				// For array/comma-separated values: requiredValue = "option1,option2,option3"
-				const options = requiredStr.split(',').map(opt => opt.trim());
-				return options.includes(fieldStr);
-			case 'not_in':
-				const notOptions = requiredStr.split(',').map(opt => opt.trim());
-				return !notOptions.includes(fieldStr);
-			default:
-				return false;
-		}
-	}
-
-
-	/**
-	 * Debug helper to see what's happening
-	 */
-	debugCondition(fieldValue, requiredValue, operator) {
-		console.group('🔍 Conditional Field Debug');
-		console.log('Field Value:', fieldValue, typeof fieldValue);
-		console.log('Required Value:', requiredValue, typeof requiredValue);
-		console.log('Operator:', operator);
-		console.log('String Field:', String(fieldValue));
-		console.log('String Required:', String(requiredValue));
-		console.log('=== comparison:', String(fieldValue) === String(requiredValue));
-		console.log('== comparison:', String(fieldValue) == String(requiredValue));
-		console.log('!== comparison:', String(fieldValue) !== String(requiredValue));
-		console.log('!= comparison:', String(fieldValue) != String(requiredValue));
-
-		const result = this.evaluateCondition(fieldValue, requiredValue, operator);
-		console.log('Final Result:', result);
-		console.groupEnd();
-
-		return result;
-	}
-
-	/**
-	 * Toggle field visibility
-	 */
-	toggleFieldVisibility(field, show) {
-		const wrapper = field.closest('.field, fieldset');
-		if (!wrapper) return;
-
-		wrapper.hidden = !show;
-		wrapper.querySelectorAll('input, select, textarea').forEach(control => {
-			control.disabled = !show;
-		});
-	}
-
-	// Stub methods for specific field types
-	initRepeaterFields(form) {
-		form.querySelectorAll('.repeater').forEach(repeater => {
-			this.hasRepeaters = true;
-			const addButton = repeater.querySelector('.add-repeater-row');
-			let temp = repeater.querySelector('template').className;
-			let template = window.getTemplate(temp);
-			const container = repeater.querySelector('.repeater-items');
-
-			if (!addButton || !template || !container) {
-				console.warn('Missing required repeater elements:',
-					{addButton, template, container});
-				return;
-			}
-
-			// Initialize Sortable
-			new Sortable(container, {
-				handle: '.repeater-row-header',
-				animation: 150,
-				onEnd: () => {
-					this.updateRepeaterOrder(repeater);
-				}
-			});
-
-		});
-	}
-
-	initQuillEditor(formConfig, itemID) {
-		let form = formConfig.element;
-		const textareas = form.querySelectorAll('textarea[data-editor=true]');
-
-		textareas.forEach(textarea => {
-			let container, editor, toolbar;
-			//create it if it doesn't exist
-			if(!textarea.parentNode.querySelector('.editor-container')){
-				container = document.createElement('div');
-				container.className = 'editor-container';
-
-				editor = document.createElement('div');
-				editor.className = 'editor';
-				toolbar = document.createElement('div');
-				toolbar.className = 'toolbar';
-				const image = textarea.dataset.allowimage === true ? `<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>` : '';
-				toolbar.id = `toolbar-${textarea.id}`;
-				toolbar.innerHTML = `
-                <span class="ql-formats">
-                    <button type="button" class="ql-p">
-                        ${jvbSettings.icons.paragraph}
-                    </button>
-                    <button type="button" class="ql-h1">
-                        ${jvbSettings.icons.h1}
-                    </button>
-                    <button type="button" class="ql-h2">
-                        ${jvbSettings.icons.h2}
-                    </button>
-                    <button type="button" class="ql-h3">
-                        ${jvbSettings.icons.h3}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                    <button type="button" class="ql-jvb_bold">
-                        ${jvbSettings.icons['bold']}
-                    </button>
-                    <button type="button" class="ql-jvb_italic">
-                        ${jvbSettings.icons['italic']}
-                    </button>
-                    <button type="button" class="ql-jvb_underline">
-                        ${jvbSettings.icons['underline']}
-                    </button>
-                    <button type="button" class="ql-jvb_strike">
-                        ${jvbSettings.icons['strike']}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                     <button type="button" class="ql-jvb_list" value="bullet">
-                        ${jvbSettings.icons['list-bullets']}
-                    </button>
-                    <button type="button" class="ql-jvb_list" value="ordered">
-                        ${jvbSettings.icons['list-numbers']}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                     <button type="button" class="ql-jvb_align" value="left">
-                        ${jvbSettings.icons['align-left']}
-                    </button>
-                     <button type="button" class="ql-jvb_align" value="center">
-                        ${jvbSettings.icons['align-center']}
-                    </button>
-                     <button type="button" class="ql-jvb_align" value="right">
-                        ${jvbSettings.icons['align-right']}
-                    </button>
-                </span>
-                <span class="ql-formats">
-                     <button type="button" class="ql-jvb_link">
-                        ${jvbSettings.icons.link}
-                    </button>
-                    ${image}
-                </span>
-            `;
-
-				container.appendChild(toolbar);
-				container.appendChild(editor);
-				textarea.parentNode.insertBefore(container, textarea);
-				textarea.style.display = 'none';
-				editor.innerHTML = textarea.value;
-			}else{
-				container = textarea.parentNode.querySelector('.editor-container');
-				editor = container.querySelector('.editor');
-				toolbar = container.querySelector('.toolbar');
-			}
-
-
-
-			const quill = new Quill(editor, {
-				theme: 'snow',
-				modules: {
-					toolbar: {
-						container: toolbar,
-						handlers: {
-							p: function() { this.quill.format('header', false); },
-							h1: function() { this.quill.format('header', 1); },
-							h2: function() { this.quill.format('header', 2); },
-							h3: function() { this.quill.format('header', 3); },
-							jvb_bold: function() {this.quill.format('bold', true)},
-							jvb_italic: function() {this.quill.format('italic', true)},
-							jvb_strike: function() {this.quill.format('strike', true)},
-							jvb_underline: function() {this.quill.format('underline', true)},
-							'jvb_align': function(value) {
-								this.quill.format('align', value === this.quill.getFormat().list ? false : value);
-							},
-							'jvb_list': function(value) {
-								this.quill.format('list', value === this.quill.getFormat().list ? false : value);
-							},
-							'jvb_link': function(value) {
-								if (value) {
-									const range = this.quill.getSelection();
-									if (range == null || range.length === 0) return;
-									// Get the existing link if any
-									const preview = this.quill.getText(range.index, range.length);
-									const existingLink = this.quill.getFormat(range).link;
-
-									// Create modal for link input
-									const modal = document.createElement('dialog');
-									modal.className = 'quill-link-modal';
-									modal.innerHTML = `
-                                    <div class="quill-link-modal-content ">
-                                        <label for="link">Enter URL</label>
-                                        <input type="url" id="link" placeholder="Enter URL" value="${existingLink || ''}" />
-                                        <div class="buttons">
-                                            <button type="button" class="save">Save</button>
-                                            ${existingLink ? '<button type="button" class="remove">Remove</button>' : ''}
-                                            <button type="button" class="cancel">Cancel</button>
-                                        </div>
-                                    </div>
-                                `;
-
-									document.body.appendChild(modal);
-									modal.showModal();
-									const input = modal.querySelector('input');
-									input.focus();
-
-									// Handle save
-									modal.querySelector('.save').addEventListener('click', () => {
-										const url = input.value;
-										if (url) {
-											this.quill.format('link', url);
-										}
-										modal.remove();
-									});
-
-									// Handle remove if link exists
-									const removeBtn = modal.querySelector('.remove');
-									if (removeBtn) {
-										removeBtn.addEventListener('click', () => {
-											this.quill.format('link', false);
-											modal.remove();
-										});
-									}
-
-									// Handle cancel
-									modal.querySelector('.cancel').addEventListener('click', () => {
-										modal.remove();
-									});
-
-									// Handle Enter key
-									input.addEventListener('keyup', (e) => {
-										if (e.key === 'Enter') {
-											const url = input.value;
-											if (url) {
-												this.quill.format('link', url);
-											}
-											modal.remove();
-										}
-									});
-								}
-							},
-							'jvb_image': function() {
-								const input = document.createElement('input');
-								input.setAttribute('type', 'file');
-								input.setAttribute('accept', 'image/jpeg,image/png,image/gif,image/webp');
-								input.style.display = 'none';
-								document.body.appendChild(input);
-
-								input.onchange = async (e) => {
-									const file = e.target.files?.[0];
-									if (!file) return;
-
-									// Validate file
-									const maxSize = 5242880; // 5MB
-									if (file.size > maxSize) {
-										this.quill.insertText(range.index, 'File too large. Maximum size is 5MB', {
-											'color': '#f00',
-											'italic': true
-										}, true);
-										input.remove();
-										return;
-									}
-
-									const range = this.quill.getSelection(true);
-									const formData = new FormData();
-									formData.append('image', file);
-
-									if (objectID) {
-										formData.append('post_id', objectID);
-									}
-
-									// Show loading state
-									if (window.jvbLoading) {
-										window.jvbLoading.showLoading('Uploading image...', 'Processing Upload');
-									}
-
-									try {
-										const response = await fetch(
-											`${jvbSettings.api}uploads/`,
-											{
-												method: 'POST',
-												headers: {
-													'X-WP-Nonce': jvbSettings.nonce
-												},
-												body: formData
-											}
-										);
-
-										if (!response.ok) {
-											throw new Error('Upload failed');
-										}
-
-										const result = await response.json();
-
-										// Insert the image at cursor position
-										this.quill.insertEmbed(range.index, 'image', result.url);
-
-									} catch (error) {
-										console.error('Upload error:', error);
-										this.quill.insertText(range.index, 'Failed to upload image. Please try again.', {
-											'color': '#f00',
-											'italic': true
-										}, true);
-									} finally {
-										if (window.jvbLoading) {
-											window.jvbLoading.hide();
-										}
-										input.remove();
-									}
-								};
-
-								input.click();
-							}
-						}
-					},
-					history: {
-						delay: 2000,
-						maxStack: 500
-					},
-					clipboard: {
-						matchVisual: false
-					}
-				}
-			});
-
-			quill.on('selection-change', function(range) {
-				const alignmentTools = toolbar.querySelector('.ql-align');
-				if (alignmentTools) {
-					if (range && range.length === 0) {
-						// Get the focused element
-						const [leaf] = this.quill.getLeaf(range.index);
-						if (leaf && leaf.domNode && leaf.domNode.tagName === 'IMG') {
-							alignmentTools.style.display = 'inline-block';
-							return;
-						}
-					}
-					alignmentTools.style.display = 'none';
-				}
-			});
-			// Update hidden textarea and trigger form change
-			quill.on('text-change', () => {
-				textarea.value = quill.root.innerHTML;
-				textarea.dispatchEvent(new Event('change', { bubbles: true }));
-			});
-		});
-	}
-	initGalleryFields(formConfig) {
-		formConfig.element.querySelectorAll('.gallery').forEach(field => {
-			const fieldName = field.querySelector('input[type="hidden"]').name;
-			const previewGrid = field.querySelector('.gallery-preview');
-
-			// Pre-populate existing images
-			if (field.dataset.images) {
-				const urls = field.dataset.images.split(',');
-				urls.forEach(url => {
-					this.addToGalleryPreview(url, previewGrid);
-				});
-			}
-
-			// Register with centralized upload manager
-			const fieldId = window.jvbUploadManager.registerUploader(field, {
-				content: formConfig.options.content,
-				postId: formConfig.options.itemID,
-				mode: 'gallery',
-				uploadType: 'image_upload',
-				fieldName: fieldName,
-				maxFiles: 20,
-				allowMultiple: true,
-				groupable: true,
-
-				onUploadComplete: (result) => {
-					this.handleGalleryUploadSuccess(result, field);
-				},
-			});
-
-			// Store field reference
-			if (!formConfig.uploadFields) {
-				formConfig.uploadFields = new Set();
-			}
-			formConfig.uploadFields.add(fieldId);
-		});
-	}
-
-	handleGalleryUploadSuccess(result, field) {
-		console.log('Gallery Upload success!', result);
-
-		if (!result.data || !result.data.length) return;
-
-		const hiddenInput = field.querySelector('input[type="hidden"]');
-		const previewGrid = field.querySelector('.gallery-preview');
-		const currentIds = hiddenInput.value ? hiddenInput.value.split(',') : [];
-
-		result.data.forEach(file => {
-			currentIds.push(file.attachment_id);
-			this.addToGalleryPreview(file.url, previewGrid);
-		});
-
-		hiddenInput.value = currentIds.join(',');
-		hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
-
-		this.showNotification(`Added ${result.data.length} image(s) to gallery`);
-	}
-	addToGalleryPreview(url, grid) {
-		let preview = window.getTemplate('galleryPreview');
-		let img = preview.querySelector('img');
-		img.src = url;
-
-		grid.appendChild(preview);
-		return preview;
-	}
-	initImageFields(formConfig) {
-		formConfig.element.querySelectorAll('.image').forEach(field => {
-			const fieldName = field.querySelector('input[type="hidden"]').name;
-			const fileInput = field.querySelector('input[type="file"]');
-
-			if (!fileInput) return;
-
-			// Register with centralized upload manager
-			const fieldId = window.jvbUploadManager.registerUploader(field, {
-				content: formConfig.options.content,
-				postId: formConfig.options.itemID,
-				mode: 'direct',
-				uploadType: 'image_upload',
-				fieldName: fieldName,
-				maxFiles: 1,
-				autoSubmit: true,
-
-				// Custom callbacks for this field type
-				onUploadStart: () => {
-					formConfig.state.uploadPending = true;
-					console.log('Image upload started - pausing autosave');
-				},
-
-				onUploadComplete: (result) => {
-					formConfig.state.uploadPending = false;
-					this.handleImageUploadSuccess(result, field);
-				},
-
-				onUploadError: (error) => {
-					formConfig.state.uploadPending = false;
-					this.handleImageUploadError(error, field);
-				}
-			});
-
-			// Store field reference for cleanup
-			if (!formConfig.uploadFields) {
-				formConfig.uploadFields = new Set();
-			}
-			formConfig.uploadFields.add(fieldId);
-		});
-	}
-
-
-	handleImageUploadSuccess(result, field) {
-		console.log('Image Upload success!', result);
-
-		if (!result.data || !result.data.length) return;
-
-		const imageDisplay = field.querySelector('.image-display');
-		removeChildren(imageDisplay);
-		imageDisplay.classList.add('has-image');
-
-		let ids = [];
-		result.data.forEach(file => {
-			let img = new Image();
-			img.src = file.url;
-			ids.push(file.attachment_id);
-			imageDisplay.appendChild(img);
-		});
-
-		const hiddenInput = field.querySelector('input[type="hidden"]');
-		hiddenInput.value = ids.join(',');
-
-		const uploadContainer = field.querySelector('.file-upload-container');
-		uploadContainer.hidden = true;
-
-		// Trigger form change event
-		hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
-
-		this.showNotification('Image updated successfully');
-	}
-
-	hasUploadsPending(formConfig) {
-		return formConfig.state.uploadPending || false;
-	}
-	forceSaveAfterUploads(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		// Check periodically if uploads are done
-		const checkUploads = () => {
-			if (!formConfig.state.uploadPending && !this.hasActiveUploads(formConfig)) {
-				console.log('All uploads complete, processing form changes');
-				this.processFormChanges(formConfig);
-				return;
-			}
-
-			console.log('Still waiting for uploads to complete...');
-			setTimeout(checkUploads, 500);
-		};
-
-		checkUploads();
-	}
-	getUploadStatus(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return null;
-
-		return {
-			uploadPending: formConfig.state.uploadPending,
-			isDirty: formConfig.state.isDirty,
-			hasTimeout: !!formConfig.state.saveTimeout
-		};
-	}
-	handleImageUploadError(error, field) {
-		console.error('Upload error:', error);
-
-		// Clear upload pending state
-		const form = field.closest('form');
-		if (form && form.id) {
-			const formConfig = this.forms.get(form.id);
-			if (formConfig) {
-				formConfig.state.uploadPending = false;
-			}
-		}
-
-		this.showNotification('Failed to upload image', 'error');
-
-		// Reset field if needed
-		const uploadContainer = field.querySelector('.file-upload-container');
-		uploadContainer.hidden = false;
-
-		// Clear any error states
-		const errorElement = field.querySelector('.file-error');
-		if (errorElement) {
-			errorElement.textContent = '';
-		}
-	}
-	handleImageRemove(field) {
-		const imageDisplay = field.querySelector('.image-display');
-		const img = imageDisplay.querySelector('img');
-		const hiddenInput = field.querySelector('input[type="hidden"]');
-		const uploadContainer = field.querySelector('.file-upload-container');
-
-		// Clear the hidden input
-		hiddenInput.value = '';
-
-		// Reset UI
-		img.src = '';
-		imageDisplay.classList.remove('has-image');
-		uploadContainer.hidden = false;
-
-		// Show notification
-		this.showNotification('Image removed');
-	}
-	initCharacterLimits(formConfig) {
-		formConfig.element.querySelectorAll('input[data-limit], textarea[data-limit]').forEach(input => {
-			const counter = input.closest('.field').querySelector('.char-count .current');
-			if (counter) {
-				const updateCount = () => {
-					const limit = parseInt(input.dataset.limit, 10);
-
-					// Update the counter
-					counter.textContent = input.value.length;
-
-					// If length exceeds limit, truncate the input value
-					if (input.value.length > limit) {
-						input.closest('.field').classList.add('reached');
-						input.value = input.value.substring(0, limit);
-						counter.textContent = limit; // Update counter after truncation
-					}else {
-						input.closest('.field').classList.remove('reached');
-					}
-				};
-
-				input.addEventListener('input', updateCount);
-				updateCount(); // Initial count
-			}
-		})
-	}
-
-	initNumberFields(formConfig) {
-		this.hasNumberFields = true;
-	}
-	handleNumberClick(e, container) {
-		let change = 0;
-		if (e.target.closest('.increase')) {
-			change += 1;
-		} else if (e.target.closest('.decrease')) {
-			change -=1;
-		}
-		if (change !== 0) {
-			let [
-				step,
-				input
-			] = [
-				parseInt(container.dataset.step),
-				container.querySelector('input'),
-			];
-
-			let value = (input.value === '') ? 0 : parseInt(input.value);
-
-			input.value = (value + (step * change * this.stepMultiplier));
-			this.handleNumberLimits(container);
-		}
-	}
-
-	handleNumberLimits(container) {
-		let [
-			min,
-			max,
-			input,
-			increase,
-			decrease
-		] = [
-			container.dataset.min,
-			container.dataset.max,
-			container.querySelector('input'),
-			container.querySelector('.increase'),
-			container.querySelector('.decrease')
-		];
-		let value = parseInt(input.value);
-		if (value < min) {
-			input.value = min;
-			decrease.disabled = true;
-		} else if (value > max) {
-			input.value = max;
-			increase.disabled = false;
-		} else if (increase.disabled) {
-			increase.disabled = false;
-		} else if (decrease.disabled) {
-			decrease.disabled = false;
-		}
-	}
-	addRepeaterRow(repeater, formConfig) {
-		let rows = repeater.querySelector('.repeater-items');
-		let index = rows.children.length;
-
-		let row = window.getTemplate(repeater.querySelector('template').className);
-
-		// Set a proper ID for the row
-		row.id = `${formConfig.id}-${repeater.dataset.field}-row-${index}`;
-
-		let base = repeater.dataset.field;
-		row.querySelectorAll('input, select, textarea').forEach((field) => {
-			let label = field.nextElementSibling;
-			if (!label || label.tagName !== 'LABEL') {
-				label = field.closest('.field')?.querySelector(`label`);
-			}
-			let name = `${base}:${index}:${field.name}`;
-			let id = `${base}:${index}:${field.name}-${field.value}`;
-
-			[field.name, field.id, label.htmlFor] = [name, id, id];
-		});
-
-		[row.dataset.index, row.querySelector('.row-number').textContent] = [index, `#${index + 1}`];
-		rows.append(row);
-
-		// Focus the first input in the new row
-		const firstInput = row.querySelector('input, select, textarea');
-		if (firstInput) {
-			firstInput.focus();
-		}
-	}
-
-	/**
-	 * Save repeater changes with reason tracking
-	 */
-	saveRepeaterChanges(formId, repeater, reason = 'manual') {
-		console.log(`Saving repeater changes due to: ${reason}`);
-
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		// Clear any other pending timeouts for this form's repeaters
-		this.clearFormRepeaterTimeouts(formId);
-
-		// Use the main form processing pipeline for consistency
-		// but process only repeater changes
-		this.processRepeaterChanges(formConfig, repeater);
-
-		// Clean up active field tracking
-		this.activeRepeaters.delete(formId);
-	}
-
-	processRepeaterChanges(formConfig, repeater) {
-		// Skip if uploads are pending
-		if (formConfig.state.uploadPending || this.hasActiveUploads(formConfig)) {
-			console.log('Skipping repeater save - uploads pending');
-
-			// Schedule a retry
-			setTimeout(() => {
-				this.processRepeaterChanges(formConfig, repeater);
-			}, 2000);
-			return;
-		}
-
-		const fieldName = repeater.dataset.field;
-
-		// Get the complete current form data (including all repeaters)
-		const currentData = this.collectFormData(formConfig.element);
-
-		// Create a change object containing only the repeater field
-		// This ensures we send the complete repeater data
-		const repeaterChanges = {
-			[fieldName]: currentData[fieldName] || []
-		};
-
-		console.log(`Repeater ${fieldName} complete data:`, repeaterChanges);
-
-		// Update stored data for this field only
-		if (!formConfig.data) {
-			formConfig.data = {};
-		}
-		formConfig.data[fieldName] = currentData[fieldName];
-		formConfig.state.isDirty = true;
-
-		// Send the complete repeater data
-		this.handleSave(repeaterChanges, formConfig);
-	}
-
-	/**
-	 * Clear all repeater timeouts for a specific form
-	 */
-	clearFormRepeaterTimeouts(formId) {
-		const form = document.getElementById(formId);
-		if (!form) return;
-
-		// Clear timeouts by form and field pattern
-		for (let [timeoutKey, timeout] of this.repeaterTimeouts) {
-			if (timeoutKey.startsWith(formId + '-')) {
-				clearTimeout(timeout);
-				this.repeaterTimeouts.delete(timeoutKey);
-			}
-		}
-
-		// Also clear the old individual row timeouts for backwards compatibility
-		const repeaterRows = form.querySelectorAll('.repeater-row');
-		repeaterRows.forEach(row => {
-			const rowId = row.id || this.generateRowId(row);
-			if (this.timeouts.has(rowId)) {
-				clearTimeout(this.timeouts.get(rowId));
-				this.timeouts.delete(rowId);
-			}
-		});
-	}
-
-	/**
-	 * Generate a consistent ID for repeater rows
-	 */
-	generateRowId(repeaterRow) {
-		if (repeaterRow.id) return repeaterRow.id;
-
-		// Generate ID based on form and row position
-		const form = repeaterRow.closest('form');
-		const repeater = repeaterRow.closest('.repeater');
-		const index = Array.from(repeater.querySelectorAll('.repeater-row')).indexOf(repeaterRow);
-		const repeaterId = repeater.dataset.field || 'repeater';
-
-		const generatedId = `${form.id}-${repeaterId}-row-${index}`;
-		repeaterRow.id = generatedId;
-		return generatedId;
-	}
-	removeRepeaterRow(removeButton, formConfig) {
-		let repeater = removeButton.closest('.repeater');
-		removeButton.closest('.repeater-row').remove();
-		this.updateRepeaterOrder(repeater);
-	}
-	updateRepeaterOrder(repeater) {
-		let items = repeater.querySelector('.repeater-items');
-		let base = repeater.dataset.field;
-		const form = repeater.closest('form');
-		const formConfig = this.forms.get(form.id);
-
-		items.querySelectorAll('.repeater-row').forEach((row, index) => {
-			[
-				row.dataset.index,
-				row.querySelector('.row-number').textContent
-			] = [
-				index,
-				`#${index + 1}`
-			];
-
-			row.querySelectorAll('[name]').forEach(field => {
-				let name = field.name.split(':').pop();
-				let newName = `${base}:${index}:${name}`;
-				let newId = `${base}-${index}-${name}`;
-				[
-					field.name,
-					field.id
-				] = [
-					newName,
-					newId
-				];
-				let label = field.closest('.field').querySelector('label');
-				if (label) {
-					label.htmlFor = newId;
-				}
-			});
-		});
-
-		// Schedule save after reorder with appropriate delay
-		this.scheduleRepeaterSave(form.id, repeater, 'reorder', this.repeaterDelays.reorder);
-	}
-
-
-	showNotification(msg, type){
-		window.jvbNotifications.showToast(msg, type);
-	}
-	/**
-	 * Public API methods
-	 */
-
-	// Get form configuration
-	getForm(formId) {
-		return this.forms.get(formId);
-	}
-
-	// Manually trigger form processing
-	processForm(formId) {
-		const formConfig = this.forms.get(formId);
-		if (formConfig) {
-			this.processFormChanges(formConfig);
-		}
-	}
-
-	submitFormUploads(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig || !formConfig.uploadFields) return;
-
-		console.log(`Submitting uploads for form: ${formId}`);
-
-		// Submit each upload field
-		formConfig.uploadFields.forEach(fieldId => {
-			window.jvbUploadManager.submitFieldUploads(fieldId);
-		});
-	}
-
-	isFormReadyToSave(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return true;
-
-		return !formConfig.state.uploadPending && !this.hasActiveUploads(formConfig);
-	}
-
-	getFormUploadProgress(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig || !formConfig.uploadFields) return null;
-
-		let totalUploads = 0;
-		let completedUploads = 0;
-		let failedUploads = 0;
-
-		formConfig.uploadFields.forEach(fieldId => {
-			const status = window.jvbUploadManager.getFieldStatus(fieldId);
-			if (status) {
-				totalUploads += status.uploadCount;
-				completedUploads += status.completed;
-				failedUploads += status.failed;
-			}
-		});
-
-		return {
-			total: totalUploads,
-			completed: completedUploads,
-			failed: failedUploads,
-			pending: totalUploads - completedUploads - failedUploads,
-			progress: totalUploads > 0 ? (completedUploads / totalUploads) * 100 : 0
-		};
-	}
-	showFormUploadStatus(formId) {
-		const progress = this.getFormUploadProgress(formId);
-		if (!progress || progress.total === 0) return;
-
-		const message = `Uploads: ${progress.completed}/${progress.total} complete` +
-			(progress.failed > 0 ? `, ${progress.failed} failed` : '') +
-			(progress.pending > 0 ? `, ${progress.pending} pending` : '');
-
-		this.showNotification(message, progress.failed > 0 ? 'warning' : 'info');
-	}
-	getFormStatus(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return null;
-
-		const baseStatus = {
-			formId,
-			isDirty: formConfig.state.isDirty,
-			uploadPending: formConfig.state.uploadPending,
-			hasTimeout: !!formConfig.state.saveTimeout
-		};
-
-		// Add upload status if upload fields exist
-		if (formConfig.uploadFields && window.jvbUploadManager) {
-			baseStatus.uploads = {};
-			formConfig.uploadFields.forEach(fieldId => {
-				baseStatus.uploads[fieldId] = window.jvbUploadManager.getFieldStatus(fieldId);
-			});
-		}
-
-		return baseStatus;
-	}
-
-	// Remove form from management
-	removeForm(formId) {
-		const formConfig = this.forms.get(formId);
-		if (!formConfig) return;
-
-		// Clean up regular timeouts
-		if (formConfig.state.saveTimeout) {
-			clearTimeout(formConfig.state.saveTimeout);
-		}
-
-		// Clean up repeater timeouts
-		this.clearFormRepeaterTimeouts(formId);
-
-		// Clean up upload fields
-		if (formConfig.uploadFields && window.jvbUploadManager) {
-			formConfig.uploadFields.forEach(fieldId => {
-				console.log(`Cleaning up upload field: ${fieldId}`);
-			});
-		}
-
-		this.forms.delete(formId);
-	}
-
-	/***************************************************
-	 *
-	 * Field rendering from json data
-	 *
-	 **************************************************/
-	populatePostsTableFields(form, postsData) {
-		if (!form || !postsData) {
-			return;
-		}
-		const formConfig = this.forms.get(form.id);
-		form.querySelectorAll('tr').forEach(row => {
-			let base = row.dataset.id;
-			let fields = JSON.parse(row.dataset.fields);
-			let images = JSON.parse(row.dataset.images);
-
-			this.populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData, options);
-		});
-	}
-	/**
-	 * Populate form fields with data values
-	 * @param {HTMLFormElement} form - The form element
-	 * @param {Object} fieldsData - Field values from API
-	 * @param {Object} imagesData - Image metadata (optional)
-	 * @param {Object} options - Additional options
-	 */
-	populateFormFields(form, fieldsData, imagesData = {}, options = {}) {
-		if (!form || !fieldsData) {
-			console.warn('FormFields: Missing form or data for population');
-			return;
-		}
-
-		console.log('Populating form fields:', { fieldsData, imagesData });
-		console.log('Form element:', form);
-		console.log('Form fields found:', form.querySelectorAll('.field').length);
-
-		// Get form configuration
-		const formConfig = this.forms.get(form.id);
-
-		// Debug: List all fields in form
-		const allFields = form.querySelectorAll('.field');
-
-		// Try alternative approach if no .field elements found
-		if (allFields.length === 0) {
-
-			// Try finding fields by name attribute instead
-			Object.keys(fieldsData).forEach(fieldName => {
-				const input = form.querySelector(`[name="${fieldName}"]`);
-				if (input) {
-					const fieldWrapper = input.closest('.field') || input.parentElement;
-					console.log(`Found field ${fieldName} via name attribute:`, fieldWrapper);
-
-					if (fieldWrapper) {
-						this.populateFieldValue(fieldWrapper, fieldName, fieldsData[fieldName], imagesData, options);
-					}
-				}
-			});
-
-			return;
-		}
-
-		// Process each field in the form
-		allFields.forEach((fieldWrapper, index) => {
-			console.log(`Processing field ${index}:`, {
-				element: fieldWrapper,
-				tagName: fieldWrapper.tagName,
-				className: fieldWrapper.className,
-				dataset: fieldWrapper.dataset
-			});
-
-			const fieldName = fieldWrapper.dataset.field;
-			console.log(`Field ${index} name:`, fieldName);
-
-			if (!fieldName) {
-				console.warn(`Field ${index} has no data-field attribute`);
-				return;
-			}
-
-			if (!(fieldName in fieldsData)) {
-				console.log(`Field ${fieldName} not in data, skipping`);
-				return;
-			}
-
-			const fieldValue = fieldsData[fieldName];
-			console.log(`About to populate field ${fieldName}:`, { fieldWrapper, fieldValue });
-
-			this.populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData, options);
-		});
-
-		// Process form changes to update any conditional fields
-		if (formConfig) {
-			this.processFormChanges(formConfig, false);
-		}
-	}
-
-	/**
-	 * Populate a single field with its value
-	 * @param {HTMLElement} fieldWrapper - The field wrapper element
-	 * @param {string} fieldName - Field name
-	 * @param {*} fieldValue - Field value
-	 * @param {Object} imagesData - Image metadata
-	 * @param {Object} options - Additional options
-	 */
-	populateFieldValue(fieldWrapper, fieldName, fieldValue, imagesData = {}, options = {}) {
-		if (!fieldWrapper || fieldValue === undefined || fieldValue === null) {
-			return;
-		}
-
-		console.log(`Populating field: ${fieldName}`, { fieldValue, fieldWrapper });
-
-		// Determine field type from classes or data attributes
-		const fieldType = this.getFieldType(fieldWrapper);
-
-		switch (fieldType) {
-			case 'image':
-				this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
-				break;
-
-			case 'gallery':
-				this.populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData);
-				break;
-
-			case 'repeater':
-				this.populateRepeaterField(fieldWrapper, fieldName, fieldValue, options);
-				break;
-
-			case 'taxonomy':
-				this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'user':
-				this.populateUserField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'location':
-				this.populateLocationField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'set':
-			case 'checkbox':
-				this.populateSetField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'select':
-			case 'radio':
-				this.populateSelectField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'true_false':
-				this.populateBooleanField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'date':
-			case 'time':
-			case 'datetime':
-				this.populateDateField(fieldWrapper, fieldName, fieldValue);
-				break;
-
-			case 'number':
-				this.populateNumberField(fieldWrapper, fieldName, fieldValue);
-				break;
-			case 'textarea':
-				if (fieldWrapper.querySelector('.editor-container')) {
-					this.populateEditorField(fieldWrapper, fieldName, fieldValue);
-				} else {
-					this.populateTextareaField(fieldWrapper, fieldName, fieldValue);
-				}
-				break;
-
-			case 'text':
-			case 'email':
-			case 'url':
-			case 'tel':
-			case 'phone':
-			default:
-				this.populateTextField(fieldWrapper, fieldName, fieldValue);
-				break;
-		}
-	}
-
-	/**
-	 * Determine field type from wrapper element
-	 * @param {HTMLElement} fieldWrapper - Field wrapper element
-	 * @returns {string} Field type
-	 */
-	getFieldType(fieldWrapper) {
-		// Check for specific field classes
-		const typeClasses = [
-			'image', 'gallery', 'repeater', 'taxonomy', 'user', 'location',
-			'set', 'checkbox', 'select', 'radio', 'true_false', 'date',
-			'time', 'datetime', 'editor', 'number', 'text', 'textarea',
-			'email', 'url', 'tel', 'phone'
-		];
-
-		for (const type of typeClasses) {
-			if (fieldWrapper.classList.contains(type)) {
-				return type;
-			}
-		}
-
-		// Check for data attribute
-		if (fieldWrapper.dataset.type) {
-			return fieldWrapper.dataset.type;
-		}
-
-		// Check input type
-		const input = fieldWrapper.querySelector('input, select, textarea');
-		if (input) {
-			if (input.tagName === 'TEXTAREA') {
-				return input.dataset.editor === 'true' ? 'editor' : 'textarea';
-			}
-			if (input.type) {
-				return input.type === 'checkbox' && !fieldWrapper.classList.contains('true_false') ? 'set' : input.type;
-			}
-		}
-
-		return 'text';
-	}
-
-	/**
-	 * Populate text-based fields
-	 */
-	populateTextField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input, textarea`);
-		if (input) {
-			input.value = String(fieldValue || '');
-
-			// Update character counter if present
-			if (input.dataset.limit) {
-				const counter = fieldWrapper.querySelector('.char-count .current');
-				if (counter) {
-					counter.textContent = input.value.length;
-				}
-			}
-		}
-	}
-
-	/**
-	 * Populate textarea fields
-	 */
-	populateTextareaField(fieldWrapper, fieldName, fieldValue) {
-		console.log(`📝 populateTextareaField called for ${fieldName}:`, {
-			fieldWrapper,
-			fieldName,
-			fieldValue
-		});
-
-		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
-			fieldWrapper.querySelector('textarea:not([data-editor="true"])');
-
-		console.log(`Found textarea for ${fieldName}:`, textarea);
-
-		if (textarea) {
-			const oldValue = textarea.value;
-			textarea.value = String(fieldValue || '');
-
-			console.log(`Set textarea value for ${fieldName}:`, {
-				oldValue,
-				newValue: textarea.value,
-				actualValue: textarea.value
-			});
-
-			// Trigger change event to update any dependencies
-			textarea.dispatchEvent(new Event('change', { bubbles: true }));
-
-			// Update character counter if present
-			if (textarea.dataset.limit) {
-				const counter = fieldWrapper.querySelector('.char-count .current');
-				if (counter) {
-					counter.textContent = textarea.value.length;
-
-					// Check if limit is reached
-					const limit = parseInt(textarea.dataset.limit, 10);
-					if (textarea.value.length >= limit) {
-						fieldWrapper.classList.add('reached');
-					} else {
-						fieldWrapper.classList.remove('reached');
-					}
-
-					console.log(`Updated character counter for ${fieldName}:`, {
-						length: textarea.value.length,
-						limit,
-						counterText: counter.textContent
-					});
-				}
-			}
-		} else {
-			console.warn(`❌ No textarea found for field ${fieldName} in wrapper:`, fieldWrapper);
-
-			// Debug what's actually in the wrapper
-			const allTextareas = fieldWrapper.querySelectorAll('textarea');
-			const allInputs = fieldWrapper.querySelectorAll('input, textarea, select');
-
-			console.log('Debug - all textareas in wrapper:', allTextareas);
-			console.log('Debug - all inputs in wrapper:', allInputs);
-			console.log('Debug - wrapper innerHTML:', fieldWrapper.innerHTML);
-		}
-	}
-
-
-
-	/**
-	 * Populate number fields
-	 */
-	populateNumberField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="number"]`);
-		if (input) {
-			input.value = Number(fieldValue) || 0;
-		}
-	}
-
-	/**
-	 * Populate boolean/true_false fields
-	 */
-	populateBooleanField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input[type="checkbox"]`);
-		if (input) {
-			input.checked = Boolean(fieldValue);
-		}
-	}
-
-	/**
-	 * Populate select/radio fields
-	 */
-	populateSelectField(fieldWrapper, fieldName, fieldValue) {
-		const value = String(fieldValue || '');
-
-		// Try select first
-		const select = fieldWrapper.querySelector(`select[name="${fieldName}"]`);
-		if (select) {
-			select.value = value;
-			return;
-		}
-
-		// Try radio buttons
-		const radio = fieldWrapper.querySelector(`input[type="radio"][name="${fieldName}"][value="${value}"]`);
-		if (radio) {
-			radio.checked = true;
-		}
-	}
-
-	/**
-	 * Populate set/checkbox fields (multiple selections)
-	 */
-	populateSetField(fieldWrapper, fieldName, fieldValue) {
-		// Parse value if it's a string
-		let values = fieldValue;
-		if (typeof fieldValue === 'string') {
-			try {
-				values = JSON.parse(fieldValue);
-			} catch (e) {
-				values = fieldValue.split(',').map(v => v.trim());
-			}
-		}
-
-		if (!Array.isArray(values)) {
-			values = [String(values)];
-		}
-
-		// Update checkboxes
-		fieldWrapper.querySelectorAll(`input[type="checkbox"][name*="${fieldName}"]`).forEach(checkbox => {
-			checkbox.checked = values.includes(checkbox.value);
-		});
-	}
-
-	/**
-	 * Populate date/time fields
-	 */
-	populateDateField(fieldWrapper, fieldName, fieldValue) {
-		const input = fieldWrapper.querySelector(`[name="${fieldName}"], input`);
-		if (input && fieldValue) {
-			// Handle different date formats
-			let dateValue = fieldValue;
-			if (typeof fieldValue === 'object' && fieldValue.date) {
-				dateValue = fieldValue.date;
-			}
-
-			// Convert to appropriate format for input type
-			try {
-				const date = new Date(dateValue);
-				if (!isNaN(date.getTime())) {
-					switch (input.type) {
-						case 'date':
-							input.value = date.toISOString().split('T')[0];
-							break;
-						case 'time':
-							input.value = date.toTimeString().slice(0, 5);
-							break;
-						case 'datetime-local':
-							input.value = date.toISOString().slice(0, 16);
-							break;
-						default:
-							input.value = dateValue;
-					}
-				}
-			} catch (e) {
-				input.value = dateValue;
-			}
-		}
-	}
-
-	/**
-	 * Populate editor fields (Quill)
-	 */
-	populateEditorField(fieldWrapper, fieldName, fieldValue) {
-		const textarea = fieldWrapper.querySelector(`textarea[name="${fieldName}"]`) ||
-			fieldWrapper.querySelector('textarea[data-editor="true"]') ||
-			fieldWrapper.querySelector('textarea');
-
-		if (!textarea) {
-			console.warn(`Editor field ${fieldName}: textarea not found`);
-			return;
-		}
-
-		const content = String(fieldValue || '');
-
-		// Update the textarea value
-		textarea.value = content;
-
-		// Try to find and update Quill editor
-		const editorContainer = fieldWrapper.querySelector('.editor');
-		if (editorContainer) {
-			// Try different ways to access the Quill instance
-			let quillInstance = null;
-
-			// Method 1: Check if Quill is stored on the editor element
-			if (editorContainer.__quill) {
-				quillInstance = editorContainer.__quill;
-			}
-			// Method 2: Check if Quill is stored as quill property
-			else if (editorContainer.quill) {
-				quillInstance = editorContainer.quill;
-			}
-			// Method 3: Try to find Quill in the global registry (if you have one)
-			else if (window.Quill && window.Quill.find) {
-				quillInstance = window.Quill.find(editorContainer);
-			}
-			// Method 4: Check all Quill instances if available
-			else if (window.Quill && window.Quill.instances) {
-				// Some setups store instances in a registry
-				for (let instance of window.Quill.instances) {
-					if (instance.container === editorContainer) {
-						quillInstance = instance;
-						break;
-					}
-				}
-			}
-
-			if (quillInstance) {
-				console.log(`Found Quill instance for ${fieldName}, setting content`);
-				// Set the content in Quill
-				quillInstance.root.innerHTML = content;
-				// Store the instance reference for future use
-				editorContainer.__quill = quillInstance;
-			} else {
-				console.warn(`Quill instance not found for ${fieldName}, setting HTML directly`);
-				// Fallback: set HTML directly
-				editorContainer.innerHTML = content;
-			}
-		} else {
-			console.warn(`Editor container not found for ${fieldName}`);
-		}
-
-		// Trigger change event on textarea
-		textarea.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	/**
-	 * Populate location fields
-	 */
-	populateLocationField(fieldWrapper, fieldName, fieldValue) {
-		if (!fieldValue || typeof fieldValue !== 'object') {
-			return;
-		}
-
-		// Location fields typically have sub-fields
-		const subFields = ['address', 'lat', 'lng', 'street', 'city', 'province', 'postal_code', 'country'];
-
-		subFields.forEach(subField => {
-			if (fieldValue[subField] !== undefined) {
-				const input = fieldWrapper.querySelector(`[name="${fieldName}_${subField}"], [name="${subField}"]`);
-				if (input) {
-					input.value = String(fieldValue[subField] || '');
-				}
-			}
-		});
-	}
-
-	/**
-	 * Populate taxonomy fields
-	 */
-	populateTaxonomyField(fieldWrapper, fieldName, fieldValue) {
-		// Handle different value formats
-		let termIds = [];
-
-		if (Array.isArray(fieldValue)) {
-			termIds = fieldValue.map(v => String(v));
-		} else if (typeof fieldValue === 'string') {
-			try {
-				const parsed = JSON.parse(fieldValue);
-				termIds = Array.isArray(parsed) ? parsed.map(v => String(v)) : [String(parsed)];
-			} catch (e) {
-				termIds = fieldValue.split(',').map(v => v.trim());
-			}
-		} else if (fieldValue) {
-			termIds = [String(fieldValue)];
-		}
-
-		if (termIds.length === 0) {
-			return;
-		}
-
-		// Update hidden input
-		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
-		if (hiddenInput) {
-			hiddenInput.value = termIds.join(',');
-		}
-
-		// Update taxonomy selector if present
-		if (fieldWrapper.dataset.fieldId && window.jvbSelector) {
-			window.jvbSelector.updateFieldFromInput(fieldWrapper.dataset.fieldId);
-		}
-	}
-
-	/**
-	 * Populate user fields (similar to taxonomy)
-	 */
-	populateUserField(fieldWrapper, fieldName, fieldValue) {
-		// Similar logic to taxonomy fields
-		this.populateTaxonomyField(fieldWrapper, fieldName, fieldValue);
-	}
-
-	/**
-	 * Populate image fields
-	 */
-	populateImageField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
-		if (!fieldValue) {
-			return;
-		}
-
-		// Handle comma-separated IDs or single ID
-		const imageIds = String(fieldValue).split(',').filter(id => parseInt(id.trim()));
-		if (imageIds.length === 0) {
-			return;
-		}
-
-		// Update hidden input
-		const hiddenInput = fieldWrapper.querySelector(`input[type="hidden"][name="${fieldName}"]`);
-		if (hiddenInput) {
-			hiddenInput.value = imageIds.join(',');
-		}
-
-		// Update image display
-		const grid = fieldWrapper.querySelector('.item-grid');
-		const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
-
-		if (grid) {
-			imageIds.forEach(imageId => {
-					let image = window.getTemplate('uploadItem');
-					let img = image.querySelector('img');
-
-					let details = image.querySelector('details');
-					let meta = window.getTemplate('uploadMeta')
-					details.append(meta);
-					[
-						img.src,
-						img.alt,
-						image.querySelector('[name="image-title"]').value,
-						image.querySelector('[name="image-alt-text"]').value,
-						image.querySelector('[name="image-caption"]').value,
-					] = [
-						imagesData[imageId].medium,
-						imagesData[imageId].alt,
-						imagesData[imageId].title,
-						imagesData[imageId].alt,
-						imagesData[imageId].caption,
-					];
-
-					details.querySelector('.upload-meta > .hint')?.remove();
-
-					grid.append(image);
-			});
-
-			if (imageIds.length > 0) {
-				if (uploadContainer) {
-					uploadContainer.hidden = true;
-				}
-			}
-		}
-	}
-
-	/**
-	 * Populate gallery fields
-	 */
-	populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
-		this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
-	}
-
-	/**
-	 * Populate repeater fields
-	 */
-	populateRepeaterField(fieldWrapper, fieldName, fieldValue, options = {}) {
-		if (!fieldValue || !Array.isArray(fieldValue)) {
-			return;
-		}
-
-		const container = fieldWrapper.querySelector('.repeater-items');
-		const template = fieldWrapper.querySelector('template');
-
-		if (!container || !template) {
-			console.warn(`Repeater field ${fieldName}: missing container or template`);
-			return;
-		}
-
-		// Clear existing rows
-		window.removeChildren(container);
-
-		// Create rows for each data item
-		fieldValue.forEach((rowData, index) => {
-			if (!rowData || typeof rowData !== 'object') {
-				return;
-			}
-
-			const row = window.getTemplate(template.className);
-			if (!row) {
-				console.warn(`Repeater field ${fieldName}: template not found`);
-				return;
-			}
-
-			// Set row ID and update row number
-			row.id = `${fieldWrapper.closest('form').id}-${fieldName}-row-${index}`;
-			row.dataset.index = index;
-
-			const rowNumber = row.querySelector('.row-number');
-			if (rowNumber) {
-				rowNumber.textContent = `#${index + 1}`;
-			}
-
-			// Update field names and populate values
-			row.querySelectorAll('input, select, textarea').forEach(field => {
-				const originalName = field.name;
-				const newName = `${fieldName}:${index}:${originalName}`;
-				const newId = `${fieldName}-${index}-${originalName}`;
-
-				// Update field identifiers
-				field.name = newName;
-				field.id = newId;
-
-				// Update label
-				const label = field.nextElementSibling;
-				if (label && label.tagName === 'LABEL') {
-					label.htmlFor = newId;
-				}
-
-				// Populate field value
-				if (rowData[originalName] !== undefined) {
-					this.populateRepeaterFieldValue(field, originalName, rowData[originalName]);
-				}
-			});
-
-			container.appendChild(row);
-		});
-	}
-
-	/**
-	 * Populate individual repeater field value
-	 */
-	populateRepeaterFieldValue(field, fieldName, fieldValue) {
-		switch (field.type) {
-			case 'checkbox':
-				field.checked = Boolean(fieldValue);
-				break;
-			case 'radio':
-				field.checked = field.value === String(fieldValue);
-				break;
-			case 'select-one':
-			case 'select-multiple':
-				field.value = String(fieldValue || '');
-				break;
-			default:
-				field.value = String(fieldValue || '');
-		}
-	}
-}
-
-// Initialize singleton
-document.addEventListener('DOMContentLoaded', () => {
-	if (!window.jvbForm) {
-		window.jvbForm = new FormFields();
-	}
-});
-
-/*
-USAGE EXAMPLES:
-
-// Basic form with save callback
-window.jvbForm.addForm(document.querySelector('#contact-form'), {
-    onSave: (changes) => {
-        console.log('Contact form saved:', changes);
-        // Send to API, show notification, etc.
-    },
-    autoSave: true,
-    saveDelay: 1000
-});
-
-// Form with multiple callbacks
-window.jvbForm.addForm(document.querySelector('#profile-form'), {
-    onSave: (changes, formConfig) => {
-        // Handle save
-        window.jvbQueue.addToQueue({
-            endpoint: '/api/profile',
-            data: changes
-        });
-    },
-    onChange: (event, formConfig) => {
-        // Handle individual field changes
-        if (event.target.name === 'username') {
-            validateUsername(event.target.value);
-        }
-    },
-    onSubmit: (event, formConfig) => {
-        // Custom submit logic
-        event.preventDefault();
-        validateAndSubmitProfile(formConfig);
-    },
-    itemID: 123,
-    api: '/api/profile'
-});
-
-// Table row form
-window.jvbForm.addForm(document.querySelector('#row-edit-form'), {
-    onSave: (changes, formConfig) => {
-        updateTableRow(formConfig.options.rowId, changes);
-    },
-    isRow: true,
-    rowId: 'row-456'
-});
-
-*/
diff --git a/assets/js/dash/InfiniteScroll.js b/assets/js/dash/InfiniteScroll.js
deleted file mode 100644
index bcc54a3..0000000
--- a/assets/js/dash/InfiniteScroll.js
+++ /dev/null
@@ -1,25 +0,0 @@
-class InfiniteScroll {
-
-	constructor () {
-		this.elements = [];
-	}
-
-
-	window.initInfiniteScroll = function (container)
-	{
-		container = (typeof container === 'string') ? document.querySelector(container)??false : container;
-		if (!container) return;
-		const observer = new IntersectionObserver(entries => {
-			entries.forEach(entry => {
-				if (entry.isIntersecting && this.hasMore) {
-					this.loadContent();
-				}
-			})
-		});
-
-		observer.observe(this.elements.scroll);
-	}
-
-}
-
-window.infiniteScroll = InfiniteScroll;
diff --git a/assets/js/dash/LoadingManager.js b/assets/js/dash/LoadingManager.js
deleted file mode 100644
index cc61878..0000000
--- a/assets/js/dash/LoadingManager.js
+++ /dev/null
@@ -1,155 +0,0 @@
-class LoadingManager {
-    constructor() {
-        this.quips = {
-			logo: [
-				"Fetching items...",
-			],
-			uploading: [
-				"Sending to server..."
-			],
-			... JSON.parse(loadingQuips.quips),
-		};
-
-        this.isLoading = false;
-		this.overlay = document.querySelector('.loading-overlay');
-		if (!this.overlay) {
-			return;
-		}
-		this.overlayMessage = this.overlay.querySelector('.message');
-		this.overlayTitle = this.overlay.querySelector('h3');
-		this.overlayIcon = this.overlay.querySelector('div.icon');
-        this.quipInterval = null;
-        this.activeLoads = 0;
-        this.currentQuips = this.defaultQuips;
-    }
-
-    showLoading(message = '', title = 'Loading', customQuips = null) {
-        this.isLoading = true;
-        this.activeLoads++;
-        this.overlayTitle.textContent = title;
-
-        // Set custom or default quips
-        this.currentQuips = customQuips || this.defaultQuips;
-
-        if (message) {
-            this.overlayMessage.textContent = message;
-        }
-        this.overlay.classList.add('active');
-		document.body.classList.add('loading');
-        document.body.style.overflow = 'hidden';
-        this.startQuipCycle();
-    }
-
-    hideLoading() {
-        this.activeLoads--;
-        if (this.activeLoads <= 0) {
-            this.activeLoads = 0;
-            this.overlay.classList.remove('active');
-            document.body.style.overflow = '';
-			document.body.classList.remove('loading');
-            this.stopQuipCycle();
-        }
-        this.isLoading = false;
-        // if(window.formManager.pendingChanges){
-        //     window.formManager.handleFormChange();
-        // }
-    }
-
-	setContent(content) {
-		console.log(content);
-		let keys = Object.keys(this.quips);
-		content = keys.map((c) => {
-			return (keys.includes(content)) ? content : false;
-		}).filter(Boolean);
-		content = (content.length === 0) ? ['logo'] : content;
-		this.currentContent = content;
-	}
-
-    startQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-        }
-
-		let quips = {};
-		this.currentContent.forEach(content => {
-			this.quips[content].forEach((c) => {
-				quips[c] = content;
-			});
-		});
-		let keys = this.shuffleArray(Object.keys(quips));
-
-        let currentIndex = 0;
-        // Set initial message
-        this.overlayMessage.textContent = keys[0];
-        this.overlayMessage.classList.remove('changing');
-		let content = '';
-		let lastContent = '';
-        this.quipInterval = setInterval(() => {
-            this.overlayMessage.classList.add('changing');
-
-            setTimeout(() => {
-                currentIndex = (currentIndex + 1) % keys.length;
-
-				content = quips[keys[currentIndex]];
-				if (content !== lastContent) {
-					window.removeChildren(this.overlayIcon);
-					this.overlayIcon.append(window.getIcon(content));
-				}
-				window.typeText(this.overlayMessage, keys[currentIndex]);
-                this.overlayMessage.classList.remove('changing');
-				lastContent = content;
-            }, 300);
-        }, 2000);
-    }
-
-    stopQuipCycle() {
-        if (this.quipInterval) {
-            clearInterval(this.quipInterval);
-            this.quipInterval = null;
-        }
-    }
-
-    shuffleArray(array) {
-        for (let i = array.length - 1; i > 0; i--) {
-            const j = Math.floor(Math.random() * (i + 1));
-            [array[i], array[j]] = [array[j], array[i]];
-        }
-        return array;
-    }
-
-    startAutosaving() {
-        document.body.classList.add('autosaving');
-    }
-
-    stopAutosaving(message = 'Saved!') {
-        document.body.classList.remove('autosaving');
-        // Show the save popup
-        const popup = document.querySelector('.save-popup');
-        popup.classList.add('show');
-        if(message !== 'Saved!'){
-            popup.innerText = message;
-        }
-
-        setTimeout(() => {
-            popup.classList.remove('show');
-        }, 1500);
-    }
-    showError(message) {
-        // Update overlay to show error state
-        this.overlayTitle.textContent = 'Error';
-        this.overlayMessage.textContent = message;
-        this.overlay.classList.add('active', 'error');
-        document.body.style.overflow = 'hidden';
-
-        // Auto-hide after delay
-        setTimeout(() => {
-            this.overlay.classList.remove('active', 'error');
-            document.body.style.overflow = '';
-        }, 3000);
-    }
-}
-
-
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbLoading = new LoadingManager();
-});
diff --git a/assets/js/dash/QueueManager.js b/assets/js/dash/QueueManager.js
deleted file mode 100644
index cd0076d..0000000
--- a/assets/js/dash/QueueManager.js
+++ /dev/null
@@ -1,2115 +0,0 @@
-/**
- * Middleman between front-end changes and backend processing
- * Uses IndexedDB to store content locally before ensuring it gets sent back to the server
- */
-class DataStore {
-	constructor(config = {}) {
-		this.config = {
-			name: false,
-			endpoint: false,
-			autosync: true,
-			syncInterval: 10000, //10 seconds
-			useIndexedDB: true,
-			apiBase: jvbSettings.api,
-			headers: {},
-			operation: {},
-			... config
-		}
-		this.headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			... this.config.headers
-		}
-		if (!config.name || !config.endpoint) {
-			return;
-		}
-
-		this.items = new Map();
-		this.queue = new Map();
-		this.filters = config.filters??{}; //Load initial filters
-		this.subscribers = new Set();
-		this.db = null;
-
-		this.initDB();
-	}
-
-	async initDB() {
-		if (!('indexedDB' in window)) return;
-
-		const request = indexedDB.open(`jvb_${this.config.name}_db`, 1);
-
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-
-			if (!db.objectStoreNames.contains('items')) {
-				db.createObjectStore('items', { keyPath: 'id' });
-			}
-
-			if (!db.objectStoreNames.contains('queue')) {
-				db.createObjectStore('queue', { keyPath: 'id' });
-			}
-		};
-
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFromDB();
-		};
-	}
-
-	async loadFromDB() {
-		if (!this.db) return;
-
-		// Load items
-		const itemTx = this.db.transaction(['items'], 'readonly');
-		const itemStore = itemTx.objectStore('items');
-		const itemRequest = itemStore.getAll();
-
-		itemRequest.onsuccess = (e) => {
-			e.target.result.forEach(item => {
-				this.items.set(item.id, item);
-			});
-			this.notify('items-loaded', Array.from(this.items.values()));
-		};
-
-		// Load queue
-		const queueTx = this.db.transaction(['queue'], 'readonly');
-		const queueStore = queueTx.objectStore('queue');
-		const queueRequest = queueStore.getAll();
-
-		queueRequest.onsuccess = (e) => {
-			e.target.result.forEach(item => {
-				this.queue.set(item.id, item);
-			});
-		};
-	}
-
-	async fetchFromServer() {
-		try {
-			const params = new URLSearchParams(this.filters);
-
-			const response = await fetch(`${jvbSettings.api}${this.config.endpoint}?${params}`, {
-				headers: this.headers
-			});
-
-			const data = await response.json();
-			console.log(data);
-			if (data.items) {
-				// Clear and update items
-				this.items.clear();
-				data.items.forEach(item => {
-					this.items.set(item.id, item);
-				});
-
-				// Save to IndexedDB
-				this.saveItemsToDB();
-
-				// Notify subscribers
-				this.notify('items-fetched', this.getFilteredItems());
-			}
-
-			return data;
-		} catch (error) {
-			console.error('Fetch error:', error);
-			this.notify('fetch-error', error);
-		}
-	}
-
-	saveItemsToDB() {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['items'], 'readwrite');
-		const store = tx.objectStore('items');
-
-		this.items.forEach(item => {
-			store.put(item);
-		});
-	}
-
-	updateItem(id, changes) {
-		const item = this.items.get(id);
-		if (!item) return;
-
-		// Apply changes optimistically
-		const updated = { ...item, ...changes, _pending: true };
-		this.items.set(id, updated);
-
-		// Add to queue
-		this.addToQueue(id, changes);
-
-		// Notify
-		this.notify('item-updated', updated);
-
-		return updated;
-	}
-
-	createItem(data) {
-		const tempId = `temp_${Date.now()}`;
-		const newItem = {
-			id: tempId,
-			...data,
-			_isNew: true,
-			_pending: true
-		};
-
-		this.items.set(tempId, newItem);
-		this.addToQueue(tempId, { ...data, _action: 'create' });
-		this.notify('item-created', newItem);
-
-		return newItem;
-	}
-
-	deleteItem(id) {
-		const item = this.items.get(id);
-		if (!item) return;
-
-		// Mark for deletion
-		item._deleted = true;
-		item._pending = true;
-		this.items.set(id, item);
-
-		this.addToQueue(id, { _action: 'delete' });
-		this.notify('item-deleted', item);
-	}
-
-	addToQueue(id, changes) {
-		const existing = this.queue.get(id) || {};
-		const merged = { ...existing, ...changes, id, timestamp: Date.now() };
-
-		this.queue.set(id, merged);
-
-		// Save queue to IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['queue'], 'readwrite');
-			tx.objectStore('queue').put(merged);
-		}
-
-		this.notify('queue-updated', this.queue.size);
-	}
-
-	async syncQueue() {
-		if (this.queue.size === 0) return;
-
-		const batch = Array.from(this.queue.values());
-
-		console.log(batch);
-
-		try {
-			const response = await fetch(`${jvbSettings.api}${this.config.endpoint}`, {
-				method: 'POST',
-				headers: this.headers,
-				body: JSON.stringify({
-					content: this.contentType,
-					operations: batch
-				})
-			});
-
-			const result = await response.json();
-
-			if (result.success) {
-				// Clear queue for successful items
-				result.processed.forEach(({ tempId, newId }) => {
-					this.queue.delete(tempId);
-
-					// Update temp IDs with real IDs
-					if (tempId !== newId) {
-						const item = this.items.get(tempId);
-						if (item) {
-							item.id = newId;
-							delete item._isNew;
-							delete item._pending;
-							this.items.delete(tempId);
-							this.items.set(newId, item);
-						}
-					} else {
-						// Just clear pending flag
-						const item = this.items.get(newId);
-						if (item) {
-							delete item._pending;
-							this.items.set(newId, item);
-						}
-					}
-				});
-
-				// Clear queue from IndexedDB
-				if (this.db) {
-					const tx = this.db.transaction(['queue'], 'readwrite');
-					const store = tx.objectStore('queue');
-					result.processed.forEach(({ tempId }) => {
-						store.delete(tempId);
-					});
-				}
-
-				this.notify('sync-success', result);
-			}
-		} catch (error) {
-			this.notify('sync-error', error);
-		}
-	}
-
-	setFilter(key, value) {
-		if (value === '' || value === null) {
-			delete this.filters[key];
-		} else {
-			this.filters[key] = value;
-		}
-
-		this.notify('filters-changed', this.getFilteredItems());
-	}
-
-	getFilteredItems() {
-		let items = Array.from(this.items.values());
-		// Apply filters
-
-		let filters = this.filters;
-		delete filters.user;
-		delete filters.content;
-		delete filters.page;
-		Object.entries(filters).forEach(([key, value]) => {
-			items = items.filter(item => {
-				if (key === 'status') {
-					if (value === 'all') {
-						return item.status === 'publish' || item.status === 'draft';
-					}
-					return item.status === value;
-				}
-				if (key === 'search') {
-					const searchLower = value.toLowerCase();
-					return item.post_title?.toLowerCase().includes(searchLower) ||
-						item.post_content?.toLowerCase().includes(searchLower);
-				}
-				// Handle taxonomy filters
-				if (key.startsWith('tax_')) {
-					const taxonomy = key.replace('tax_', '');
-					return item.taxonomies?.[taxonomy]?.includes(value);
-				}
-				return true;
-			});
-		});
-
-		// Exclude deleted items
-		items = items.filter(item => !item._deleted);
-
-		return items;
-	}
-
-	subscribe(callback) {
-		this.subscribers.add(callback);
-		return () => this.subscribers.delete(callback);
-	}
-
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-}
-window.jvbStore = DataStore;
-
-class QueueManager {
-	constructor() {
-		//Core Components
-		this.a11y = window.jvbA11y;
-		this.errors = window.jvbError;
-		this.cache = window.jvbCache;
-		this.debouncer = window.debouncer;
-		//Config
-		this.STORAGE_KEY = 'jvb_queue';
-		this.API = `${jvbSettings.api}queue`;
-		this.defaultHeaders = {
-			'Content-Type': 'application/json',
-			'X-WP-Nonce': jvbSettings.nonce
-		};
-		this.maxRetries = 3;
-
-		this.queue = new Map();
-		this.hasChanges = this.cache.getItem('queueHasChanges')??false;
-		this.isProcessing = false;
-		this.lastPollTime = null;
-		this.pendingUIUpdates = new Map();
-
-		this.keyHandler = this.handleEscape.bind(this);
-		this.statuses = [
-			'queued', 'localProcessing', 'uploading',
-			'pending', 'processing', 'completed',
-			'failed', 'failed_permanent'
-		];
-
-		this.icons = {
-			queued: 'refresh', localProcessing: 'refresh', uploading: 'syncing',
-			pending: 'cloud', processing: 'syncing', completed: 'synced',
-			failed: 'error', failed_permanent: 'error'
-		};
-
-
-
-		this.statusMessages = {
-			'queued': 'Waiting to send to server...',
-			'localProcessing': 'Processing locally...',
-			'uploading': 'Sending to server...',
-			'pending': 'Sent to server - waiting to be processed...',
-			'processing': 'Server is working on it...',
-			'completed': 'All done!',
-			'failed': 'There was an error',
-			'failed_permanent': 'Failed permanently'
-		};
-
-
-
-		// Skip if not logged in
-		if (!jvbSettings.currentUser) {
-			return;
-		}
-
-		this.initElements();
-		this.loadQueue();
-
-		this.polling = {
-			interval: null, base: 5000, max: 60000,
-			consecutiveNoChanges: 0, isActive: false,
-			lastActivity: Date.now(), startTime: null
-		}
-
-		this.setupActivityTracking();
-		this.initEventListeners();
-		this.fetchOperations(true);
-
-	}
-
-
-	initElements() {
-		this.panel = document.querySelector('aside#queue');
-		if (!this.panel) return;
-
-		this.elements = {
-			queueItems: '.qitems',
-			toggle: '.qtoggle',
-			countdown: '.countdown',
-			refresh: '.refreshNow',
-			popup: '.popup',
-			filters: '.filters',
-			filterButtons: '.filter',
-			retryButton: 'button.retry',
-			dismissButton: 'button.dismiss',
-			cancelButton: 'button.cancel'
-		};
-
-		this.queuedItems 	= this.panel.querySelector(this.elements.queueItems);
-		this.toggle 		= this.panel.querySelector(this.elements.toggle);
-		this.countdown 		= this.panel.querySelector(this.elements.countdown);
-		this.refresh		= this.panel.querySelector(this.elements.refresh);
-		this.popup			= this.panel.querySelector(this.elements.popup);
-		this.filters 		= this.panel.querySelector(this.elements.filters);
-		this.filterButtons	= this.filters.querySelectorAll(this.elements.filterButtons);
-		this.retryButton	= this.panel.querySelector(this.elements.retryButton);
-		this.dismissButton	= this.panel.querySelector(this.elements.dismissButton);
-
-		this.statusButtons = {};
-		this.statuses.forEach(status => {
-			this.statusButtons[status] = this.panel.querySelector(`.filter[data-filter="${status}"]`);
-		});
-	}
-
-	initEventListeners() {
-		// Bind handlers to maintain context
-		this.clickHandler = this.handleClick.bind(this);
-		this.handleOnline = () => {
-			this.updateNetworkIndicator();
-			this.maybeProcessQueue(this.hasChanges);
-		};
-		this.handleOffline = () => this.updateNetworkIndicator();
-		this.handleBeforeUnload = (e) => {
-			const hasPending = [...this.queue.values()].some(item =>
-				['queued', 'localProcessing', 'uploading'].includes(item.status)
-			);
-			if (hasPending) {
-				e.preventDefault();
-				return 'You have unsaved changes.';
-			}
-		};
-		this.trackActivity = () => {
-			this.polling.lastActivity = Date.now();
-		};
-
-		// Add listeners
-		document.addEventListener('click', this.clickHandler);
-		window.addEventListener('online', this.handleOnline);
-		window.addEventListener('offline', this.handleOffline);
-		window.addEventListener('beforeunload', this.handleBeforeUnload);
-
-		// Activity tracking
-		const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
-		events.forEach(event => {
-			document.addEventListener(event, this.trackActivity, { passive: true });
-		});
-	}
-	/**
-	 * Update network indicator
-	 */
-	updateNetworkIndicator() {
-		// Update UI based on online status
-		if (this.panel) {
-			this.panel.classList.toggle('offline', !navigator.onLine);
-		}
-	}
-	handleClick(e) {
-		if (window.targetCheck(e, '#queue ' + this.elements.toggle)) {
-			this.togglePanel();
-			this.maybeAddEmptyState();
-		}
-		// Individual item actions
-		else if (window.targetCheck(e, '#queue .item ' + this.elements.retryButton)) {
-			const operation = e.target.closest('.item');
-			if (operation?.dataset.id) {
-				this.performItemAction(operation.dataset.id, 'retry');
-			}
-		}
-		else if (window.targetCheck(e, '#queue .item ' + this.elements.cancelButton)) {
-			const operation = e.target.closest('.item');
-			if (operation?.dataset.id) {
-				this.performItemAction(operation.dataset.id, 'cancel');
-			}
-		}
-		else if (window.targetCheck(e, '#queue .item ' + this.elements.dismissButton)) {
-			const operation = e.target.closest('.item');
-			if (operation?.dataset.id) {
-				this.performItemAction(operation.dataset.id, 'dismiss');
-			}
-		}
-		// Bulk actions
-		else if (window.targetCheck(e, '#queue ' + this.elements.retryButton)) {
-			this.performBulkAction('retry');
-		}
-		else if (window.targetCheck(e, '#queue ' + this.elements.dismissButton)) {
-			this.performBulkAction('dismiss');
-		}
-		else if (window.targetCheck(e, '#queue ' + this.elements.filterButtons)) {
-			this.handleFilterClick(e);
-		}
-		else if (window.targetCheck(e, '#queue ' + this.elements.refresh)) {
-			this.handleRefreshClick(e);
-		}
-
-		// Close panel when clicking outside
-		if (this.panel.classList.contains('expanded') &&
-			!this.panel.contains(e.target) &&
-			e.target !== this.toggle) {
-			this.closePanel();
-		}
-	}
-
-	closePanel(message = 'Closed Queue Panel') {
-		this.panel.classList.remove('expanded');
-		this.toggle.title = 'Show Queue';
-		this.toggle.ariaExpanded = false;
-		this.a11y.announce(message);
-		document.removeEventListener('keydown', this.keyHandler);
-	}
-
-	openPanel(message = 'Opened Queue Panel') {
-		this.panel.classList.add('expanded');
-		this.toggle.title = 'Hide Queue';
-		this.toggle.ariaExpanded = true;
-		this.a11y.announce(message);
-		document.addEventListener('keydown', this.keyHandler);
-	}
-
-	handleEscape(e) {
-		if (e.key === 'Escape') {
-			this.closePanel('Closed Queue Panel with escape key');
-		}
-	}
-
-	maybeAddEmptyState() {
-		let empty = this.queuedItems.querySelector('.emptyQueue');
-		if (empty) {
-			empty.remove();
-		}
-		if (this.queuedItems.children.length === 0) {
-			let empty = window.getTemplate('emptyQueue');
-			this.queuedItems.append(empty);
-			this.a11y.announce('Nothing queued.');
-		}
-	}
-
-
-	/**
-	 * Track user activity to adjust polling
-	 */
-	setupActivityTracking() {
-		const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
-
-		events.forEach(event => {
-			document.addEventListener(event, () => {
-				this.polling.lastActivity = Date.now();
-			}, { passive: true });
-		});
-	}
-
-	/**
-	 *
-	 * Queue Item Operations
-	 *
-	 */
-	/**
-	 * Add item to queue - Map operations work normally
-	 */
-	async addToQueue(operation) {
-		console.log('Queuing operation: ', operation);
-		let id = operation.id ?? this.generateId();
-		if ('append' in operation) {
-			id = id + operation.append;
-		}
-
-		if (!operation.endpoint || !operation.data) {
-			console.error('Invalid operation - missing endpoint or data');
-			return false;
-		}
-
-		const result = await this.updateItem(id, operation);
-
-		this.updateStatusPanel('pending');
-		this.addPopup(result.item.popup);
-		this.setChanges(true);
-
-		return result.item.id;
-	}
-
-	/**
-	 * Generate a unique operation ID
-	 *
-	 * @returns {string} Unique ID
-	 */
-	generateId() {
-		// Create a timestamp-based prefix
-		const timestamp = new Date().getTime().toString(36);
-
-		// Add random component
-		const randomPart = Math.random().toString(36).substring(2, 8);
-
-		// Add counter to ensure uniqueness within same millisecond
-		this.idCounter = (this.idCounter || 0) + 1;
-		const counter = this.idCounter.toString(36);
-
-		return `u${jvbSettings.currentUser}-${timestamp}-${randomPart}-${counter}`;
-	}
-
-
-	async performBulkAction(action, filterFn = null) {
-		const items = filterFn ?
-			[...this.queue.values()].filter(filterFn) :
-			[...this.queue.values()].filter(item => {
-				switch(action) {
-					case 'dismiss': return item.status === 'completed';
-					case 'retry': return ['failed', 'failed_permanent'].includes(item.status);
-					case 'cancel': return ['queued', 'pending'].includes(item.status);
-					default: return false;
-				}
-			});
-
-		if (!items.length) {
-			this.addPopup(`No operations available for ${action}`);
-			return;
-		}
-
-		try {
-			const result = await this.performQueueAction(
-				items.map(item => item.id),
-				action
-			);
-
-			// Handle results based on action
-			if (['dismiss', 'cancel'].includes(action)) {
-				result.processed_ids.forEach(id => this.removeItem(id, action));
-			} else if (action === 'retry') {
-				result.processed_ids.forEach(id => {
-					this.updateItem(id, {
-						status: 'pending',
-						retries: (this.queue.get(id)?.retries || 0) + 1,
-						error_message: null
-					});
-				});
-				this.setChanges(true);
-			}
-
-			this.addPopup(`${action} completed: ${result.processed_count} operations`);
-		} catch (error) {
-			// Centralized error handling
-			await window.jvbError.log(error, {
-				component: 'QueueManager',
-				action: `bulk_${action}`,
-				itemCount: items.length
-			});
-		}
-	}
-
-	handleFilterClick(e) {
-		this.filterButtons.forEach(btn => btn.classList.remove('active'));
-		const button = e.target.closest(this.elements.filterButtons);
-		button.classList.add('active');
-		const filter = button.dataset.filter || 'all';
-		this.fetchOperations(true, { status: filter });
-	}
-
-	handleRefreshClick(e) {
-		this.refresh.classList.add('refreshing');
-		this.fetchOperations(true, { force: true }).finally(() => {
-			this.refresh.classList.remove('refreshing');
-		});
-	}
-
-	togglePanel() {
-		this.panel.classList.toggle('expanded');
-		if (this.panel.classList.contains('expanded')) {
-			this.openPanel();
-		} else {
-			this.closePanel();
-		}
-	}
-
-	async performItemAction(operationId, action, options = {}) {
-		const item = this.queue.get(operationId);
-		if (!item) {
-			this.addPopup(`Operation ${operationId} not found`);
-			return false;
-		}
-
-		// Validate action is allowed for current status
-		const allowedActions = this.getAllowedActions(item.status);
-		if (!allowedActions.includes(action)) {
-			this.addPopup(`Cannot ${action} operation in current state`);
-			return false;
-		}
-
-		try {
-			// Update UI immediately for responsiveness
-			const tempStatus = `${action}ing`;
-			this.updateItem(operationId, { status: tempStatus });
-			this.addPopup(`${action.charAt(0).toUpperCase() + action.slice(1)}ing operation...`);
-
-			const result = await this.performQueueAction(operationId, action);
-
-			// Handle different action results
-			switch (action) {
-				case 'cancel':
-				case 'dismiss':
-					this.removeItem(operationId, action);
-					break;
-				case 'retry':
-					this.updateItem(operationId, {
-						status: 'pending',
-						retries: (item.retries || 0) + 1,
-						error_message: null,
-						retryAfter: null
-					});
-					this.setChanges(true);
-					break;
-			}
-
-			this.addPopup(`Operation ${action}ed successfully`);
-			return true;
-
-		} catch (error) {
-			// Revert status on error
-			this.updateItem(operationId, {
-				status: item.status,
-				error_message: `${action} failed: ${error.message}`
-			});
-			this.addPopup(`${action} failed: ${error.message}`);
-			return false;
-		}
-	}
-
-	getAllowedActions(status) {
-		const actionMap = {
-			'queued': ['cancel'],
-			'localProcessing': ['cancel'],
-			'pending': ['cancel'],
-			'processing': [],
-			'completed': ['dismiss'],
-			'failed': ['retry', 'dismiss'],
-			'failed_permanent': ['retry', 'dismiss']
-		};
-		return actionMap[status] || [];
-	}
-
-	/**
-	 * Queue Operations
-	 */
-	/**
-	 * Fetch operations from the server
-	 * @param {boolean} refresh
-	 * @param {Object} options Filter options
-	 * @returns {Promise<Object>} Server response
-	 */
-	async fetchOperations(refresh = false, options = {}) {
-		const params = new URLSearchParams(options);
-		const url = `${this.API}?${params.toString()}`;
-
-		try {
-			const data = await this.cache.fetchWithCache(url,
-				{ method: 'GET', headers: this.defaultHeaders },
-				{
-					content: 'queue',
-					forceRefresh: refresh,
-					maxAge: 30000 // 30 seconds for queue data
-				}
-			);
-
-			if (data?.operations) {
-				this.processOperationsData(data.operations);
-			}
-			return data;
-		} catch (error) {
-			// Cache's fetchWithCache already handles errors via window.jvbError
-			return { operations: [], error: error.message };
-		}
-	}
-
-	async performQueueAction(operationIds, action) {
-		const ids = Array.isArray(operationIds) ? operationIds : [operationIds];
-
-		try {
-			const response = await fetch(this.API, {
-				method: 'POST',
-				headers: this.defaultHeaders,
-				body: JSON.stringify({ ids, action })
-			});
-
-			if (!response.ok) {
-				const errorData = await response.json().catch(() => ({}));
-				throw new Error(errorData.message || `${action} failed: ${response.status}`);
-			}
-
-			const result = await response.json();
-			if (!result.success) {
-				throw new Error(result.message || `${action} operation failed`);
-			}
-
-			return result;
-
-		} catch (error) {
-			const result = await window.jvbError.log(error, {
-				component: 'QueueManager',
-				operation: 'performQueueAction',
-				action: action,
-				operationIds: ids,
-				itemCount: ids.length
-			}, () => this.performQueueAction(operationIds, action)); // Retry callback
-
-			if (result.retried) {
-				return result; // Return successful retry result
-			} else {
-				throw error; // Re-throw if not retried
-			}
-		}
-	}
-
-	processOperationsData(operations) {
-		if (!Array.isArray(operations)) {
-			console.warn('Invalid operations data received:', operations);
-			return;
-		}
-
-		const validOperations = operations.filter(op =>
-			op && typeof op === 'object' && op.id
-		);
-
-		if (validOperations.length !== operations.length) {
-			console.warn(`Filtered ${operations.length - validOperations.length} invalid operations`);
-		}
-
-		// 🔧 FIX: Track which server operations we received
-		const serverIds = new Set(validOperations.map(op => op.id));
-
-		// Remove dismissed items (items not returned by server)
-		const itemsToRemove = [];
-		for (const [localId, localItem] of this.queue) {
-			if (!serverIds.has(localId) && localItem.status !== 'queued') {
-				// Item not returned by server and wasn't just queued - likely dismissed
-				itemsToRemove.push(localId);
-			}
-		}
-
-		// 🔧 FIX: Use centralized removeItem
-		for (const id of itemsToRemove) {
-			console.log(`Server dismissed operation: ${id}`);
-			this.removeItem(id, 'server-dismissed', { skipSave: true }); // Batch save below
-		}
-
-		// 🔧 FIX: Use centralized updateItem for all server updates
-		const updatePromises = validOperations.map(op =>
-			this.updateItem(op.id, op, { skipSave: true }) // Batch save below
-		);
-
-		// Wait for all updates to complete, then save once
-		Promise.all(updatePromises).then(() => {
-			this.saveQueue();
-		});
-	}
-
-	/**
-	 *
-	 * @param {boolean} on
-	 */
-	setChanges(on) {
-		this.hasChanges = on;
-		this.cache.setItem('queueHasChanges', on);
-		this.maybeProcessQueue(on);
-	}
-
-	/**
-	 * Starts a timeout after X ms to process queue if no changes are made
-	 * @param on
-	 */
-	maybeProcessQueue(on) {
-		console.log('Checking to process queue...');
-		if (!on) {
-			this.debouncer.cancel('queue');
-			return;
-		}
-
-		this.debouncer.schedule(
-			'queue',
-			() => { this.processQueue()},
-			2500
-		);
-	}
-
-	/**
-	 * Method to manually trigger immediate processing (bypass debounce)
-	 */
-	processQueueImmediately() {
-		console.log('Processing queue immediately - bypassing debounce');
-		this.processQueue();
-	}
-
-	async processQueue() {
-
-		if (!navigator.onLine) {
-			console.log('Offline - postponing queue processing');
-			return;
-		}
-
-		const pendingItems = this.getPendingItems();
-
-		if (pendingItems.length === 0) {
-			this.setChanges(false);
-			return;
-		}
-
-		try {
-			console.log(`Processing ${pendingItems.length} queued items`);
-
-			const batchSize = this.getOptimalBatchSize();
-			for (let i = 0; i < pendingItems.length; i += batchSize) {
-				const batch = pendingItems.slice(i, i + batchSize);
-				await this.processBatchWithIsolation(batch);
-
-				if (i + batchSize < pendingItems.length) {
-					await new Promise(resolve => setTimeout(resolve, 500));
-				}
-			}
-
-			this.setChanges(false);
-			this.updateStatusPanel('synced');
-
-		} catch (error) {
-			await window.jvbError.log(error, {
-				component: 'QueueManager',
-				operation: 'processQueue',
-				pendingCount: pendingItems.length,
-				batchSize: this.getOptimalBatchSize()
-			});
-
-			this.setChanges(false);
-			this.addPopup('Processing will retry automatically');
-		}
-	}
-
-	getPendingItems() {
-		return [...this.queue.values()].filter(item =>
-			item.status === 'queued'
-		);
-	}
-
-	async processBatchWithIsolation(batch) {
-		const batchResults = [];
-
-		// Process each item with error isolation
-		for (const item of batch) {
-			try {
-				const result = await this.processItem(item);
-				batchResults.push({ id: item.id, success: true, result });
-			} catch (error) {
-				console.error(`Failed to process item ${item.id}:`, error);
-				batchResults.push({ id: item.id, success: false, error: error.message });
-			}
-		}
-
-		// Batch update all results
-		const updates = batchResults.map(result => ({
-			id: result.id,
-			data: result.success ?
-				{ status: 'pending', sent_at: Date.now() } :
-				{
-					status: 'failed',
-					error_message: result.error,
-					retries: (this.queue.get(result.id)?.retries || 0) + 1,
-					failed_at: Date.now()
-				}
-		}));
-
-		updates.forEach(item => this.updateItem(item.id, item.data, {skipSave: true}));
-		await this.saveQueue();
-	}
-
-	getOptimalBatchSize() {
-		const systemLoad = this.getSystemLoad();
-
-		if (systemLoad < 0.3) return 5;     // Low load - larger batches
-		if (systemLoad < 0.6) return 3;     // Medium load - medium batches
-		return 1;                           // High load - single items
-	}
-
-	getSystemLoad() {
-		// Simple heuristic based on queue size and active operations
-		const queueSize = this.queue.size;
-		const activeCount = [...this.queue.values()]
-			.filter(item => ['localProcessing', 'uploading'].includes(item.status)).length;
-
-		return Math.min((queueSize + activeCount * 2) / 20, 1);
-	}
-
-	sendUpdate(item) {
-		if ('onUpdate' in item && typeof item.onUpdate === "function") {
-			console.log('Calling on update for item');
-			item.onUpdate(item);
-		}
-	}
-	sendComplete(item) {
-		if ('onComplete' in item && typeof item.onComplete === "function") {
-			item.onComplete(item);
-		}
-	}
-
-	async processItem(item) {
-		console.log('Processing item:', item.id, item.title);
-		this.a11y.announce(`Processing ${item.title}...`);
-		this.updateItem(item.id, {status: 'localProcessing'});
-
-		try {
-			let result = await this.makeRequest(item);
-
-			this.updateItem(item.id, {
-				status: 'pending',
-				result: result,
-				sent_at: Date.now()
-			});
-
-			this.a11y.announce(`${item.title} sent to server for processing`);
-
-			// Only start polling if we're not already polling
-			if (!this.polling.isActive) {
-				console.log('Starting polling for pending server operations');
-				this.startPolling();
-			}
-
-		} catch (error) {
-			const result = await window.jvbError.log(error, {
-				component: 'QueueManager',
-				operation: 'processItem',
-				operationId: item.id
-			}, () => this.makeRequest(item)); // Retry callback
-
-			if (!result.retried) {
-				this.updateItem(item.id, {
-					status: (item.retries || 0) >= this.maxRetries ? 'failed_permanent' : 'failed',
-					error_message: result.message,
-					retries: (item.retries || 0) + 1
-				});
-			}
-		}
-	}
-
-	/**
-	 * Send operation to server
-	 * @param item
-	 * @returns {Promise<any>}
-	 */
-	async makeRequest(item) {
-		this.updateItem(item.id, {status: 'uploading'});
-
-		// Check if data is FormData
-		const isFormData = item.data instanceof FormData;
-
-		let headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			...item.headers
-		};
-
-		// Only set Content-Type for non-FormData requests
-		if (!isFormData) {
-			headers['Content-Type'] = 'application/json';
-		}
-
-		console.log('Sending Data...', item.data);
-
-		let requestBody;
-
-		if (isFormData) {
-			// For FormData, append user and id directly to the FormData
-			item.data.append('user', jvbSettings.currentUser);
-			item.data.append('id', item.id);
-			requestBody = item.data;
-		} else if (typeof item.data === "object") {
-			// For regular objects, JSON stringify as before
-			item.data['user'] = jvbSettings.currentUser;
-			item.data['id'] = item.id;
-			requestBody = JSON.stringify(item.data);
-		} else {
-			// For strings or other data types
-			requestBody = item.data;
-		}
-
-		try {
-			const response = await fetch (`${jvbSettings.api}${item.endpoint}`,
-				{
-					method: item.method,
-					headers,
-					body: requestBody
-				});
-			if (!response.ok) {
-				const errorData = await response.json().catch(() => ({}));
-				throw new Error(errorData.message || `Request failed with status ${response.status}`);
-			}
-
-			return await response.json();
-		} catch (error) {
-			throw error;
-		}
-
-	}
-
-	/**
-	 * Smart polling with backoff
-	 */
-	async startPolling() {
-		if (this.polling.isActive) return;
-
-		this.polling.isActive = true;
-		this.polling.consecutiveNoChanges = 0;
-		this.polling.startTime = Date.now();
-
-		const poll = async () => {
-			try {
-				if (!this.shouldContinuePolling()) {
-					this.stopPolling();
-					return;
-				}
-
-				const result = await this.cache.fetchWithCache(
-					`${this.API}?${new URLSearchParams({ polling: 'true' })}`,
-					{ method: 'GET', headers: this.defaultHeaders },
-					{
-						maxAge: 30000,
-						content: 'queue_polling',
-						timeout: 15000
-					}
-				);
-
-				if (result.operations && Array.isArray(result.operations)) {
-					const hasChanges = this.processPollingData(result.operations);
-					this.polling.consecutiveNoChanges = hasChanges ? 0 : this.polling.consecutiveNoChanges + 1;
-				} else {
-					this.polling.consecutiveNoChanges++;
-				}
-
-				const delay = this.calculatePollingDelay();
-				if (this.polling.isActive) {
-					this.polling.interval = setTimeout(poll, delay);
-				}
-
-			} catch (error) {
-				await window.jvbError.log(error, {
-					component: 'QueueManager',
-					operation: 'polling',
-					consecutiveFailures: this.polling.consecutiveNoChanges,
-					isActive: this.polling.isActive
-				});
-
-				this.polling.consecutiveNoChanges++;
-
-				if (this.polling.isActive && this.shouldContinuePolling()) {
-					const errorDelay = Math.min(this.polling.max, 30000);
-					this.polling.interval = setTimeout(poll, errorDelay);
-				} else {
-					this.stopPolling();
-				}
-			}
-		};
-
-		poll();
-	}
-
-	calculatePollingDelay() {
-		const baseDelay = this.polling.base;
-		const maxDelay = this.polling.max;
-
-		// Exponential backoff based on consecutive no-changes
-		const backoffMultiplier = Math.min(
-			Math.pow(1.5, this.polling.consecutiveNoChanges),
-			8 // Cap the multiplier
-		);
-
-		// Reduce frequency if user is inactive
-		const inactivityMultiplier = this.isUserActive() ? 1 : 2;
-
-		return Math.min(
-			baseDelay * backoffMultiplier * inactivityMultiplier,
-			maxDelay
-		);
-	}
-
-	isUserActive() {
-		return this.polling.lastActivity &&
-			(Date.now() - this.polling.lastActivity) < 60000; // 1 minute
-	}
-
-	processPollingData(operations) {
-		if (operations.length === 0) return false;
-
-		let hasChanges = false;
-
-		// Update operations from server (batch at end)
-		for (const operation of operations) {
-			const localItem = this.queue.get(operation.id);
-
-			if (!localItem) {
-				// New operation from server
-				this.updateItem(operation.id, operation, { skipSave: true });
-				hasChanges = true;
-			} else if (localItem.status !== operation.status ||
-				localItem.progress_percentage !== operation.progress_percentage) {
-				// Status or progress changed
-				this.updateItem(operation.id, operation, { skipSave: true });
-				hasChanges = true;
-			}
-		}
-
-		// Remove operations completed/dismissed on server
-		const serverIds = new Set(operations.map(op => op.id));
-		for (const [localId, localItem] of this.queue) {
-			if (!serverIds.has(localId) &&
-				['pending', 'processing'].includes(localItem.status)) {
-				this.removeItem(localId, 'server-completed', { skipSave: true });
-				hasChanges = true;
-			}
-		}
-
-		// Single save if any changes
-		if (hasChanges) {
-			this.saveQueue();
-			console.debug(`Polling update completed`);
-		}
-
-		return hasChanges;
-	}
-	/**
-	 * Check if polling should continue
-	 * @returns {boolean} Whether to continue polling
-	 */
-	shouldContinuePolling() {
-		// Stop if offline
-		if (!navigator.onLine) {
-			return false;
-		}
-
-		// Check for local items that need server monitoring
-		const pendingServerItems = [...this.queue.values()].filter(item =>
-			['pending', 'processing', 'uploading'].includes(item.status)
-		);
-
-		// If we have items waiting on the server, keep polling
-		if (pendingServerItems.length > 0) {
-			return true;
-		}
-
-		// Also stop if user has been inactive for too long
-		const isUserInactive = this.polling.lastActivity &&
-			(Date.now() - this.polling.lastActivity) > (5 * 60 * 1000); // 5 minutes
-
-		if (isUserInactive) {
-			console.log('User inactive for 5+ minutes - stopping polling');
-			return false;
-		}
-
-		// Stop polling if no server-pending items
-		return false;
-	}
-
-	stopPolling() {
-		this.polling.isActive = false;
-		if (this.polling.interval) {
-			clearTimeout(this.polling.interval);
-			this.polling.interval = null;
-		}
-		console.log('Polling stopped');
-	}
-
-
-	/**
-	 * Fetch specific operation by ID
-	 */
-	async fetchOperation(operationId) {
-		return await this.fetchOperations(true, {
-			ids: operationId,
-			limit: 1
-		});
-	}
-
-
-	/**
-	 * Load queue from cache - now just 12 lines!
-	 */
-	async loadQueue() {
-		try {
-			const [queueMap, metadata] = await Promise.all([
-				this.cache.getItem('user_queue', 'queue'),
-				this.cache.getItem('queue_metadata', 'queue')
-			]);
-
-			if (queueMap instanceof Map && queueMap.size > 0) {
-				console.debug(`Loading ${queueMap.size} queue items from cache`);
-				this.queue.clear();
-
-				const updates = Array.from(queueMap.entries()).map(([id, item]) => ({
-					id, data: item
-				}));
-
-				updates.forEach(item => this.updateItem(item.id, item.data, {skipSave: true}));
-				await this.saveQueue();
-			} else {
-				this.queue = new Map();
-			}
-
-			this.updateUIFromLoadedQueue();
-
-		} catch (error) {
-			await window.jvbError.log(error, {
-				component: 'QueueManager',
-				operation: 'loadQueue',
-				action: 'cache_recovery'
-			});
-
-			this.queue = new Map();
-			this.addPopup('Queue data recovered');
-		}
-	}
-
-	/**
-	 * Save queue to cache
-	 */
-	async saveQueue() {
-		try {
-			const filteredQueue = this.filterQueueForSaving();
-			const stats = this.getQueueStats();
-
-			await Promise.all([
-				this.cache.setItem('user_queue', filteredQueue, 'queue'),
-				this.cache.setItem('queue_stats', stats, 'queue'),
-				this.cache.setItem('queue_metadata', {
-					lastSaved: Date.now(),
-					version: '1.0',
-					totalOperations: filteredQueue.size,
-					hasChanges: this.hasChanges
-				}, 'queue')
-			]);
-
-		} catch (error) {
-			const result = await window.jvbError.log(error, {
-				component: 'QueueManager',
-				operation: 'saveQueue',
-				queueSize: this.queue.size
-			}, () => this.saveQueue()); // Retry callback
-
-			if (!result.retried) {
-				// Fallback save
-				try {
-					await this.cache.setItem('user_queue', new Map(), 'queue');
-				} catch (fallbackError) {
-					console.warn('Even fallback save failed');
-				}
-			}
-		}
-	}
-
-	/**
-	 * Helper: Filter queue items that should be saved
-	 */
-	filterQueueForSaving() {
-		const filteredQueue = new Map();
-
-		for (const [id, item] of this.queue) {
-			if (this.shouldSaveItem(item)) {
-				filteredQueue.set(id, item);
-			}
-		}
-
-		return filteredQueue;
-	}
-
-	/**
-	 * Helper: Determine if an item should be persisted
-	 */
-	shouldSaveItem(item) {
-		// Always save active items
-		if (['queued', 'localProcessing', 'uploading'].includes(item.status)) {
-			return true;
-		}
-
-		// Save recent completed items (1 hour)
-		if (item.status === 'completed' && item.completed_at) {
-			const oneHourAgo = Date.now() - (60 * 60 * 1000);
-			return new Date(item.completed_at).getTime() > oneHourAgo;
-		}
-
-		// Save failed items that can still be retried
-		return (item.status === 'failed' || item.status === 'failed_permanent') &&
-			item.retries < this.maxRetries;
-	}
-
-	/**
-	 * Get queue statistics
-	 *
-	 * @returns {Object} Queue statistics by status
-	 */
-	getQueueStats() {
-		const stats = {};
-
-		// Initialize all statuses to 0
-		this.statuses.forEach(status => {
-			stats[status] = 0;
-		});
-
-		// Count items by status
-		for (const item of this.queue.values()) {
-			if (stats[item.status] !== undefined) {
-				stats[item.status]++;
-			}
-		}
-
-		return stats;
-	}
-
-	/**
-	 *
-	 * UI Updates
-	 *
-	 */
-	/**
-	 * Enhanced action buttons with cancel support
-	 */
-	updateActionButtons(item, container) {
-		window.removeChildren(container);
-
-		switch (item.status) {
-			case 'queued':
-			case 'localProcessing':
-			case 'pending':
-				// Show cancel button for in-progress items
-				const cancelBtn = window.getTemplate('button');
-				cancelBtn.classList.add('cancel');
-				cancelBtn.textContent = 'Cancel';
-				container.appendChild(cancelBtn);
-				break;
-
-			case 'failed':
-			case 'failed_permanent':
-				// Show retry and dismiss buttons
-				const retryBtn = window.getTemplate('button');
-				const dismissBtn = window.getTemplate('button');
-
-				retryBtn.classList.add('retry');
-				retryBtn.textContent = 'Retry';
-				retryBtn.disabled = item.retries >= this.maxRetries;
-
-				dismissBtn.classList.add('dismiss');
-				dismissBtn.textContent = 'Dismiss';
-
-				container.appendChild(retryBtn);
-				container.appendChild(dismissBtn);
-				break;
-
-			case 'completed':
-				// Show dismiss button only
-				const dismissCompletedBtn = window.getTemplate('button');
-				dismissCompletedBtn.classList.add('dismiss');
-				dismissCompletedBtn.textContent = 'Dismiss';
-				container.appendChild(dismissCompletedBtn);
-				break;
-		}
-	}
-
-	/**
-	 * Update Queue Item
-	 * @param id
-	 * @param newData
-	 * @param {object} options
-	 * @param {bool} options.skipSave If you don't want to update the queue
-	 * @param {bool} options.skipUI If you don't want to update the UI
-	 * @returns {Promise<{item: any, wasNew: boolean, oldStatus: null}>}
-	 */
-	async updateItem(id, newData, options = {}) {
-		const wasNew = !this.queue.has(id);
-		let oldStatus = null;
-		let oldItem = null;
-
-		if (wasNew) {
-			//Check queue for any item in the queue with the same endpoint and that canMerge === true
-			let pendingItems = this.getPendingItems();
-			if (pendingItems.length > 0) {
-				for (const item of pendingItems) {
-					if (item.canMerge && item.endpoint === newData.endpoint){
-						let mergeData = window.deepMerge(item.data, newData.data);
-						newData = item;
-						id = newData.id;
-						newData.data = mergeData;
-						break;
-					}
-				}
-			}
-			// Create new item with defaults
-			const item = {
-				id: id,
-				endpoint: false,
-				method: 'POST',
-				headers: {},
-				canMerge: true,
-				title: 'Queue operation',
-				popup: 'Processing...',
-				user: jvbSettings.currentUser,
-				started_at: Date.now(),
-				status: 'queued',
-				retries: 0,
-				dependencies: [],
-				type: false,
-				data: false,
-				...newData
-			};
-			this.queue.set(id, item);
-			console.log(`Created new queue item: ${id}`);
-		} else {
-			// Update existing item
-			oldItem = this.queue.get(id);
-			oldStatus = oldItem.status;
-
-			// Deep merge for data, shallow merge for other properties
-			const updated = {
-				...oldItem,
-				...newData
-			};
-
-			// Special handling for data merging if both exist
-			if (oldItem.data && newData.data && typeof oldItem.data === 'object' && typeof newData.data === 'object') {
-				updated.data = window.deepMerge(oldItem.data, newData.data);
-			}
-
-			this.queue.set(id, updated);
-		}
-
-		const item = this.queue.get(id);
-
-		// Handle status change side effects
-		if (oldStatus !== item.status) {
-			this.handleStatusChange(item, oldStatus, wasNew);
-		}
-
-		// Always save and update UI unless explicitly skipped
-		if (!options.skipSave) {
-			await this.saveQueue();
-		}
-		if (!options.skipUI) {
-			this.scheduleUIUpdate(id, item);
-		}
-
-		return { item, wasNew, oldStatus };
-	}
-
-	handleStatusChange(item, oldStatus) {
-		// Start polling if server-pending
-		if (['pending', 'processing'].includes(item.status) && !this.polling.isActive) {
-			this.startPolling();
-		}
-
-		if(oldStatus && item.status !== oldStatus) {
-			if (['completed', 'failed_permanent'].includes(item.status)){
-				this.sendComplete(item);
-			} else {
-				this.sendUpdate(item);
-			}
-		}
-	}
-
-	/**
-	 * THE ONLY removeItem method - replaces all duplicates
-	 * @param id The Operation ID
-	 * @param {string} reason An optional message
-	 * @param {object} options Optional options Including:
-	 * @param {bool} options.skipSave Whether to skip saving the queue
-	 * @param {bool} options.skipUI Whether to skip UI updates
-	 */
-	async removeItem(id, reason = 'dismissed', options = {}) {
-		const item = this.queue.get(id);
-		if (!item) {
-			console.warn(`Attempted to remove non-existent item: ${id}`);
-			return false;
-		}
-
-		// Log removal for debugging
-		console.log(`Removing queue item ${id} (${item.title}) - reason: ${reason}`);
-
-		// Handle removal side effects
-		this.handleItemRemoval(item, reason);
-
-		// Remove from queue
-		this.queue.delete(id);
-
-		// Always save and update UI unless explicitly skipped
-		if (!options.skipSave) {
-			await this.saveQueue();
-		}
-		if (!options.skipUI) {
-			this.scheduleUIUpdate(id, null);
-		}
-
-		return true;
-	}
-
-	handleItemRemoval(item, reason) {
-		// Clear any related timeouts/intervals
-		if (item.retryTimeout) {
-			clearTimeout(item.retryTimeout);
-		}
-
-		// Log analytics/metrics
-		if (reason === 'completed') {
-			console.log(`Operation completed: ${item.title} (${Date.now() - item.started_at}ms)`);
-		}
-
-		// Clean up any temporary data
-		if (item.data instanceof FormData) {
-			// FormData cleanup if needed
-			console.log('Cleaned up FormData for removed item');
-		}
-	}
-
-	/**
-	 * Schedule UI update (batched)
-	 */
-	scheduleUIUpdate(itemId, item = null) {
-		this.pendingUIUpdates.set(itemId, item);
-		if (!this.uiUpdateScheduled) {
-			this.uiUpdateScheduled = true;
-			requestAnimationFrame(() => {
-				this.processPendingUIUpdates();
-				this.uiUpdateScheduled = false;
-			});
-		}
-	}
-
-	/**
-	 * Process all pending UI updates in a single batch
-	 */
-	processPendingUIUpdates() {
-		if (this.pendingUIUpdates.size === 0) return;
-
-		const updates = new Map(this.pendingUIUpdates);
-		this.pendingUIUpdates.clear();
-
-		// Process all updates
-		for (const [itemId, item] of updates) {
-			if (item === null) {
-				this.removeItemFromUI(itemId);
-			} else {
-				this.updateItemInUI(item);
-			}
-		}
-
-		// Update global UI state once after all items processed
-		this.updateGlobalUIState();
-	}
-
-	/**
-	 * Update or create a single item in the UI
-	 */
-	updateItemInUI(item) {
-		let listItem = this.queuedItems.querySelector(`.item[data-id="${item.id}"]`);
-
-		if (!listItem) {
-			listItem = this.createItemElement(item);
-			this.queuedItems.prepend(listItem);
-		} else {
-			this.updateItemElement(listItem, item);
-		}
-	}
-
-	/**
-	 * Remove item from UI with animation
-	 */
-	removeItemFromUI(itemId) {
-		const element = this.queuedItems.querySelector(`.item[data-id="${itemId}"]`);
-		if (!element) return;
-
-		element.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
-		element.style.opacity = '0';
-		element.style.transform = 'translateX(-100%)';
-
-		setTimeout(() => {
-			element.remove();
-			this.updateGlobalUIState();
-		}, 300);
-	}
-
-	/**
-	 * Create a new item element
-	 */
-	createItemElement(item) {
-		const listItem = window.getTemplate('queueItem');
-		listItem.dataset.id = item.id;
-		this.updateItemElement(listItem, item);
-		return listItem;
-	}
-
-	/**
-	 * Update an existing item element
-	 */
-	updateItemElement(element, item) {
-		// Remove old status classes
-		this.statuses.forEach(status => element.classList.remove(status));
-		element.classList.add(item.status);
-
-		// Update content
-		let timeDisplay = '';
-
-		if (item.updated_at) {
-			// Server now sends ISO format timestamps - much more reliable!
-			timeDisplay = window.formatTimeAgo(new Date(item.updated_at));
-		} else if (item.created_at) {
-			timeDisplay = window.formatTimeAgo(new Date(item.created_at));
-		}
-		const progressPercent = this.calculateProgress(item);
-
-		// Update text content safely
-		const typeEl = element.querySelector('.type');
-		const statusEl = element.querySelector('.status');
-		const detailsEl = element.querySelector('.info .details');
-		const timeEl = element.querySelector('.info .time');
-		const progressFill = element.querySelector('.progress .fill');
-
-		if (typeEl) typeEl.textContent = item.title;
-		if (statusEl)  {
-			statusEl.querySelector('.icon')?.remove();
-			let status = this.getStatusLabel(item.status);
-			statusEl.title = status;
-			statusEl.prepend(window.getIcon(this.icons[item.status]));
-			statusEl.querySelector('span').textContent = status;
-		}
-		if (detailsEl) detailsEl.textContent = this.getItemMessage(item);
-		if (timeEl) timeEl.textContent = timeDisplay;
-		if (progressFill) progressFill.style.width = `${progressPercent}%`;
-
-		if (item.status === 'processing' && item.estimated_completion) {
-			const estimatedEl = element.querySelector('.estimated-completion');
-			if (estimatedEl) {
-				const remaining = window.formatTimeSoon(new Date(item.estimated_completion));
-				estimatedEl.textContent = `Est. ${remaining}`;
-			}
-		}
-
-		// Update action buttons
-		const actionsContainer = element.querySelector('.actions');
-		if (actionsContainer) {
-			this.updateActionButtons(item, actionsContainer);
-		}
-	}
-
-	/**
-	 * Update global UI state (status, counts, etc.)
-	 */
-	updateGlobalUIState() {
-		this.updateStatusPanel();
-		this.updateFilterCounts();
-		this.maybeAddEmptyState();
-	}
-
-	/**
-	 * Helper: Update UI after loading queue
-	 */
-	updateUIFromLoadedQueue() {
-		// Schedule updates for all loaded items
-		for (const item of this.queue.values()) {
-			this.scheduleUIUpdate(item.id, item);
-		}
-
-		// Check if there are pending changes
-		this.setChanges([...this.queue.values()].some(item =>
-			['queued', 'localProcessing', 'uploading'].includes(item.status)
-		));
-
-		if (this.hasChanges) {
-			this.updateStatusPanel('pending');
-		}
-	}
-	/**
-	 * Update filter counts
-	 */
-	updateFilterCounts() {
-		const stats = this.getQueueStats();
-
-		if (this.filterButtons) {
-			this.filterButtons.forEach(button => {
-				const filter = button.dataset.filter;
-				if (!filter || filter === 'all') return;
-
-				const count = stats[filter] || 0;
-				button.dataset.count = count;
-
-				// Update badge
-				let badge = button.querySelector('.count-badge');
-				if (count > 0) {
-					if (!badge) {
-						badge = document.createElement('span');
-						badge.className = 'count-badge';
-						button.appendChild(badge);
-					}
-					badge.textContent = count;
-				} else if (badge) {
-					badge.remove();
-				}
-			});
-		}
-	}
-
-	/**
-	 * Update status panel
-	 *
-	 * @param {string} status Special status to display
-	 */
-	updateStatusPanel(status) {
-		if (!this.panel) return;
-
-		// Get pending count for badge
-		const pendingCount = [...this.queue.values()].filter(item =>
-			['queued', 'localProcessing', 'uploading'].includes(item.status)
-		).length;
-
-		// Update indicator status
-		this.panel.classList.remove('offline', 'pending', 'synced', 'image_processing');
-
-		if (!navigator.onLine) {
-			this.panel.classList.add('offline');
-			this.addPopup('Looks like we\'re offline...');
-		} else if (status === 'pending' || pendingCount > 0) {
-			this.panel.classList.add('pending');
-		} else if (status === 'image_processing') {
-			this.panel.classList.add('image_processing');
-			this.addPopup('Processing images...');
-		} else if (status === 'synced' || pendingCount === 0) {
-			this.panel.classList.add('synced');
-		}
-
-		// Update queue stats
-		this.updateQueueStats();
-	}
-
-	/**
-	 * Update queue statistics
-	 */
-	updateQueueStats() {
-		// Calculate stats
-		const stats = this.getQueueStats();
-
-		// Update filter buttons with counts
-		Object.entries(stats).forEach(([status, count]) => {
-			const button = this.statusButtons[status];
-			if (button) {
-				button.dataset.count = count;
-
-				// Update count badge
-				if (count > 0) {
-					let badge = button.querySelector('.count');
-					if (!badge) {
-						badge = document.createElement('span');
-						badge.className = 'count';
-						button.appendChild(badge);
-					}
-					badge.textContent = count;
-				} else {
-					const badge = button.querySelector('.count');
-					if (badge) {
-						badge.remove();
-					}
-				}
-			}
-		});
-
-		// Update the status indicator badge
-		const badgeCount = stats.queued + stats.localProcessing + stats.uploading;
-		const statusIndicator = this.panel.querySelector('.queue-status-indicator');
-		const statusCount = this.panel.querySelector('.queue-status-count');
-
-		if (statusIndicator) {
-			statusIndicator.classList.toggle('active', badgeCount > 0);
-		}
-
-		if (statusCount) {
-			statusCount.textContent = badgeCount > 0 ? badgeCount : '';
-		}
-
-		// Update action buttons
-		if (this.retryButton) {
-			this.retryButton.disabled = stats.failed === 0;
-		}
-
-		this.dismissButton.disabled = stats.completed === 0;
-	}
-	/**
-	 * Show popup message
-	 *
-	 * @param {string} message Message to show
-	 * @param {number} delay Time in ms to show popup
-	 */
-	addPopup(message, delay = 2000) {
-		if (!this.popup) return;
-
-		this.a11y.announce(message);
-		this.popup.innerHTML = message;
-		this.popup.classList.add('showing');
-
-		setTimeout(() => {
-			this.popup.classList.remove('showing');
-			setTimeout(() => {
-				// Clear popup content after fadeout
-				while (this.popup.firstChild) {
-					this.popup.removeChild(this.popup.firstChild);
-				}
-			}, 50);
-		}, delay);
-	}
-
-
-	calculateProgress(item) {
-		// Use server-provided progress percentage if available
-		if (item.progress_percentage !== undefined) {
-			return Math.max(0, Math.min(100, item.progress_percentage));
-		}
-
-		// Fallback to status-based progress
-		switch (item.status) {
-			case 'queued':
-			case 'failed':
-			case 'failed_permanent':
-				return 0;
-			case 'localProcessing':
-				return 5;
-			case 'uploading':
-				return 40;
-			case 'pending':
-				return 65;
-			case 'processing':
-				return 85;
-			case 'completed':
-				return 100;
-			default:
-				return 0;
-		}
-	}
-	/**
-	 * Get status label for display
-	 *
-	 * @param {string} status Operation status
-	 * @returns {string} Human-readable status
-	 */
-	getStatusLabel(status) {
-		switch (status) {
-			case 'queued':
-				return 'Received';
-			case 'localProcessing':
-				return 'Processing Data';
-			case 'uploading':
-				return 'Sending to Server';
-			case 'pending':
-				return 'Sent to Server';
-			case 'processing':
-				return 'Server processing';
-			case 'completed':
-				return 'Completed';
-			case 'failed':
-				return 'Failed';
-			case 'failed_permanent':
-				return 'Failed';
-			default:
-				return status;
-		}
-	}
-
-	/**
-	 * Get detailed message for an item
-	 *
-	 * @param {Object} item Queue item
-	 * @returns {string} Detailed message
-	 */
-	getItemMessage(item) {
-		// Server provides better error messages and status info
-		let message = this.statusMessages[item.status] || '';
-
-		// Show progress for processing items
-		if (item.status === 'processing' && item.progress_count && item.count) {
-			message = `Processing ${item.progress_count} of ${item.count}`;
-			if (item.progress_percentage) {
-				message += ` (${item.progress_percentage}%)`;
-			}
-		}
-
-		// Add error if failed
-		if ((item.status === 'failed' || item.status === 'failed_permanent') && item.error_message) {
-			message = `Error: ${item.error_message}`;
-		}
-
-		// Show estimated completion for processing items
-		if (item.status === 'processing' && item.estimated_completion) {
-			const remaining = window.formatTimeSoon(new Date(item.estimated_completion));
-			message += ` • Est. ${remaining}`;
-		}
-
-		return message;
-	}
-
-	cleanup() {
-		// Clear all timeouts and intervals
-		this.stopPolling();
-
-		// Remove all event listeners
-		document.removeEventListener('click', this.clickHandler);
-		window.removeEventListener('online', this.handleOnline);
-		window.removeEventListener('offline', this.handleOffline);
-		window.removeEventListener('beforeunload', this.handleBeforeUnload);
-		document.removeEventListener('keydown', this.handleEscape);
-
-		// Clear activity tracking
-		const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
-		events.forEach(event => {
-			document.removeEventListener(event, this.trackActivity);
-		});
-
-		// Clear references
-		this.queue.clear();
-		this.pendingUIUpdates.clear();
-
-		console.log('QueueManager destroyed');
-	}
-
-	/**
-	 * Create reply button template
-	 */
-	createReplyButtonTemplate() {
-		const button = document.createElement('button');
-		button.type = 'button';
-		button.className = 'reply';
-		button.dataset.action = 'make-response';
-		button.innerHTML = window.getIcon('reply') + '<span>Reply</span>';
-		return button;
-	}
-
-	/**
-	 * Create comments button template
-	 */
-	createCommentsButtonTemplate() {
-		const button = document.createElement('a');
-		button.className = 'button';
-		button.innerHTML = window.getIcon('response') + '{ <span class="count"></span> }';
-		return button;
-	}
-
-	/**
-	 * Create vote buttons template
-	 */
-	createVoteButtonsTemplate() {
-		const vote = document.createElement('div');
-		vote.className = 'vote';
-		vote.innerHTML = `
-            <button type="button" class="up" onClick="handleVote(this)"
-                data-vote="up">${window.getIcon('upvoted') + window.getIcon('upvote')}
-                <span>{ <span class="count">0</span> }</span>
-            </button>
-            <button type="button" class="down" onClick="handleVote(this)"
-                data-vote="down">${window.getIcon('downvoted') + window.getIcon('downvote')}
-                <span>{ <span class="count">0</span> }</span>
-            </button>
-        `;
-		return vote;
-	}
-}
-//
-// document.addEventListener('DOMContentLoaded', () => {
-// 	window.jvbQueue = new QueueManager();
-// });
-
-// Theme switching functionality
-document.addEventListener('DOMContentLoaded', function() {
-	const themeSwitch = document.getElementById('theme-switch');
-
-	if (!themeSwitch) return;
-
-	// Initialize theme from localStorage or system preference
-	const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
-	const storedTheme = localStorage.getItem('theme');
-
-	if (storedTheme) {
-		document.documentElement.classList.toggle('dark', storedTheme === 'dark');
-		themeSwitch.checked = storedTheme === 'dark';
-	} else {
-		document.documentElement.classList.toggle('dark', prefersDark.matches);
-		themeSwitch.checked = prefersDark.matches;
-	}
-
-	// Handle theme switch changes
-	themeSwitch.addEventListener('change', async function () {
-		const isDark = this.checked;
-		document.documentElement.classList.toggle('dark', isDark);
-		localStorage.setItem('theme', isDark ? 'dark' : 'light');
-
-		// If user is logged in, save preference
-		if (jvbSettings.currentUser !== null) {
-			try {
-				await fetch(`${jvbSettings.api}settings`, {
-					method: 'POST',
-					headers: {
-						'Content-Type': 'application/json',
-						'X-WP-Nonce': jvbSettings.nonce,
-						'action_nonce': jvbSettings.dash,
-					},
-					body: JSON.stringify({
-						dark_mode: isDark
-					})
-				});
-			} catch (error) {
-				console.error('Failed to save theme preference:', error);
-			}
-		}
-
-		// Update label
-		const label = document.getElementById('theme-switch');
-		if (label) {
-			label.title = isDark ? 'Toggle Light Mode' : 'Toggle Dark Mode';
-		}
-	});
-
-	// Handle system theme changes
-	prefersDark.addEventListener('change', (e) => {
-		if (!localStorage.getItem('theme')) {
-			const isDark = e.matches;
-			document.documentElement.classList.toggle('dark', isDark);
-			themeSwitch.checked = isDark;
-		}
-	});
-});
-
-// window.addEventListener('beforeunload', () => window.jvbQueue?.cleanup());
diff --git a/assets/js/dash/QueueManagerBackup.js b/assets/js/dash/QueueManagerBackup.js
deleted file mode 100644
index ad8e641..0000000
--- a/assets/js/dash/QueueManagerBackup.js
+++ /dev/null
@@ -1,2666 +0,0 @@
-/**
- * Refactor statuses:
- * 1) queued                 - local queue
- * 1) localProcessing         - any local processing that can be done in the browser
- * 2) uploading             - sending to server
- * 3) pending; x in line     - sent to server, waiting
- * 4) processing             - server actively working on it
- * 5) completed             - with optional refresh
- */
-class QueueManagerBackup {
-    constructor() {
-        // localStorage.clear();
-        //Core components
-        this.a11y = window.jvbA11y;
-        this.errors = window.jvbError;
-        this.cache = window.jvbCache;
-
-        //Config
-        this.STORAGE_KEY = 'jvb_queue';
-        this.API = `${jvbSettings.api}queue`;
-        this.maxRetries = 3;
-
-        //Initialize State
-        this.queue = new Map();
-        this.hasChanges = false;
-        this.processing = false;
-        this.lastPollTime = null;
-
-		//Status Tracking
-		this.statuses = [
-			'queued',           // Local, waiting to be sent
-			'localProcessing',  // Being processed client-side
-			'uploading',        // Being sent to server
-			'pending',          // On server, waiting to be processed
-			'processing',       // Server is actively processing
-			'completed',        // Successfully completed
-			'failed',           // Failed, can be retried
-			'failed_permanent'  // Failed too many times
-		];
-
-        //Initialize UI
-        this.loadTemplates();
-        this.initUI();
-
-        // Operation type handlers
-        this.processors = {
-            'handle_vote': this.processVote.bind(this),
-            'invite_artist': this.processArtistInvite.bind(this),
-            'new_news': this.processNewNews.bind(this),
-            'new_response': this.processNewResponse.bind(this),
-            'bio_update': this.processBioUpdate.bind(this),
-            'favourite_toggle': this.processFavourite.bind(this),
-            'favourite_notes': this.processFavouriteNotes.bind(this),
-            'favourite_list_create': this.processFavouriteListCreate.bind(this),
-            'favourite_list_add': this.processFavouriteListAddItems.bind(this),
-            'favourite_list_remove': this.processFavouriteListRemoveItems.bind(this),
-            'favourite_list_delete': this.processFavouriteListDelete.bind(this),
-            'favourite_list_share': this.processFavouriteListShare.bind(this),
-            'favourite_list_unshare': this.processFavouriteListUnshare.bind(this),
-            'user_settings': this.processSettingsUpdate.bind(this),
-            'image_upload': this.processFileUpload.bind(this),
-            'content_create': this.processContentCreation.bind(this),
-            'batch_creation': this.processBatchCreation.bind(this),
-            'content_update': this.processContentUpdate.bind(this),
-        };
-
-        // Cache types that need to be cleared after operations
-        this.cacheTypesToClear = {
-            'handle_vote': ['karma'],
-            'new_news': ['news'],
-            'new_response': ['responses', 'news'],
-            'bio_update': ['artist'],
-            'favourite_toggle': ['favourites', 'favouritesManager'],
-            'favourite_notes': ['favouritesManager'],
-            'favourite_list_create': ['list-item', 'favourite-lists'],
-            'favourite_list_add': ['list-item', 'favourite-lists'],
-            'favourite_list_remove': ['list-item', 'favourite-lists'],
-            'favourite_list_delete': ['list-item', 'favourite-lists'],
-            'favourite_list_share': ['list-item', 'favourite-lists'],
-            'favourite_list_unshare': ['list-item', 'favourite-lists'],
-        };
-
-        // Human-readable operation names
-        this.operationNames = {
-            'handle_vote': 'Adding your voice',
-            'new_news': 'New News',
-            'invite_artist': 'Sending Invites...',
-            'new_response': 'Sending in your response',
-            'image_upload': 'Image Upload',
-            'content_update': 'Content Update',
-            'content_create': 'New Content',
-            'user_settings': 'Settings Update',
-            'favourite_toggle': 'Favourite Update',
-            'bio_update': 'Profile Update',
-            'batch_creation': 'Batch Content Creation',
-            'favourite_notes': 'Favourite Notes',
-            'favourite_list_create': 'List Creation',
-            'favourite_list_add': 'List Update',
-            'favourite_list_remove': 'List Update',
-            'favourite_list_delete': 'List Deletion',
-            'favourite_list_share': 'List Sharing',
-            'favourite_list_unshare': 'List Share Removal'
-        };
-
-        // Status messages for UI
-        this.statusMessages = {
-            'queued': 'Waiting to send to server...',
-            'localProcessing': 'Processing locally...',
-            'uploading': 'Sending to server...',
-            'pending': 'In line for processing...',
-            'processing': 'Server is working on it...',
-            'completed': 'All done!',
-            'failed': 'There was an error',
-            'failed_permanent': 'Failed permanently'
-        };
-
-        // Skip if not logged in
-        if (!jvbSettings.currentUser) {
-            return;
-        }
-
-        // Load queue from localStorage
-        this.loadQueue();
-
-        // Set up polling configuration
-        this.pollingConfig = {
-            enabled: true,
-            interval: 10000,
-            minInterval: 5000,
-            maxInterval: 60000,
-            countdown: true,
-            backoffFactor: 1.5, // Increase interval by this factor when no changes
-            consecutiveNoChanges: 0, // Track consecutive polls with no changes
-            maxNoChangesCount: 8, // Cap on increasing interval
-			lastETag: null,
-			lastUserActivity: Date.now(),
-			inactiveThreshold: 5 * 60 * 1000
-        };
-
-		// Track user activity
-		this.setupActivityTracking();
-
-        // Set up event listeners
-        this.initEventListeners();
-
-        // Initial fetch of server operations
-        this.fetchOperations(true).then(() => {
-            this.startPolling();
-        });
-
-        // Process queue periodically if there are changes
-        setInterval(() => {
-            if (this.hasChanges) {
-                this.processQueue();
-            }
-        }, 3000);
-    }
-
-	/**
-	 * Track user activity to adjust polling
-	 */
-	setupActivityTracking() {
-		const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
-
-		events.forEach(event => {
-			document.addEventListener(event, () => {
-				this.pollingConfig.lastUserActivity = Date.now();
-			}, { passive: true });
-		});
-	}
-
-    /**
-     * Initialize UI elements
-     */
-    initUI() {
-        if (!jvbSettings.currentUser) return;
-
-        // Main panel elements
-        this.panel = document.querySelector('#queue-status-panel');
-        if (!this.panel) return;
-
-        this.panelList = this.panel.querySelector('.queue-list');
-        this.togglePanel = this.panel.querySelector('.queue-status-toggle');
-        this.countdown = this.panel.querySelector('.refresh-countdown');
-        this.refreshButton = this.panel.querySelector('.manual-refresh');
-        this.popup = this.panel.querySelector('.popup');
-
-        // Filter buttons
-        this.filterButtonsContainer = this.panel.querySelector('.queue-filters');
-        this.filterButtons = this.panel.querySelectorAll('.queue-filters .filter');
-
-        // Status-specific filter buttons
-        this.statusButtons = {};
-        this.statuses.forEach(status => {
-            this.statusButtons[status] = this.panel.querySelector(`.filter[data-filter="${status}"]`);
-        });
-
-        // Action buttons
-        this.retryButton = this.panel.querySelector('.retry-failed');
-        this.clearButton = this.panel.querySelector('.dismiss-completed');
-
-        // Initialize status panel
-        this.initStatusPanel();
-    }
-
-
-
-
-    /**
-     * Load HTML templates
-     */
-    loadTemplates() {
-        this.templates = new Map();
-
-        // Default templates
-        this.templates.set('replyButton', this.createReplyButtonTemplate());
-        this.templates.set('commentsButton', this.createCommentsButtonTemplate());
-        this.templates.set('voteButton', this.createVoteButtonsTemplate());
-
-        // Load templates from DOM
-        document.querySelectorAll('template').forEach(template => {
-            const classes = Array.from(template.classList);
-            if (classes.length > 0) {
-                const item = template.content.cloneNode(true).firstElementChild;
-                classes.forEach(key => {
-                    if (!this.templates.has(key)) {
-                        this.templates.set(key, item);
-                    }
-                });
-            }
-        });
-    }
-
-    /**
-     * Create reply button template
-     */
-    createReplyButtonTemplate() {
-        const button = document.createElement('button');
-        button.type = 'button';
-        button.className = 'reply';
-        button.dataset.action = 'make-response';
-        button.innerHTML = jvbSettings.icons.reply + '<span>Reply</span>';
-        return button;
-    }
-
-    /**
-     * Create comments button template
-     */
-    createCommentsButtonTemplate() {
-        const button = document.createElement('a');
-        button.className = 'button';
-        button.innerHTML = jvbSettings.icons.response + '{ <span class="count"></span> }';
-        return button;
-    }
-
-    /**
-     * Create vote buttons template
-     */
-    createVoteButtonsTemplate() {
-        const vote = document.createElement('div');
-        vote.className = 'vote';
-        vote.innerHTML = `
-            <button type="button" class="up" onClick="handleVote(this)"
-                data-vote="up">${jvbSettings.icons.upvoted + jvbSettings.icons.upvote}
-                <span>{ <span class="count">0</span> }</span>
-            </button>
-            <button type="button" class="down" onClick="handleVote(this)"
-                data-vote="down">${jvbSettings.icons.downvoted + jvbSettings.icons.downvote}
-                <span>{ <span class="count">0</span> }</span>
-            </button>
-        `;
-        return vote;
-    }
-
-    /**
-     * Initialize status panel
-     */
-    initStatusPanel() {
-        if (!this.panel) return;
-
-        // Toggle panel visibility
-        if (this.togglePanel) {
-            this.togglePanel.addEventListener('click', () => {
-                this.panel.classList.toggle('expanded');
-                this.togglePanel.title = this.panel.classList.contains('expanded')
-                    ? 'Hide Queue' : 'Show Queue';
-
-                const message = this.panel.classList.contains('expanded')
-                    ? 'Opened Queue Panel' : 'Closed Queue Panel';
-                this.a11y.announce(message);
-
-                this.maybeAddEmptyState();
-            });
-        }
-
-        // Close panel when clicking outside
-        document.addEventListener('click', (e) => {
-            if (this.panel.classList.contains('expanded') &&
-                !this.panel.contains(e.target) &&
-                e.target !== this.togglePanel) {
-                this.panel.classList.remove('expanded');
-            }
-        });
-
-        // Handle Escape key
-        document.addEventListener('keydown', (e) => {
-            if (e.key === 'Escape' && this.panel.classList.contains('expanded')) {
-                this.panel.classList.remove('expanded');
-            }
-        });
-
-        // Retry failed operations
-        if (this.retryButton) {
-            this.retryButton.addEventListener('click', () => this.retryFailedOperations());
-        }
-
-        // Clear completed operations
-        if (this.clearButton) {
-            this.clearButton.addEventListener('click', () => this.clearCompletedOperations());
-        }
-
-        // Filter buttons
-        if (this.filterButtons) {
-            this.filterButtons.forEach(button => {
-                button.addEventListener('click', () => {
-                    this.filterButtons.forEach(btn => btn.classList.remove('active'));
-                    button.classList.add('active');
-
-                    const filter = button.dataset.filter || 'all';
-                    this.fetchOperations(true, {status: filter});
-                });
-            });
-        }
-
-        // Refresh button
-        if (this.refreshButton) {
-            this.refreshButton.addEventListener('click', () => {
-                this.refreshButton.classList.add('refreshing');
-                this.fetchOperations(true, {force: true}).finally(() => {
-                    this.refreshButton.classList.remove('refreshing');
-                });
-            });
-        }
-
-        // Item-specific actions
-        this.panelList.addEventListener('click', (e) => {
-            // Retry operation
-            if (e.target.classList.contains('retry-operation')) {
-                const operationItem = e.target.closest('.queue-item');
-                if (operationItem) {
-                    this.retryOperation(operationItem.dataset.id);
-                }
-            }
-
-            // Dismiss operation
-            else if (e.target.classList.contains('dismiss-operation')) {
-                const operationItem = e.target.closest('.queue-item');
-                if (operationItem) {
-                    this.dismissOperations([operationItem.dataset.id]);
-                }
-            }
-        });
-
-        // Initialize empty state
-        this.maybeAddEmptyState();
-    }
-
-    /**
-     * Set up event listeners
-     */
-    initEventListeners() {
-        // Network status change
-        window.addEventListener('online', () => {
-            this.updateNetworkIndicator();
-            if (this.hasChanges) {
-                this.processQueue();
-            }
-        });
-
-        window.addEventListener('offline', () => {
-            this.updateNetworkIndicator();
-            this.updateStatusPanel('offline');
-        });
-
-        // Before unload warning for unsaved changes
-        window.addEventListener('beforeunload', (e) => {
-            const hasPendingLocalChanges = [...this.queue.values()].some(item =>
-                ['queued', 'localProcessing', 'uploading'].includes(item.status)
-            );
-
-            if (hasPendingLocalChanges) {
-                e.preventDefault();
-                return e.returnValue = 'You have unsaved changes that haven\'t been sent to the server. Are you sure you want to leave?';
-            }
-        });
-    }
-
-    /**
-     * Maybe add empty state message
-     */
-    maybeAddEmptyState() {
-        if (this.panelList.children.length === 0) {
-            this.panelList.innerHTML = '<div class="no-operations">Everything up to date</div>';
-            this.a11y.announce('Nothing queued.');
-        } else if (this.panelList.children.length > 1 && this.panelList.querySelector('.no-operations')) {
-            this.panelList.querySelector('.no-operations').remove();
-        }
-    }
-
-    /**
-     * Add an operation to the queue
-     *
-     * @param {Object} operation Operation data
-     * @returns {string} Operation ID
-     */
-    async addToQueue(operation) {
-        // Try to merge with similar operations
-        const mergedOperation = this.mergeOperations(operation);
-        operation = mergedOperation || operation;
-
-        // Generate unique ID
-        const id = this.generateId();
-
-        // Create queue item
-        const item = {
-            id: id,
-            type: operation.type,
-            data: operation.data,
-            user: jvbSettings.currentUser,
-            started_at: Date.now(),
-            status: 'queued',
-            retries: 0,
-            dependencies: operation.dependencies || [],
-			onUpdate: operation.onUpdate??false
-        };
-
-        // Add to queue
-        this.queue.set(id, item);
-        this.saveQueue();
-
-        // Update UI
-        this.updateStatusPanel('pending');
-        this.addPopup(this.getPopupMessage(item));
-        this.hasChanges = true;
-
-        return item.id;
-    }
-
-    /**
-     * Generate a unique operation ID
-     *
-     * @returns {string} Unique ID
-     */
-    generateId() {
-        // Create a timestamp-based prefix
-        const timestamp = new Date().getTime().toString(36);
-
-        // Add random component
-        const randomPart = Math.random().toString(36).substring(2, 8);
-
-        // Add counter to ensure uniqueness within same millisecond
-        this.idCounter = (this.idCounter || 0) + 1;
-        const counter = this.idCounter.toString(36);
-
-        return `u${jvbSettings.currentUser}-${timestamp}-${randomPart}-${counter}`;
-    }
-
-    /**
-     * Load queue from localStorage
-     */
-    loadQueue() {
-        try {
-            const storedData = localStorage.getItem(this.STORAGE_KEY);
-            if (!storedData) return;
-
-            const queueData = JSON.parse(storedData);
-            if (!queueData || !Array.isArray(queueData)) return;
-
-            this.queue = new Map();
-            queueData.forEach(item => {
-                if (item && item.id) {
-                    this.queue.set(item.id, item);
-                }
-            });
-
-            // Update UI for loaded items
-            [...this.queue.values()].forEach(item => {
-                this.updatePanelItem(item);
-            });
-
-            // Set hasChanges if there are queued items
-            this.hasChanges = [...this.queue.values()].some(item =>
-                ['queued', 'localProcessing', 'uploading'].includes(item.status)
-            );
-        } catch (error) {
-            console.error('Error loading queue:', error);
-            this.queue = new Map();
-        }
-    }
-
-
-    /**
-     * Save queue to localStorage
-     */
-    saveQueue() {
-        try {
-            // Filter items for storage
-            const queueToSave = [...this.queue.values()].filter(item => {
-                // Keep local items
-                if (['queued', 'localProcessing', 'uploading'].includes(item.status)) {
-                    return true;
-                }
-
-                // Keep completed items for a short while
-                if (item.status === 'completed' && item.completed_at) {
-                    const hourAgo = Date.now() - (60 * 60 * 1000);
-                    return new Date(item.completed_at).getTime() > hourAgo;
-                }
-
-                // Keep failed items that might be retried
-                return (item.status === 'failed' || item.status === 'failed_permanent') &&
-                    item.retries < this.maxRetries;
-            });
-
-            // Serialize and store
-            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(queueToSave));
-
-            // Also save stats for quick access
-            localStorage.setItem(
-                this.STORAGE_KEY + '_stats',
-                JSON.stringify(this.getQueueStats())
-            );
-        } catch (error) {
-            console.error('Error saving queue:', error);
-            // If we hit storage limits, clean up
-            this.emergencyQueueCleanup();
-        }
-    }
-
-    /**
-     * Emergency cleanup when storage is full
-     */
-    emergencyQueueCleanup() {
-        console.warn('Emergency queue cleanup triggered');
-
-        // Keep only essential pending items
-        const tempQueue = this.queue;
-        this.queue = new Map();
-
-        for (const [key, item] of tempQueue.entries()) {
-            if (item && item.status === 'pending') {
-                this.queue.set(key, item);
-            }
-        }
-
-        // Save the reduced queue
-        try {
-            localStorage.setItem(this.STORAGE_KEY, JSON.stringify([...this.queue.values()]));
-            localStorage.removeItem(this.STORAGE_KEY + '_stats');
-        } catch (error) {
-            console.error('Failed even after cleanup:', error);
-            // Last resort - clear everything
-            localStorage.removeItem(this.STORAGE_KEY);
-            localStorage.removeItem(this.STORAGE_KEY + '_stats');
-        }
-    }
-
-    /**
-     * Fetch operations from the server
-     *
-     * @param {boolean} initial Whether this is the initial fetch
-     * @param {Object} options Filter options
-     * @returns {Promise<Object>} Server response
-     */
-    async fetchOperations(initial = false, options = {}) {
-        this.processing = true;
-
-        // Start loading indicator
-        if (this.refreshButton) {
-            this.refreshButton.classList.add('refreshing');
-        }
-
-        try {
-            // Build query parameters
-            const params = new URLSearchParams();
-
-            if (options.status) {
-                params.append('status', options.status);
-            }
-
-            if (options.ids) {
-                params.append('ids', Array.isArray(options.ids) ? options.ids.join(',') : options.ids);
-            }
-
-            if (options.limit) {
-                params.append('limit', options.limit);
-            }
-
-            const url = `${this.API}?${params.toString()}`;
-
-            // Set up fetch options
-            const fetchOptions = {
-                method: 'GET',
-                headers: {
-                    'X-WP-Nonce': jvbSettings.nonce
-                }
-            };
-
-			// Add ETag support
-			if (!initial && !options.force && this.pollingConfig.lastETag) {
-				fetchOptions.headers['If-None-Match'] = this.pollingConfig.lastETag;
-			}
-
-			// Add If-Modified-Since as backup
-			if (!initial && !options.force && this.lastPollTime) {
-				fetchOptions.headers['If-Modified-Since'] = new Date(this.lastPollTime).toUTCString();
-			}
-
-            // Execute fetch
-            const response = await fetch(url, fetchOptions);
-
-            // If 304 Not Modified, nothing has changed
-            if (response.status === 304) {
-                this.pollingConfig.consecutiveNoChanges++;
-				return { operations: [], cached: true };
-            }
-
-			// Store ETag for next request
-			const etag = response.headers.get('ETag');
-			if (etag) {
-				this.pollingConfig.lastETag = etag;
-			}
-
-            // Reset consecutive no changes counter
-            this.pollingConfig.consecutiveNoChanges = 0;
-
-            // Update last poll time
-            this.lastPollTime = new Date().toISOString();
-
-            // Parse response
-            const data = await response.json();
-
-            // Process server operations
-            if (data.operations && Array.isArray(data.operations)) {
-                for (const op of data.operations) {
-                    // Convert to expected format
-                    const operation = {
-                        id: op.id,
-                        type: op.type,
-                        data: op.data || {},
-                        user: op.user_id,
-                        status: op.status,
-                        retries: op.retries || 0,
-                        started_at: op.started_at,
-                        created_at: op.created_at,
-                        completed_at: op.completed_at,
-                        estimated_completion: op.estimated_completion,
-                        queue_position: op.queue_position,
-                        error_message: op.error_message
-                    };
-
-                    // Update or add to local queue
-                    this.updateItem(op.id, operation);
-                }
-            }
-
-            // Update UI
-            this.maybeAddEmptyState();
-            this.updateStatusPanel();
-            this.updateFilterCounts();
-
-            return data;
-        } catch (error) {
-            console.error('Failed to fetch operations:', error);
-
-			return { operations: [], error: true };
-        } finally {
-            // End loading indicator
-            if (this.refreshButton) {
-                this.refreshButton.classList.remove('refreshing');
-            }
-            this.processing = false;
-        }
-    }
-
-    /**
-     * Update a queue item
-     *
-     * @param {string} id Item ID
-     * @param {Object} newData New data to merge
-     * @returns {boolean} Success
-     */
-    updateItem(id, newData) {
-        // If item doesn't exist yet, create it
-        if (!this.queue.has(id)) {
-            this.queue.set(id, {
-                id,
-                ...newData
-            });
-            this.saveQueue();
-            this.updatePanelItem(this.queue.get(id));
-            return true;
-        }
-
-        // Get existing item
-        const item = this.queue.get(id);
-        const oldStatus = item.status;
-
-        // Merge updates
-        const updated = {
-            ...item,
-            ...newData
-        };
-
-        // Start polling if server-side processing is happening
-        if (['pending', 'processing'].includes(updated.status) &&
-            oldStatus !== updated.status) {
-            this.startPolling();
-        }
-
-        // Update queue
-        this.queue.set(id, updated);
-        this.saveQueue();
-
-        // Update UI
-        this.updatePanelItem(updated, oldStatus);
-
-        // Handle status transitions
-        if (oldStatus !== updated.status) {
-            this.handleStatusChange(updated, oldStatus);
-        }
-
-        return true;
-    }
-
-    /**
-     * Handle status change for an item
-     *
-     * @param {Object} item Item that changed
-     * @param {string} oldStatus Previous status
-     */
-    handleStatusChange(item, oldStatus) {
-        // Clear caches when operation completes
-        if (item.status === 'completed' && oldStatus !== 'completed') {
-            const cacheTypes = this.cacheTypesToClear[item.type];
-            if (cacheTypes && cacheTypes.length > 0) {
-                cacheTypes.forEach(cacheType => {
-                    this.cache.clearByContent(cacheType);
-                });
-            }
-
-            // Show completion notification
-            this.addPopup(`${this.getOperationTitle(item)} completed`);
-        }
-        // Log failures
-        else if (item.status === 'failed' && oldStatus !== 'failed') {
-            this.errors.logErrorToServer('operation_failed', item.error_message || 'Operation failed', {
-                operation_id: item.id,
-                type: item.type,
-                content_type: item.data.content,
-                retries: item.retries
-            });
-
-            // Show failure notification
-            this.addPopup(`Error: ${item.error_message || 'Unknown error'}`);
-        }
-    }
-
-    /**
-     * Update panel item in the UI
-     *
-     * @param {Object} item Item to update
-     * @param {string} oldStatus Previous status
-     */
-    updatePanelItem(item, oldStatus = null) {
-        // Find existing panel item or create new one
-        let panelItem = this.panelList.querySelector(`.queue-item[data-id="${item.id}"]`);
-        if (!panelItem) {
-            panelItem = this.createPanelItem(item);
-            return;
-        }
-
-        // Update status classes
-        if (oldStatus) {
-            panelItem.classList.remove(oldStatus);
-        } else {
-            this.statuses.forEach(status => panelItem.classList.remove(status));
-        }
-        panelItem.classList.add(item.status);
-
-        // Update status text
-        const statusElement = panelItem.querySelector('.queue-item-status');
-        if (statusElement) {
-            if (oldStatus) {
-                statusElement.classList.remove(oldStatus);
-            } else {
-                this.statuses.forEach(status => statusElement.classList.remove(status));
-            }
-            statusElement.classList.add(item.status);
-            statusElement.textContent = this.getStatusLabel(item.status);
-        }
-
-        // Update details
-        const details = panelItem.querySelector('.queue-item-details');
-        if (details) {
-            const detailsText = details.querySelector('.details');
-            if (detailsText) {
-                detailsText.textContent = this.getItemMessage(item);
-            }
-
-            // Update completion time if available
-            const completed = details.querySelector('.completed');
-            if (completed && item.completed_at) {
-                completed.textContent = `| Finished: ${window.formatTimeAgo(item.completed_at)}`;
-            }
-        }
-
-        // Update progress bar
-        const progressBar = panelItem.querySelector('.progress-bar .progress-fill');
-        if (progressBar) {
-            progressBar.style.width = `${this.calculateProgressPercentage(item)}%`;
-        }
-
-        // Update action buttons for completed/failed items
-        if (['completed', 'failed', 'failed_permanent'].includes(item.status)) {
-            const actionsContainer = panelItem.querySelector('.queue-item-actions');
-            if (actionsContainer) {
-                actionsContainer.innerHTML = this.renderActionButtons(item);
-            }
-        }
-    }
-
-    /**
-     * Create a new panel item
-     *
-     * @param {Object} item Item to create
-     * @returns {HTMLElement} Created panel item
-     */
-    createPanelItem(item) {
-        const panelItem = document.createElement('div');
-        panelItem.className = `queue-item ${item.status}`;
-        panelItem.dataset.id = item.id;
-        panelItem.dataset.type = item.type;
-
-        // Format timestamps
-        const timestamp = item.created_at ? new Date(item.created_at).getTime() : item.started_at;
-        const timeDisplay = timestamp ? window.formatTimeAgo(new Date(timestamp)) : '';
-
-        // Calculate progress
-        const progressPercent = this.calculateProgressPercentage(item);
-
-        // Get operation type display name
-        let typeName = this.operationNames[item.type] || item.type;
-        if (typeName && typeName.includes('Content') && item.data && item.data.content) {
-            typeName = typeName.replace('Content', window.uppercaseFirst(item.data.content));
-        }
-
-        // Build HTML
-        panelItem.innerHTML = `
-            <div class="queue-item-header">
-                <span class="queue-item-type">${typeName}</span>
-                <span class="queue-item-status ${item.status}">
-                    ${this.getStatusLabel(item.status)}
-                </span>
-                ${['completed', 'failed', 'failed_permanent'].includes(item.status) ?
-            '<button class="queue-item-dismiss dismiss-operation" title="Dismiss">&times;</button>' : ''}
-            </div>
-
-            <div class="queue-item-progress">
-                <div class="progress-bar">
-                    <div class="progress-fill" style="width: ${progressPercent}%"></div>
-                </div>
-                <div class="progress-details">
-                    ${item.progress ?
-            `${item.progress.current || 0} of ${item.progress.total || '?'}` : ''}
-                </div>
-            </div>
-
-            <div class="queue-item-details">
-                <div class="details">${this.getItemMessage(item)}</div>
-                <div class="time">
-                    ${jvbSettings.icons.clock}
-                    <span class="started">Started: ${timeDisplay}</span>
-                    <span class="completed"></span>
-                </div>
-            </div>
-
-            <div class="queue-item-actions">
-                ${this.renderActionButtons(item)}
-            </div>
-        `;
-
-        // Add to panel
-        this.panelList.insertBefore(panelItem, this.panelList.firstChild);
-
-        return panelItem;
-    }
-
-    /**
-     * Get status label for display
-     *
-     * @param {string} status Operation status
-     * @returns {string} Human-readable status
-     */
-    getStatusLabel(status) {
-        switch (status) {
-            case 'queued':
-                return 'Received';
-            case 'localProcessing':
-                return 'Processing Data';
-            case 'uploading':
-                return 'Sending to Server';
-            case 'pending':
-                return 'Sent to Server';
-            case 'processing':
-                return 'Server processing';
-            case 'completed':
-                return 'Completed';
-            case 'failed':
-                return 'Failed';
-            case 'failed_permanent':
-                return 'Failed';
-            default:
-                return status;
-        }
-    }
-
-    /**
-     * Get detailed message for an item
-     *
-     * @param {Object} item Queue item
-     * @returns {string} Detailed message
-     */
-    getItemMessage(item) {
-        // Start with default message
-        let message = this.statusMessages[item.status] || '';
-
-        // Add position info if available
-        if (item.queue_position) {
-            if (item.queue_position === 0) {
-                message += ' You are next in line.';
-            } else {
-                message += ` You are #${item.queue_position} in line.`;
-            }
-        }
-
-        // Add estimated completion time if available
-        if (item.estimated_completion) {
-            message += `\nShould be done ${window.formatTimeSoon(item.estimated_completion)}`;
-        }
-
-        // Add error if failed
-        if ((item.status === 'failed' || item.status === 'failed_permanent') && item.error_message) {
-            message += `\nError: ${item.error_message}`;
-        }
-
-        return item.message || message;
-    }
-
-    /**
-     * Render action buttons for an item
-     *
-     * @param {Object} item Queue item
-     * @returns {string} HTML for action buttons
-     */
-    renderActionButtons(item) {
-        if (item.status === 'failed' || item.status === 'failed_permanent') {
-            return `
-                <button class="retry-operation" ${item.retries >= this.maxRetries ? 'disabled' : ''}>
-                    Retry
-                </button>
-                <button class="dismiss-operation">
-                    Dismiss
-                </button>
-            `;
-        }
-
-        if (item.status === 'completed') {
-            // Add refresh button for content updates
-            let refresh = ['batch_creation', 'content_update'].includes(item.type)
-                ? '<button class="refresh-content">Refresh</button>'
-                : '';
-
-            return `
-                ${refresh}
-                <button class="dismiss-operation">
-                    Dismiss
-                </button>
-            `;
-        }
-
-        return '';
-    }
-
-    /**
-     * Calculate progress percentage
-     *
-     * @param {Object} operation Operation
-     * @returns {number} Progress percentage (0-100)
-     */
-    calculateProgressPercentage(operation) {
-        switch (operation.status) {
-            case 'queued':
-            case 'failed':
-            case 'failed_permanent':
-                return 0;
-
-            case 'localProcessing':
-                // Use progress data if available
-                if (operation.progress && operation.progress.current && operation.progress.total) {
-                    return 5 + (35 * (operation.progress.current / operation.progress.total));
-                }
-                return 5;
-
-            case 'uploading':
-                return 40;
-
-            case 'pending':
-                return 65;
-
-            case 'processing':
-                if (operation.data && operation.data.progress) {
-                    return 75 + (25 * (operation.data.progress.percentage / 100));
-                }
-                return 85;
-
-            case 'completed':
-                return 100;
-
-            default:
-                return 0;
-        }
-    }
-
-    /**
-     * Get operation title
-     *
-     * @param {Object} item Operation item
-     * @returns {string} Human-readable title
-     */
-    getOperationTitle(item) {
-        let message = this.operationNames[item.type] || item.type;
-
-        if (message.includes('Content') && item.data && item.data.content) {
-            return message.replace('Content', window.uppercaseFirst(item.data.content));
-        }
-
-        return message;
-    }
-
-    /**
-     * Get popup message for an operation
-     *
-     * @param {Object} item Operation item
-     * @returns {string} Message for popup
-     */
-    getPopupMessage(item) {
-        switch (item.type) {
-            case 'handle_vote':
-                return 'Adding your voice...';
-            case 'new_response':
-                return 'Adding your voice...';
-            case 'new_news':
-                return 'Creating new news...';
-            case 'image_upload':
-                return 'Uploading image...';
-            case 'user_settings':
-                return 'Updating settings...';
-            case 'content_update':
-                return `Updating ${item.data.content}...`;
-            case 'content_create':
-                return `Creating new ${item.data.content}...`;
-            case 'favourite_toggle':
-                return 'Updating Favourites...';
-            case 'invite_artist':
-                return 'Inviting artists...';
-            case 'bio_update':
-                return 'Updating bio...';
-            case 'batch_creation':
-                return `Sending ${item.data.content} batch to server...`;
-            case 'favourite_notes':
-                return 'Processing note...';
-            case 'favourite_list_create':
-                return 'Processing new list...';
-            case 'favourite_list_add':
-                return 'Processing list changes...';
-            case 'favourite_list_remove':
-                return 'Processing list changes...';
-            case 'favourite_list_delete':
-                return 'Processing deletion...';
-            case 'favourite_list_share':
-                return 'Processing list sharing...';
-            case 'favourite_list_unshare':
-                return 'Processing un-share...';
-            default:
-                return `Processing ${item.type}...`;
-        }
-    }
-
-    /**
-     * Show popup message
-     *
-     * @param {string} message Message to show
-     * @param {number} delay Time in ms to show popup
-     */
-    addPopup(message, delay = 2000) {
-        if (!this.popup) return;
-
-        this.a11y.announce(message);
-        this.popup.innerHTML = message;
-        this.popup.classList.add('showing');
-
-        setTimeout(() => {
-            this.popup.classList.remove('showing');
-            setTimeout(() => {
-                // Clear popup content after fadeout
-                while (this.popup.firstChild) {
-                    this.popup.removeChild(this.popup.firstChild);
-                }
-            }, 50);
-        }, delay);
-    }
-
-
-    /**
-     * Update network indicator
-     */
-    updateNetworkIndicator() {
-        // Update UI based on online status
-        if (this.panel) {
-            this.panel.classList.toggle('offline', !navigator.onLine);
-        }
-    }
-
-    /**
-     * Update status panel
-     *
-     * @param {string} status Special status to display
-     */
-    updateStatusPanel(status) {
-        if (!this.panel) return;
-
-        // Get pending count for badge
-        const pendingCount = [...this.queue.values()].filter(item =>
-            ['queued', 'localProcessing', 'uploading'].includes(item.status)
-        ).length;
-
-        // Update indicator status
-        this.panel.classList.remove('offline', 'pending', 'synced', 'image_processing');
-
-        if (!navigator.onLine) {
-            this.panel.classList.add('offline');
-            this.addPopup('Looks like we\'re offline...');
-        } else if (status === 'pending' || pendingCount > 0) {
-            this.panel.classList.add('pending');
-        } else if (status === 'image_processing') {
-            this.panel.classList.add('image_processing');
-            this.addPopup('Processing images...');
-        } else if (status === 'synced' || pendingCount === 0) {
-            this.panel.classList.add('synced');
-        }
-
-        // Update queue stats
-        this.updateQueueStats();
-    }
-
-    /**
-     * Update queue statistics
-     */
-    updateQueueStats() {
-        // Calculate stats
-        const stats = this.getQueueStats();
-
-        // Update filter buttons with counts
-        Object.entries(stats).forEach(([status, count]) => {
-            const button = this.statusButtons[status];
-            if (button) {
-                button.dataset.count = count;
-
-                // Update count badge
-                if (count > 0) {
-                    let badge = button.querySelector('.count-badge');
-                    if (!badge) {
-                        badge = document.createElement('span');
-                        badge.className = 'count-badge';
-                        button.appendChild(badge);
-                    }
-                    badge.textContent = count;
-                } else {
-                    const badge = button.querySelector('.count-badge');
-                    if (badge) {
-                        badge.remove();
-                    }
-                }
-            }
-        });
-
-        // Update the status indicator badge
-        const badgeCount = stats.queued + stats.localProcessing + stats.uploading;
-        const statusIndicator = this.panel.querySelector('.queue-status-indicator');
-        const statusCount = this.panel.querySelector('.queue-status-count');
-
-        if (statusIndicator) {
-            statusIndicator.classList.toggle('active', badgeCount > 0);
-        }
-
-        if (statusCount) {
-            statusCount.textContent = badgeCount > 0 ? badgeCount : '';
-        }
-
-        // Update action buttons
-        if (this.retryButton) {
-            this.retryButton.disabled = stats.failed === 0;
-        }
-
-        if (this.clearButton) {
-            this.clearButton.disabled = stats.completed === 0;
-        }
-    }
-
-    /**
-     * Get queue statistics
-     *
-     * @returns {Object} Queue statistics by status
-     */
-    getQueueStats() {
-        const stats = {};
-
-        // Initialize all statuses to 0
-        this.statuses.forEach(status => {
-            stats[status] = 0;
-        });
-
-        // Count items by status
-        for (const item of this.queue.values()) {
-            if (stats[item.status] !== undefined) {
-                stats[item.status]++;
-            }
-        }
-
-        return stats;
-    }
-
-
-    /**
-     * Update filter counts
-     */
-    updateFilterCounts() {
-        const stats = this.getQueueStats();
-
-        if (this.filterButtons) {
-            this.filterButtons.forEach(button => {
-                const filter = button.dataset.filter;
-                if (!filter || filter === 'all') return;
-
-                const count = stats[filter] || 0;
-                button.dataset.count = count;
-
-                // Update badge
-                let badge = button.querySelector('.count-badge');
-                if (count > 0) {
-                    if (!badge) {
-                        badge = document.createElement('span');
-                        badge.className = 'count-badge';
-                        button.appendChild(badge);
-                    }
-                    badge.textContent = count;
-                } else if (badge) {
-                    badge.remove();
-                }
-            });
-        }
-    }
-
-    /**
-     * Process the queue
-     *
-     * @returns {Promise<void>}
-     */
-    async processQueue() {
-        if (!this.hasChanges) return;
-
-        // Check if we're online
-        if (!navigator.onLine) {
-            this.updateStatusPanel('offline');
-            return;
-        }
-
-        try {
-            // Find items that are ready to process
-            const pendingItems = [...this.queue.values()].filter(item =>
-                item.status === 'queued' &&
-                (!item.retryAfter || item.retryAfter <= Date.now())
-            );
-
-            if (pendingItems.length === 0) {
-                this.hasChanges = false;
-                return;
-            }
-
-            // Only process a batch at a time
-            const batchSize = 5;
-            const itemsToProcess = pendingItems.slice(0, batchSize);
-
-            // Update UI
-            this.updateStatusPanel('pending');
-
-            // Process each item
-            for (const item of itemsToProcess) {
-                try {
-                    this.a11y.announce(`Now processing ${this.getOperationTitle(item)} locally.`);
-
-                    await this.processItem(item);
-
-                    this.a11y.announce('Finished sending to server.');
-                } catch (error) {
-                    // Log error
-                    this.errors.log(error, {
-                        component: 'QueueManager',
-                        action: 'processQueue',
-                        type: item.type,
-                        operation_id: item.id
-                    });
-
-                    // Update item status
-                    this.updateItem(item.id, {
-                        status: (item.retries >= this.maxRetries - 1) ? 'failed_permanent' : 'failed',
-                        error_message: error.message || 'Unknown error',
-                        retries: (item.retries || 0) + 1,
-                        retryAfter: (item.retries < this.maxRetries - 1) ?
-                            Date.now() + (5000 * Math.pow(2, item.retries || 0)) : undefined
-                    });
-
-                    // Notify user
-                    this.addPopup(`Error processing ${this.getOperationTitle(item)}: ${error.message || 'Unknown error'}`);
-                }
-            }
-
-            // Update UI
-            this.updateStatusPanel();
-
-            // Check if there are more items to process
-            const remainingPending = [...this.queue.values()].filter(item =>
-                ['queued', 'localProcessing'].includes(item.status)
-            ).length;
-
-            if (remainingPending > 0) {
-                // Schedule another processing round
-                setTimeout(() => {
-                    this.hasChanges = true;
-                    this.processQueue();
-                }, 1000);
-            } else {
-                this.hasChanges = false;
-                this.updateStatusPanel('synced');
-            }
-        } catch (error) {
-            console.error('Error in queue processing:', error);
-            this.addPopup('Whoops! Something went wrong');
-        }
-    }
-
-    /**
-     * Process a single queue item
-     *
-     * @param {Object} item Queue item to process
-     * @returns {Promise<void>}
-     */
-    async processItem(item) {
-        this.updateItem(item.id, {status: 'localProcessing'});
-
-        try {
-            // Find the processor for this operation type
-            const processor = this.processors[item.type];
-            if (!processor) {
-                throw new Error(`Unknown operation type: ${item.type}`);
-            }
-
-            // Process the item
-            this.a11y.announce(`Processing ${this.getOperationTitle(item)} locally.`);
-            const result = await processor(item);
-
-            // Store result and update status
-            this.updateItem(item.id, {
-                result: result,
-                status: 'pending'
-            });
-
-            this.a11y.announce(`${this.getOperationTitle(item)} sent to server for processing.`);
-
-            // Start polling for status updates
-            this.startPolling();
-        } catch (error) {
-            this.handleProcessError(item, error);
-            throw error;
-        }
-    }
-
-    /**
-     * Handle processing error
-     *
-     * @param {Object} item Item that failed
-     * @param {Error} error The error
-     */
-    handleProcessError(item, error) {
-        console.error(`Error processing ${item.type}:`, error);
-
-        const errorContext = {
-            operation_id: item.id,
-            type: item.type,
-            content_type: item.data.content,
-            attempt: (item.retries || 0) + 1
-        };
-
-        // Use ErrorHandler
-        this.errors.log(error, {
-            component: 'QueueManager',
-            action: item.type,
-            ...errorContext
-        });
-
-        // Update item
-        this.updateItem(item.id, {
-            status: (item.retries >= this.maxRetries - 1) ? 'failed_permanent' : 'failed',
-            error_message: error.message || 'Unknown error',
-            retries: (item.retries || 0) + 1,
-            retryAfter: (item.retries < this.maxRetries - 1) ?
-                Date.now() + (5000 * Math.pow(2, item.retries || 0)) : undefined
-        });
-
-        // Notify user
-        this.addPopup(`Error processing ${this.getOperationTitle(item)}: ${error.message || 'Unknown error'}`);
-    }
-
-    /**
-     * Make API request for an operation
-     *
-     * @param {Object} item Operation item
-     * @param {string} endpoint API endpoint
-     * @param {string} method HTTP method
-     * @param {Object|FormData} data Request data
-     * @param {Object} additionalHeaders Additional headers
-     * @param {string} title Operation title for messages
-     * @returns {Promise<Object>} Response data
-     */
-    async makeRequest(item, endpoint, method, data, additionalHeaders = {}, title) {
-        this.updateItem(item.id, {status: 'uploading'});
-
-
-        const isFormData = data instanceof FormData;
-        const headers = {
-            'X-WP-Nonce': jvbSettings.nonce,
-            ...additionalHeaders
-        };
-
-		if (!isFormData && method !== 'GET') {
-			headers['Content-Type'] = 'application/json';
-		}
-
-		console.log('Sending Data: ',data);
-
-        try {
-            const response = await fetch(`${jvbSettings.api}${endpoint}`, {
-                method,
-                headers,
-                body: isFormData ? data : JSON.stringify(data)
-            });
-
-
-
-            if (!response.ok) {
-                const errorData = await response.json().catch(() => ({}));
-                throw new Error(errorData.message || `Request failed with s[tatus ${response.status}`);
-            }
-
-            return await response.json();
-        } catch (error) {
-            this.handleProcessError(item, error);
-            throw error;
-        }
-    }
-
-    /**
-     * Start polling for operation status updates
-     */
-	startPolling() {
-		this.stopPolling();
-
-		const interval = this.getPollingInterval();
-
-		// Log polling strategy (remove in production)
-		console.log(`Polling in ${interval}ms (no-changes: ${this.pollingConfig.consecutiveNoChanges})`);
-
-		if (this.pollingConfig.countdown && this.countdown) {
-			this.startCountdown(interval / 1000);
-		}
-
-		this.pollingTimer = setTimeout(() => {
-			this.fetchOperations(false).then((result) => {
-				// Only continue polling if we have pending operations or recent activity
-				const shouldContinue = this.hasServerPendingOperations() ||
-					(Date.now() - this.pollingConfig.lastUserActivity) < this.pollingConfig.inactiveThreshold;
-
-				if (shouldContinue) {
-					this.startPolling();
-				} else {
-					console.log('Stopping polling: no pending operations and user inactive');
-					this.stopPolling();
-					this.updateStatusPanel('synced');
-				}
-			});
-		}, interval);
-	}
-
-    /**
-     * Stop polling
-     */
-    stopPolling() {
-        if (this.pollingTimer) {
-            clearTimeout(this.pollingTimer);
-            this.pollingTimer = null;
-        }
-
-        if (this.countdownTimer) {
-            clearInterval(this.countdownTimer);
-            this.countdownTimer = null;
-        }
-
-        // Remove refreshing state
-        if (this.refreshButton) {
-            this.refreshButton.classList.remove('refreshing');
-        }
-    }
-
-    /**
-     * Get adaptive polling interval
-     *
-     * @returns {number} Polling interval in milliseconds
-     */
-	getPollingInterval() {
-		const now = Date.now();
-		const timeSinceActivity = now - this.pollingConfig.lastUserActivity;
-		const isUserActive = timeSinceActivity < this.pollingConfig.inactiveThreshold;
-
-		// Base interval
-		let interval = this.pollingConfig.interval;
-
-		// Increase interval based on consecutive no-changes
-		const noChangesCount = Math.min(
-			this.pollingConfig.consecutiveNoChanges,
-			this.pollingConfig.maxNoChangesCount
-		);
-
-		interval *= Math.pow(this.pollingConfig.backoffFactor, noChangesCount);
-
-		// If user is inactive, poll less frequently
-		if (!isUserActive) {
-			interval *= 3; // Poll 3x less when user is inactive
-		}
-
-		// If we have pending operations, poll more frequently
-		const hasPendingOps = this.hasServerPendingOperations();
-		if (hasPendingOps && isUserActive) {
-			interval = Math.max(interval * 0.5, this.pollingConfig.minInterval);
-		}
-
-		// Clamp between min and max
-		return Math.min(
-			Math.max(interval, this.pollingConfig.minInterval),
-			this.pollingConfig.maxInterval
-		);
-	}
-
-    /**
-     * Start countdown timer
-     *
-     * @param {number} seconds Seconds to count down
-     */
-    startCountdown(seconds) {
-        // Clear existing countdown
-        if (this.countdownTimer) {
-            clearInterval(this.countdownTimer);
-        }
-
-        if (!this.countdown) return;
-
-        // Set initial value
-        let remainingSeconds = Math.round(seconds);
-        this.countdown.textContent = remainingSeconds;
-        this.countdown.classList.add('counting');
-
-        // Start countdown
-        this.countdownTimer = setInterval(() => {
-            remainingSeconds--;
-
-            if (remainingSeconds <= 0) {
-                clearInterval(this.countdownTimer);
-                this.countdownTimer = null;
-                this.countdown.textContent = '';
-                this.countdown.classList.remove('counting');
-                return;
-            }
-
-            this.countdown.textContent = remainingSeconds;
-        }, 1000);
-    }
-
-
-    /**
-     * Check if there are pending operations on the server
-     *
-     * @returns {boolean} Whether there are pending server operations
-     */
-    hasServerPendingOperations() {
-        return [...this.queue.values()].some(item =>
-            item.status === 'pending' ||
-            item.status === 'processing'
-        );
-    }
-
-    /**
-     * Retry a failed operation
-     *
-     * @param {string} operationId ID of operation to retry
-     */
-    retryOperation(operationId) {
-        const operation = this.queue.get(operationId);
-
-        if (operation && (operation.status === 'failed' || operation.status === 'failed_permanent')) {
-            // Send request to server
-            fetch(`${this.API}`, {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                    'X-WP-Nonce': jvbSettings.nonce
-                },
-                body: JSON.stringify({
-                    ids: operationId,
-                    action: 'retry'
-                })
-            }).then(response => {
-                if (!response.ok) {
-                    throw new Error('Failed to retry operation');
-                }
-                return response.json();
-            }).then(data => {
-                if (data.success) {
-                    // Update local status
-                    this.updateItem(operationId, {
-                        status: 'pending',
-                        error_message: null
-                    });
-
-                    // Start polling for status updates
-                    this.startPolling();
-
-                    // Show notification
-                    this.addPopup('Operation queued for retry');
-                }
-            }).catch(error => {
-                console.error('Error retrying operation:', error);
-                this.addPopup('Failed to retry operation');
-            });
-        }
-    }
-
-    /**
-     * Retry all failed operations
-     */
-    retryFailedOperations() {
-        // Find failed operations
-        const failedIds = [...this.queue.values()]
-            .filter(item =>
-                (item.status === 'failed' || item.status === 'failed_permanent') &&
-                item.retries < this.maxRetries
-            )
-            .map(item => item.id);
-
-        if (failedIds.length === 0) return;
-
-        // Confirm with user
-        if (confirm(`Retry ${failedIds.length} failed operations?`)) {
-            // Send request to server
-            fetch(`${this.API}`, {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                    'X-WP-Nonce': jvbSettings.nonce
-                },
-                body: JSON.stringify({
-                    ids: failedIds.join(','),
-                    action: 'retry'
-                })
-            }).then(response => {
-                if (!response.ok) {
-                    throw new Error('Failed to retry operations');
-                }
-                return response.json();
-            }).then(data => {
-                if (data.success) {
-                    // Update local status for each operation
-                    failedIds.forEach(id => {
-                        this.updateItem(id, {
-                            status: 'pending',
-                            error_message: null
-                        });
-                    });
-
-                    // Start polling for status updates
-                    this.startPolling();
-
-                    // Show notification
-                    this.addPopup(`Retrying ${failedIds.length} operations...`);
-                }
-            }).catch(error => {
-                console.error('Error retrying operations:', error);
-                this.addPopup('Failed to retry operations');
-            });
-        }
-    }
-
-    /**
-     * Clear completed operations
-     */
-    clearCompletedOperations() {
-        // Find completed operations
-        const completedIds = [...this.queue.values()]
-            .filter(item => item.status === 'completed')
-            .map(item => item.id);
-
-        if (completedIds.length === 0) return;
-
-        // Dismiss operations
-        this.dismissOperations(completedIds);
-    }
-
-    /**
-     * Dismiss operations
-     *
-     * @param {string[]} operationIds IDs of operations to dismiss
-     */
-    dismissOperations(operationIds) {
-        if (!operationIds.length) return;
-
-        // Send request to server
-        fetch(`${this.API}`, {
-            method: 'POST',
-            headers: {
-                'Content-Type': 'application/json',
-                'X-WP-Nonce': jvbSettings.nonce
-            },
-            body: JSON.stringify({
-                ids: operationIds.join(','),
-                action: 'dismiss'
-            })
-        }).then(response => {
-            if (!response.ok) {
-                throw new Error('Failed to dismiss operations');
-            }
-            return response.json();
-        }).then(data => {
-            if (data.success) {
-                // Remove from local queue
-                operationIds.forEach(id => {
-                    // Remove from UI with animation
-                    const item = this.panelList.querySelector(`.queue-item[data-id="${id}"]`);
-                    if (item) {
-                        item.style.opacity = '0';
-                        item.style.transition = 'opacity 0.3s ease-out';
-                        setTimeout(() => {
-                            item.remove();
-                            this.maybeAddEmptyState();
-                        }, 300);
-                    }
-
-                    // Remove from queue
-                    this.queue.delete(id);
-                });
-
-                // Save updated queue
-                this.saveQueue();
-
-                // Update UI
-                this.updateQueueStats();
-                this.updateFilterCounts();
-
-                // Notify user
-                this.a11y.announce('Removed completed tasks');
-            }
-        }).catch(error => {
-            console.error('Error dismissing operations:', error);
-            this.addPopup('Failed to dismiss operations');
-        });
-    }
-
-    //INDIVIDUAL PROCESSORS
-    /**
-     * Process vote operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processVote(item) {
-        item.data.id = item.id;
-        return await this.makeRequest(
-            item,
-            'vote',
-            'POST',
-            item.data,
-            {},
-            'Adding Your Voice'
-        );
-    }
-
-    /**
-     * Process artist invite operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processArtistInvite(item) {
-        const data = {
-            user: jvbSettings.currentUser,
-            invites: item.data,
-            id: item.id
-        };
-
-        return await this.makeRequest(
-            item,
-            'invitations',
-            'POST',
-            data,
-            { 'action_nonce': jvbSettings.dash },
-            'Inviting Artist'
-        );
-    }
-
-    /**
-     * Process new news operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processNewNews(item) {
-        const formData = new FormData();
-
-        // Append all data from item
-        for (const [key, value] of Object.entries(item.data)) {
-            formData.append(key, value);
-        }
-
-        formData.append('id', item.id);
-
-        return await this.makeRequest(
-            item,
-            'news',
-            'POST',
-            formData,
-            { 'action_nonce': jvbSettings.dash },
-            'Adding News Post'
-        );
-    }
-
-    /**
-     * Process new response operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processNewResponse(item) {
-        item.data.id = item.id;
-        return await this.makeRequest(
-            item,
-            'response',
-            'POST',
-            item.data,
-            {},
-            'Adding your response'
-        );
-    }
-
-    /**
-     * Process settings update operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processSettingsUpdate(item) {
-        const formData = new FormData();
-
-        // Append all data fields
-        for (const [key, value] of Object.entries(item.data)) {
-            formData.append(key, value);
-        }
-
-        formData.append('id', item.id);
-
-        return await this.makeRequest(
-            item,
-            'settings',
-            'POST',
-            formData,
-            { 'action_nonce': jvbSettings.dash },
-            'Updating Settings'
-        );
-    }
-
-    /**
-     * Process bio update operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processBioUpdate(item) {
-
-
-        item.data.id = item.id;
-		console.log(item);
-		console.log(item.data);
-
-        return await this.makeRequest(
-            item,
-            'bio',
-            'POST',
-            item.data,
-            { 'action_nonce': jvbSettings.dash },
-            'Updating Bio'
-        );
-    }
-
-    /**
-     * Process content update operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processContentUpdate(item) {
-        const apiData = {
-            id: item.id,
-            user: item.user,
-            posts: item.data.posts || {},
-            content: item.data.content
-        };
-
-        return await this.makeRequest(
-            item,
-            'set',
-            'POST',
-            apiData,
-            { 'action_nonce': jvbSettings.dash },
-            'Updating Content'
-        );
-    }
-
-    /**
-     * Process content creation operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processContentCreation(item) {
-        const apiData = {
-            id: item.id,
-            content: item.data.content,
-            ...item.data
-        };
-
-        return await this.makeRequest(
-            item,
-            'create',
-            'POST',
-            apiData,
-            { 'action_nonce': jvbSettings.dash },
-            `Creating ${item.data.content === 'artwork' ? 'Artwork' : window.uppercaseFirst(item.data.content)+'s'}`
-        );
-    }
-
-	/**
-	 * Process batch creation operation
-	 *
-	 * @param {Object} item Queue item
-	 * @returns {Promise<Object>} Operation result
-	 */
-	async processBatchCreation(item) {
-		const formData = new FormData();
-
-		// Add common data
-		formData.append('content', item.data.content);
-		formData.append('user', item.data.user);
-		formData.append('mode', item.data.mode);
-		formData.append('id', item.id);
-
-		// Process each batch
-		item.data.files.forEach((batch, batchIndex) => {
-			if (!batch.files || !Array.isArray(batch.files)) {
-				console.error('Invalid batch structure:', batch);
-				return;
-			}
-
-			// Add files for this batch - batch.files contains objects with {file: File, metadata: {}}
-			batch.files.forEach((fileObj, fileIndex) => {
-				// Extract the actual File object
-				const file = fileObj.file || fileObj.processedFile || fileObj;
-
-				if (file instanceof File) {
-					formData.append(`files[${batchIndex}][${fileIndex}]`, file);
-				} else {
-					console.error('Invalid file object:', fileObj);
-				}
-			});
-
-			// Add metadata for this batch - combine individual file metadata
-			const batchMetadata = {
-				type: batch.type,
-				metadata: batch.metadata || {},
-				files_metadata: batch.files.map(fileObj => fileObj.metadata || {})
-			};
-
-			formData.append(`files_data[${batchIndex}]`, JSON.stringify(batchMetadata));
-		});
-
-		return await this.makeRequest(
-			item,
-			'create/batch',
-			'POST',
-			formData,
-			{ 'action_nonce': jvbSettings.dash },
-			`Creating ${item.data.content === 'artwork' ? 'Artwork' : window.uppercaseFirst(item.data.content)+'s'}`
-		);
-	}
-
-    /**
-     * Process file upload operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-	async processFileUpload(item) {
-		const formData = new FormData();
-
-		// Add common fields
-		formData.append('content', item.data.content);
-		formData.append('user', item.data.user);
-		formData.append('id', item.id);
-		formData.append('mode', item.data.mode);
-
-		// Add optional fields
-		if (item.data.postId) {
-			formData.append('post_id', item.data.postId);
-		}
-
-		if (item.data.termId) {
-			formData.append('term_id', item.data.termId);
-		}
-
-		if (item.data.fieldName) {
-			formData.append('field_name', item.data.fieldName);
-		}
-
-		console.log('Processing files for upload:', item.data.files);
-
-		// Handle files properly based on structure
-		if (item.data.files && Array.isArray(item.data.files)) {
-			let fileIndex = 0;
-
-			item.data.files.forEach((fileGroup, groupIndex) => {
-				console.log(`Processing group ${groupIndex}:`, fileGroup);
-
-				if (fileGroup.files && Array.isArray(fileGroup.files)) {
-					fileGroup.files.forEach((fileObj) => {
-						// Extract the actual File object
-						let file = null;
-
-						if (fileObj instanceof File) {
-							file = fileObj;
-						} else if (fileObj.file instanceof File) {
-							file = fileObj.file;
-						} else if (fileObj.processedFile instanceof File) {
-							file = fileObj.processedFile;
-						}
-
-						if (file) {
-							console.log(`Adding file ${fileIndex}:`, file.name, file.size, file.type);
-							formData.append(`files[${fileIndex}]`, file);
-
-							// Add metadata if available
-							if (fileObj.metadata) {
-								formData.append(`metadata[${fileIndex}]`, JSON.stringify(fileObj.metadata));
-							}
-
-							fileIndex++;
-						} else {
-							console.error('Invalid file object:', fileObj);
-						}
-					});
-				} else {
-					console.error('Invalid file group structure:', fileGroup);
-				}
-			});
-
-			console.log(`Total files added to FormData: ${fileIndex}`);
-		}
-
-		return await this.makeRequest(
-			item,
-			'uploads',
-			'POST',
-			formData,
-			{ 'action_nonce': jvbSettings.dash },
-			'Uploading Files'
-		);
-	}
-
-    /**
-     * Process favourite toggle operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavourite(item) {
-        const batchData = {
-            adds: [],
-            removes: [],
-            id: item.id,
-            user: item.user
-        };
-
-        // Sort adds and removes
-        item.data.forEach(favItem => {
-            const action = favItem.action;
-            const itemCopy = {...favItem};
-            delete itemCopy.action;
-
-            if (action === 'add') {
-                batchData.adds.push(itemCopy);
-            } else {
-                batchData.removes.push(itemCopy);
-            }
-        });
-
-        return await this.makeRequest(
-            item,
-            'favourites',
-            'POST',
-            batchData,
-            { 'action_nonce': jvbSettings.dash },
-            'Setting Favourites'
-        );
-    }
-
-    /**
-     * Process favourite notes operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavouriteNotes(item) {
-        return await this.makeRequest(
-            item,
-            'favourites',
-            'POST',
-            {
-                operation: 'update_notes',
-                type: item.data.type,
-                target_id: item.data.target_id,
-                notes: item.data.notes,
-                id: item.id
-            },
-            { 'action_nonce': jvbSettings.favourites },
-            'Saving Note'
-        );
-    }
-
-    /**
-     * Process favourite list create operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavouriteListCreate(item) {
-        return await this.makeRequest(
-            item,
-            'favourites/lists',
-            'POST',
-            {
-                operation: 'create',
-                name: item.data.name,
-                description: item.data.description,
-                items: item.data.items,
-                id: item.id
-            },
-            { 'action_nonce': jvbSettings.favourites },
-            'Creating List'
-        );
-    }
-
-    /**
-     * Process adding items to a favourite list
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavouriteListAddItems(item) {
-        return await this.makeRequest(
-            item,
-            'favourites/lists',
-            'POST',
-            {
-                id: item.id,
-                items: item.data.items,
-                list_id: item.data.list_id,
-                operation: 'add_items'
-            },
-            { 'action_nonce': jvbSettings.favourites },
-            'Adding to List'
-        );
-    }
-
-    /**
-     * Process removing items from a favourite list
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavouriteListRemoveItems(item) {
-        return await this.makeRequest(
-            item,
-            'favourites/lists',
-            'POST',
-            {
-                id: item.id,
-                items: item.data.items,
-                list_id: item.data.list_id,
-                operation: 'remove_items'
-            },
-            { 'action_nonce': jvbSettings.favourites },
-            'Removing from List'
-        );
-    }
-
-    /**
-     * Process favourite list delete operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavouriteListDelete(item) {
-        return await this.makeRequest(
-            item,
-            'favourites/lists',
-            'POST',
-            {
-                operation: 'delete',
-                list_id: item.id
-            },
-            { 'action_nonce': jvbSettings.favourites },
-            'Deleting list'
-        );
-    }
-
-    /**
-     * Process favourite list share operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavouriteListShare(item) {
-        return await this.makeRequest(
-            item,
-            'favourites/lists/shares',
-            'POST',
-            {
-                operation: 'add',
-                email: item.data.email,
-                list_id: item.id},
-            { 'action_nonce': jvbSettings.favourites },
-            'Sharing List'
-        );
-    }
-
-    /**
-     * Process favourite list unshare operation
-     *
-     * @param {Object} item Queue item
-     * @returns {Promise<Object>} Operation result
-     */
-    async processFavouriteListUnshare(item) {
-        return await this.makeRequest(
-            item,
-            'favourites/lists/shares',
-            'POST',
-            {
-                operation: 'remove',
-                email: item.data.email,
-                list_id: item.id
-            },
-            { 'action_nonce': jvbSettings.favourites },
-            'Removing Share Access'
-        );
-    }
-
-    /**
-     * Try to merge similar operations to reduce queue size
-     *
-     * @param {Object} operation New operation
-     * @returns {Object|false} Merged operation or false if no merge
-     */
-    mergeOperations(operation) {
-        // Find pending operations of the same type
-        const pendingOperations = [...this.queue.values()].filter(item =>
-            item.status === 'queued' &&
-            item.type === operation.type
-        );
-
-        if (pendingOperations.length === 0) return false;
-
-        // Handle based on operation type
-        switch (operation.type) {
-            case 'content_update':
-                return this.mergeContentUpdates(pendingOperations, operation);
-
-            case 'user_settings':
-            case 'bio_update':
-                return this.mergeSettingsUpdates(pendingOperations, operation);
-
-            case 'favourite_toggle':
-                return this.mergeFavouriteToggles(pendingOperations, operation);
-
-            case 'image_upload':
-                return this.mergeImageUploads(pendingOperations, operation);
-
-            default:
-                return false;
-        }
-    }
-
-    /**
-     * Merge content update operations
-     *
-     * @param {Array} existingOperations Existing operations
-     * @param {Object} newOperation New operation
-     * @returns {Object|false} Merged operation or false
-     */
-    mergeContentUpdates(existingOperations, newOperation) {
-        if (!existingOperations.length) return false;
-
-        // Start with empty set of merged posts
-        const mergedPosts = {};
-
-        // Gather existing post data
-        existingOperations.forEach(operation => {
-            if (!operation.data || !operation.data.posts) return;
-
-            Object.entries(operation.data.posts).forEach(([postID, postData]) => {
-                if (!mergedPosts[postID]) {
-                    mergedPosts[postID] = { ...postData };
-                } else {
-                    mergedPosts[postID] = this.mergePostData(mergedPosts[postID], postData);
-                }
-            });
-        });
-
-        // Merge in new operation's posts
-        if (newOperation.data && newOperation.data.posts) {
-            Object.entries(newOperation.data.posts).forEach(([postID, postData]) => {
-                if (!mergedPosts[postID]) {
-                    mergedPosts[postID] = { ...postData };
-                } else {
-                    mergedPosts[postID] = this.mergePostData(mergedPosts[postID], postData);
-                }
-            });
-        }
-
-        // Remove all pending content updates
-        existingOperations.forEach(op => {
-            this.queue.delete(op.id);
-        });
-
-        // Return merged operation
-        return {
-            type: 'content_update',
-            data: {
-                posts: mergedPosts,
-                content: newOperation.data.content
-            }
-        };
-    }
-
-    /**
-     * Merge post data from multiple updates
-     *
-     * @param {Object} existing Existing post data
-     * @param {Object} update New post data
-     * @returns {Object} Merged post data
-     */
-    mergePostData(existing, update) {
-        const merged = { ...existing };
-
-        // Ensure content stays consistent
-        merged.content = existing.content;
-
-        // Merge fields
-        Object.entries(update).forEach(([key, value]) => {
-            if (key === 'taxonomies') {
-                // Special handling for taxonomies
-                merged.taxonomies = merged.taxonomies || {};
-                Object.entries(value).forEach(([taxKey, terms]) => {
-                    // Keep most recent terms
-                    merged.taxonomies[taxKey] = [...terms];
-                });
-            } else if (key !== 'content') {
-                // For other fields, take newest value
-                merged[key] = value;
-            }
-        });
-
-        return merged;
-    }
-
-    /**
-     * Merge settings update operations
-     *
-     * @param {Array} existingOperations Existing operations
-     * @param {Object} newOperation New operation
-     * @returns {Object|false} Merged operation or false
-     */
-    mergeSettingsUpdates(existingOperations, newOperation) {
-        if (!existingOperations.length) return false;
-
-        // Get most recent state of each field
-        const mergedFields = {};
-
-        // Gather existing fields
-        existingOperations.forEach(operation => {
-            if (!operation.data) return;
-
-            Object.entries(operation.data).forEach(([field, value]) => {
-                // Skip user field
-                if (field !== 'user') {
-                    if (Array.isArray(value)) {
-                        // For arrays, merge and remove duplicates
-                        mergedFields[field] = mergedFields[field] || [];
-                        mergedFields[field] = [...new Set([...mergedFields[field], ...value])];
-                    } else {
-                        // For regular fields, take latest value
-                        mergedFields[field] = value;
-                    }
-                }
-            });
-        });
-
-        // Merge in new operation's fields
-        if (newOperation.data) {
-            Object.entries(newOperation.data).forEach(([field, value]) => {
-                if (field !== 'user') {
-                    if (Array.isArray(value)) {
-                        mergedFields[field] = mergedFields[field] || [];
-                        mergedFields[field] = [...new Set([...mergedFields[field], ...value])];
-                    } else {
-                        mergedFields[field] = value;
-                    }
-                }
-            });
-        }
-
-        // Remove pending operations
-        existingOperations.forEach(op => {
-            this.queue.delete(op.id);
-        });
-
-        // Return merged operation
-        return {
-            type: newOperation.type,
-            data: {
-                user: newOperation.data.user,
-                ...mergedFields
-            }
-        };
-    }
-
-    /**
-     * Merge favourite toggle operations
-     *
-     * @param {Array} existingOperations Existing operations
-     * @param {Object} newOperation New operation
-     * @returns {Object|false} Merged operation or false
-     */
-    mergeFavouriteToggles(existingOperations, newOperation) {
-        if (!existingOperations.length || !newOperation.data || !newOperation.data[0]) return false;
-
-        // Find toggle for this target
-        const targetId = newOperation.data[0].target_id;
-        const targetType = newOperation.data[0].type;
-
-        const existingOperation = existingOperations.find(op =>
-            op.data && op.data[0] &&
-            op.data[0].target_id === targetId &&
-            op.data[0].type === targetType
-        );
-
-        if (existingOperation) {
-            // Use latest toggle action
-            this.queue.delete(existingOperation.id);
-            return newOperation;
-        }
-
-        return false;
-    }
-
-    /**
-     * Merge image upload operations
-     *
-     * @param {Array} existingOperations Existing operations
-     * @param {Object} newOperation New operation
-     * @returns {Object|false} Merged operation or false
-     */
-    mergeImageUploads(existingOperations, newOperation) {
-        if (!existingOperations.length || !newOperation.data) return false;
-
-        // Only merge if there's a groupId
-        if (!newOperation.data.groupId) return false;
-
-        // Find uploads with same groupId
-        const groupUploads = existingOperations.filter(op =>
-            op.data && op.data.groupId === newOperation.data.groupId
-        );
-
-        if (!groupUploads.length) return false;
-
-        // Collect all files from group
-        const files = [];
-
-        // Add files from existing operations
-        groupUploads.forEach(op => {
-            if (op.data.files) {
-                files.push(...op.data.files);
-            } else if (op.data.file) {
-                files.push(op.data.file);
-            }
-        });
-
-        // Add files from new operation
-        if (newOperation.data.files) {
-            files.push(...newOperation.data.files);
-        } else if (newOperation.data.file) {
-            files.push(newOperation.data.file);
-        }
-
-        // Remove individual uploads
-        groupUploads.forEach(op => {
-            this.queue.delete(op.id);
-        });
-
-        // Return batch operation
-        return {
-            type: 'image_upload',
-            data: {
-                groupId: newOperation.data.groupId,
-                content: newOperation.data.content,
-                postId: newOperation.data.postId,
-                fieldName: newOperation.data.fieldName,
-                files: files
-            }
-        };
-    }
-
-    //TODO: Still necessary?
-    /**
-     * Initialize the refresh button
-     */
-    initRefreshButton() {
-
-        this.refreshButton.addEventListener('click', () => {
-            if(this.refreshedOnce && this.panelList.children.length === 1 && this.panelList.children[0].classList.contains('no-operations')){
-                this.addPopup('Nothing to refresh');
-                return;
-            }
-            this.refreshedOnce = true;
-            // Show refreshing state
-            this.refreshButton.classList.add('refreshing');
-
-            // Clear existing countdown and polling
-            this.stopPolling();
-
-            // Get current filter
-            const activeFilter = this.filterButtonsContainer.querySelector('.active');
-            const currentFilter = activeFilter ? activeFilter.dataset.filter : 'all';
-
-            // Fetch operations with current filter and force flag
-            this.fetchOperations({
-                initial: false,
-                force: true
-            }).then(() => {}).finally(() => {
-                // Remove refreshing state
-                this.refreshButton.classList.remove('refreshing');
-            });
-        });
-    }
-}
-
-// Initialize QueueManager on page load
-document.addEventListener('DOMContentLoaded', () => {
-    window.jvbQueue = new QueueManagerBackup();
-});
-
-// Theme switching functionality
-document.addEventListener('DOMContentLoaded', function() {
-    const themeSwitch = document.getElementById('theme-switch');
-
-    if (!themeSwitch) return;
-
-    // Initialize theme from localStorage or system preference
-    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
-    const storedTheme = localStorage.getItem('theme');
-
-    if (storedTheme) {
-        document.documentElement.classList.toggle('dark', storedTheme === 'dark');
-        themeSwitch.checked = storedTheme === 'dark';
-    } else {
-        document.documentElement.classList.toggle('dark', prefersDark.matches);
-        themeSwitch.checked = prefersDark.matches;
-    }
-
-    // Handle theme switch changes
-    themeSwitch.addEventListener('change', async function () {
-        const isDark = this.checked;
-        document.documentElement.classList.toggle('dark', isDark);
-        localStorage.setItem('theme', isDark ? 'dark' : 'light');
-
-        // If user is logged in, save preference
-        if (jvbSettings.currentUser !== null) {
-            try {
-                await fetch(`${jvbSettings.api}settings`, {
-                    method: 'POST',
-                    headers: {
-                        'Content-Type': 'application/json',
-                        'X-WP-Nonce': jvbSettings.nonce,
-                        'action_nonce': jvbSettings.dash,
-                    },
-                    body: JSON.stringify({
-                        dark_mode: isDark
-                    })
-                });
-            } catch (error) {
-                console.error('Failed to save theme preference:', error);
-            }
-        }
-
-        // Update label
-        const label = document.getElementById('theme-switch');
-        if (label) {
-            label.title = isDark ? 'Toggle Light Mode' : 'Toggle Dark Mode';
-        }
-    });
-
-    // Handle system theme changes
-    prefersDark.addEventListener('change', (e) => {
-        if (!localStorage.getItem('theme')) {
-            const isDark = e.matches;
-            document.documentElement.classList.toggle('dark', isDark);
-            themeSwitch.checked = isDark;
-        }
-    });
-});
-
-
diff --git a/assets/js/dash/TaxonomySelector.js b/assets/js/dash/TaxonomySelector.js
deleted file mode 100644
index 0f0f6cc..0000000
--- a/assets/js/dash/TaxonomySelector.js
+++ /dev/null
@@ -1,1151 +0,0 @@
-/**
- * Centralized Taxonomy Selector
- * Handles all taxonomy selection fields on the page from a single location
- */
-class TaxonomySelector {
-	constructor() {
-		this.a11y = window.jvbA11y;
-		// this.cache = window.jvbCache;
-		this.error = window.jvbError;
-		this.index = -1;
-
-		this.stores = new Map();
-
-		// Central field management
-		this.fields = new Map();           // fieldId -> field configuration
-		this.terms = new Map();            // taxonomy -> Map(termId -> term data)
-		this.selectedTerms = new Map();    // Current selection (cleared on modal close)
-
-		// Current modal context
-		this.activeField = null;
-		this.currentConfig = null;
-
-		// Modal state
-		this.page = 1;
-		this.hasMore = true;
-		this.isLoading = false;
-		this.searchQuery = '';
-		this.navigationPath = [];
-		this.currentParent = 0;
-		this.currentParentName = '';
-		this.fetchSpecificTerms = false;
-		this.disabled = false;
-
-		// Search debouncing
-		this.searchHandler = null;
-
-		// Prefetch system
-		this.prefetchCache = new Map();
-		this.prefetchQueue = [];
-		this.isPrefetching = false;
-
-		this.init();
-	}
-
-	/**
-	 * Initialize the selector
-	 */
-	init() {
-		this.initModal();
-		this.scanExistingFields();
-		this.initGlobalListeners();
-		this.initPrefetching();
-	}
-
-	/**
-	 * Scan page for existing taxonomy fields and register them
-	 */
-	scanExistingFields() {
-		const selectors = document.querySelectorAll('.field.taxonomy, .field.post');
-		selectors.forEach(selector => {
-			try {
-				this.registerField(selector);
-			} catch (error) {
-				this.error.log(error, {
-					component: 'TaxonomySelector',
-					action: 'scanExistingFields',
-					container: selector.dataset.name
-				});
-			}
-		});
-	}
-
-	/**
-	 * Register a taxonomy field
-	 */
-	registerField(field, options = {}) {
-		let input = field.querySelector('input[type=hidden]');
-		if (!input) {
-			return;
-		}
-
-		if (!('fieldId' in field.dataset)) {
-			field.dataset.fieldId = this.createFieldId(field);
-		}
-		let fieldId = field.dataset.fieldId;
-		let button = field.querySelector('button.taxonomy-toggle');
-
-		let config = {
-			id: fieldId,
-			input: input,
-			container: field,
-			taxonomy: button.dataset.taxonomy,
-			name: field.dataset.field,
-			singular: button.dataset.singular,
-			plural: button.dataset.plural,
-			maxSelection: parseInt(button.dataset.max) || 0,
-			canSearch: 'search' in button.dataset,
-			canCreate: 'creatable' in button.dataset,
-			isRequired: 'required' in button.dataset,
-			selectedTerms: new Set(input.value.split(',')),
-			toggle: button,
-			selectedContainer: field.querySelector('.selected-items'),
-			...options
-		};
-
-		this.fields.set(fieldId, config);
-
-		// Initialize display for any pre-selected values
-		this.initFieldDisplay(fieldId);
-
-		return fieldId;
-	}
-
-	/**
-	 * Create unique field ID
-	 */
-	createFieldId(field) {
-		let input = field.querySelector('input[type=hidden]');
-		this.index++;
-		return 'selector-'+this.index;
-
-	}
-
-	/**
-	 * Initialize display for a field with existing values
-	 */
-	initFieldDisplay(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const value = field.input.value.trim();
-		if (value !== '') {
-			const selectedIds = value.split(',')
-				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id));
-
-			if (selectedIds.length > 0) {
-				this.updateFieldDisplay(fieldId, selectedIds);
-			}
-		}
-	}
-
-	/**
-	 * Update field display with selected term IDs
-	 */
-	async updateFieldDisplay(fieldId, selectedIds) {
-		const field = this.fields.get(fieldId);
-		if (!field || selectedIds.length === 0) return;
-
-		// Check if we already have these terms cached
-		const taxonomy = field.taxonomy;
-		const needsFetch = [];
-		const cachedTerms = [];
-
-		selectedIds.forEach(termId => {
-			if (this.terms.has(taxonomy) && this.terms.get(taxonomy).has(termId)) {
-				const term = this.terms.get(taxonomy).get(termId);
-				cachedTerms.push(term);
-				field.selectedTerms.add(termId);
-			} else {
-				needsFetch.push(termId);
-			}
-		});
-
-		// Display cached terms immediately
-		cachedTerms.forEach(term => {
-			this.addTermToDisplay(fieldId, term.id, term.name, term.path);
-		});
-
-		// Fetch missing terms if needed
-		if (needsFetch.length > 0) {
-			try {
-				this.fetchSpecificTerms = needsFetch.join(',');
-				const fetchedTerms = await this.fetchTerms(fieldId);
-
-				fetchedTerms.forEach(term => {
-					field.selectedTerms.add(term.id);
-					this.addTermToDisplay(fieldId, term.id, term.name, term.path);
-				});
-			} catch (error) {
-				console.error('Failed to fetch missing terms:', error);
-			} finally {
-				this.fetchSpecificTerms = false;
-			}
-		}
-	}
-
-	/**
-	 * Initialize modal elements
-	 */
-	initModal() {
-		this.modalID = 'dialog#jvb-selector';
-		this.modal = document.querySelector(this.modalID);
-
-		if (!this.modal) {
-			console.warn('Taxonomy selector modal not found');
-			return;
-		}
-
-		this.initModalElements();
-
-		// Initialize modal instance
-		this.modalInstance = new window.jvbModal(this.modal, {
-			handleForm: false,
-			save: null,
-			open: null,
-			onOpen: () => this.openModal(),
-			onClose: () => this.closeModal()
-		});
-	}
-
-	/**
-	 * Initialize modal element references
-	 */
-	initModalElements() {
-		this.elements = {
-			searchInput: this.modal.querySelector('input[type=search]'),
-			termsList: this.modal.querySelector('.items-container'),
-			termsWrap: this.modal.querySelector('.items-wrap'),
-			breadcrumbs: this.modal.querySelector('nav.term-navigation'),
-			loading: this.modal.querySelector('.loading'),
-			loadingText: this.modal.querySelector('.loading span'),
-			clearSearch: this.modal.querySelector('.clear-search'),
-			selectedTerms: this.modal.querySelector('.selected-items'),
-			backButton: this.modal.querySelector('.back-to-parent'),
-			sentinel: this.modal.querySelector('.scroll-sentinel'),
-			modalTitle: this.modal.querySelector('#modal-title'),
-			modalContent: this.modal.querySelector('.modal-content'),
-			createNewSection: this.modal.querySelector('.create-new-term'),
-			favouriteTerms: this.modal.querySelector('.favourite-terms'),
-		};
-
-		// Initialize intersection observer for infinite scroll
-		this.observer = new IntersectionObserver((entries) => {
-			entries.forEach(entry => {
-				if (entry.isIntersecting && !this.isLoading && this.hasMore) {
-					this.fetchTerms(this.activeField);
-				}
-			});
-		}, {
-			root: this.elements.termsWrap,
-			threshold: 0.5
-		});
-	}
-
-	/**
-	 * Set up global event delegation
-	 */
-	initGlobalListeners() {
-		document.addEventListener('click', this.handleClick.bind(this));
-		document.addEventListener('change', this.handleChange.bind(this));
-	}
-
-	/**
-	 * Handle global click events
-	 */
-	handleClick(e) {
-		// Handle taxonomy toggle buttons
-		const toggleButton = window.targetCheck(e, '.taxonomy-toggle');
-		if (toggleButton) {
-			e.preventDefault();
-			this.handleToggleClick(toggleButton);
-			return;
-		}
-
-		// Handle remove selected term buttons
-		const removeButton = window.targetCheck(e, 'button.remove-item');
-		if (removeButton && e.target.closest('.jvb-selector')) {
-			const fieldId = this.getFieldId(removeButton);
-			const termId = removeButton.closest('.selected-item').dataset.id;
-			this.removeSelectedTerm(fieldId, termId);
-			return;
-		}
-
-		// Handle modal close button
-		if (e.target.matches('.modal-close')) {
-			if (this.modalInstance) {
-				this.modalInstance.handleClose();
-			}
-			return;
-		}
-
-		// Handle clicks within the modal
-		if (this.modal && this.modal.contains(e.target)) {
-			this.handleModalClick(e);
-		}
-	}
-
-	/**
-	 * Handle global change events
-	 */
-	handleChange(e) {
-		// Handle hidden input changes for taxonomy fields
-		const taxonomyField = window.targetCheck(e, '.taxonomy.field, .post.field');
-		if (taxonomyField && e.target.type === 'hidden') {
-			const fieldId = this.getFieldId(e.target);
-			this.updateFieldFromInput(fieldId);
-			return;
-		}
-
-		// Handle modal changes
-		if (this.modal && this.modal.contains(e.target)) {
-			this.handleModalChange(e);
-		}
-	}
-
-	/**
-	 * Handle toggle button click
-	 */
-	handleToggleClick(toggle) {
-		try {
-			const fieldId = this.getFieldId(toggle);
-			const field = this.fields.get(fieldId);
-
-			if (!field) {
-				console.error('Field not found for toggle:', fieldId);
-				return;
-			}
-
-			this.setActiveField(fieldId);
-			this.modalInstance.handleOpen();
-
-		} catch (error) {
-			console.error('Error handling toggle click:', error);
-			this.error?.handleError(error, {
-				component: 'TaxonomySelector',
-				action: 'handleToggleClick'
-			});
-		}
-	}
-
-	/**
-	 * Set the active field for modal operations
-	 */
-	setActiveField(fieldId) {
-		this.activeField = fieldId;
-		this.currentConfig = this.fields.get(fieldId);
-
-		// Clear modal selection state
-		this.selectedTerms.clear();
-
-		// Copy field's current selections to modal state
-		if (this.currentConfig.selectedTerms) {
-			this.currentConfig.selectedTerms.forEach(termId => {
-				// Find term data to populate modal selection
-				const taxonomy = this.currentConfig.taxonomy;
-				if (this.terms.has(taxonomy) && this.terms.get(taxonomy).has(termId)) {
-					const term = this.terms.get(taxonomy).get(termId);
-					this.selectedTerms.set(termId, {
-						id: termId,
-						name: term.name,
-						path: term.path
-					});
-				}
-			});
-		}
-	}
-
-	/**
-	 * Handle clicks within modal
-	 */
-	handleModalClick(e) {
-		if (window.targetCheck(e, '.remove-item')) {
-			let selectedItem = window.targetCheck(e, '.selected-item');
-			if (selectedItem) {
-				this.removeSelectedTermFromModal(selectedItem.dataset.id);
-			}
-		} else if (window.targetCheck(e, '.back-to-parent')) {
-			this.toParent();
-		} else if (window.targetCheck(e, '.toggle-children')) {
-			let termItem = e.target.closest('li');
-			this.toChild(
-				parseInt(termItem.dataset.id),
-				termItem.querySelector('.term-name').textContent
-			);
-		} else if (window.targetCheck(e, '.path-level')) {
-			let pathLevel = window.targetCheck(e, '.path-level');
-			if (pathLevel.textContent !== this.currentParentName) {
-				this.navigateToPath(pathLevel);
-			}
-		}
-	}
-
-	/**
-	 * Handle changes within modal (checkboxes)
-	 */
-	handleModalChange(e) {
-		if (window.targetCheck(e, this.modalID) && e.target.type === 'checkbox') {
-			e.preventDefault();
-			e.stopPropagation();
-
-			const termId = parseInt(e.target.closest('li').dataset.id);
-			const label = e.target.closest('li').querySelector('label');
-
-			if (e.target.checked) {
-				this.addSelectedTermToModal(termId, label.title, label.dataset.path);
-			} else {
-				this.removeSelectedTermFromModal(termId);
-			}
-		}
-	}
-
-	/**
-	 * Open modal and initialize
-	 */
-	openModal() {
-		if (!this.activeField || !this.currentConfig) {
-			console.error('No active field set for modal');
-			return;
-		}
-
-		this.resetModalState();
-		this.updateModalForTaxonomy();
-		this.observer.observe(this.elements.sentinel);
-
-		// Set up search if enabled
-		if (this.currentConfig.canSearch) {
-			this.elements.searchInput.focus();
-			this.searchHandler = window.debounce(() => this.handleSearch(), 300);
-			this.elements.searchInput.addEventListener('input', this.searchHandler);
-		}
-
-		// Initialize creator if available
-		if (this.currentConfig.canCreate && 'jvbTaxCreator' in window) {
-			this.creator = new window.jvbTaxCreator(this);
-		}
-
-		// Display current selections
-		this.updateModalSelections();
-
-		// Fetch terms
-		this.fetchTerms(this.activeField);
-	}
-
-	/**
-	 * Close modal and save selections
-	 */
-	closeModal() {
-		this.observer.unobserve(this.elements.sentinel);
-		window.removeChildren(this.elements.termsList);
-
-		// Save selections to active field
-		if (this.activeField) {
-			this.saveSelectionsToField(this.activeField);
-		}
-
-		// Cleanup
-		if (this.currentConfig?.canSearch && this.searchHandler) {
-			this.elements.searchInput.removeEventListener('input', this.searchHandler);
-		}
-
-		if (this.creator) {
-			delete this.creator;
-		}
-
-		this.activeField = null;
-		this.currentConfig = null;
-	}
-
-	/**
-	 * Reset modal state
-	 */
-	resetModalState() {
-		this.page = 1;
-		this.hasMore = true;
-		this.isLoading = false;
-		this.searchQuery = '';
-		this.navigationPath = [];
-		this.currentParent = 0;
-		this.currentParentName = '';
-		this.disabled = false;
-
-		window.removeChildren(this.elements.termsList);
-		window.removeChildren(this.elements.selectedTerms);
-		this.elements.searchInput.value = '';
-	}
-
-	/**
-	 * Update modal content for current taxonomy
-	 */
-	updateModalForTaxonomy() {
-		if (!this.currentConfig) return;
-
-		this.elements.modalTitle.textContent = `Select ${this.currentConfig.plural}`;
-
-		const searchWrapper = this.modal.querySelector('.search-wrapper');
-		if (searchWrapper) {
-			searchWrapper.style.display = this.currentConfig.canSearch ? 'block' : 'none';
-		}
-
-		if (this.elements.createNewSection) {
-			this.elements.createNewSection.style.display = this.currentConfig.canCreate ? 'block' : 'none';
-			this.elements.createNewSection.hidden = !this.currentConfig.canCreate;
-		}
-
-		const openMessage = `Opened ${this.currentConfig.singular} selection. Choose from checkboxes or search to filter results.`;
-		this.a11y?.announce(openMessage);
-	}
-
-	/**
-	 * Update modal selections display
-	 */
-	updateModalSelections() {
-		window.removeChildren(this.elements.selectedTerms);
-
-		this.selectedTerms.forEach((termData, id) => {
-			this.addTermToModalDisplay(id, termData.name, termData.path);
-		});
-
-		this.checkSelectionLimits();
-	}
-
-	/**
-	 * Add selected term to modal
-	 */
-	addSelectedTermToModal(id, name, path) {
-		this.selectedTerms.set(id, {
-			id: id,
-			name: name,
-			path: path
-		});
-
-		this.addTermToModalDisplay(id, name, path);
-		this.checkSelectionLimits();
-
-		// Check the corresponding checkbox
-		const checkbox = this.elements.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) {
-			checkbox.checked = true;
-		}
-	}
-
-	/**
-	 * Remove selected term from modal
-	 */
-	removeSelectedTermFromModal(id) {
-		this.selectedTerms.delete(id);
-
-		// Remove from modal display
-		const selectedItem = this.elements.selectedTerms.querySelector(`[data-id="${id}"]`);
-		if (selectedItem) {
-			selectedItem.remove();
-		}
-
-		// Uncheck the corresponding checkbox
-		const checkbox = this.elements.termsList.querySelector(`input[value="${id}"]`);
-		if (checkbox) {
-			checkbox.checked = false;
-		}
-
-		this.checkSelectionLimits();
-	}
-
-	/**
-	 * Add term to modal display
-	 */
-	addTermToModalDisplay(id, name, path) {
-		const item = window.getTemplate('selectedTerm').cloneNode(true);
-		item.dataset.id = id;
-		item.dataset.path = path;
-		item.dataset.name = name;
-		item.dataset.taxonomy = this.currentConfig.taxonomy;
-		item.querySelector('span').textContent = path;
-		item.querySelector('button').title = `Remove ${name}`;
-
-		this.elements.selectedTerms.appendChild(item);
-	}
-
-	/**
-	 * Check selection limits and disable/enable checkboxes
-	 */
-	checkSelectionLimits() {
-		if (!this.currentConfig || this.currentConfig.maxSelection === 0) {
-			return;
-		}
-
-		this.disabled = this.selectedTerms.size >= this.currentConfig.maxSelection;
-		this.setCheckboxes(this.disabled);
-	}
-
-	/**
-	 * Set checkbox disabled state
-	 */
-	setCheckboxes(disabled) {
-		this.elements.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
-			if (!checkbox.checked) {
-				checkbox.disabled = disabled;
-			}
-		});
-	}
-
-	/**
-	 * Save modal selections to field
-	 */
-	saveSelectionsToField(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Clear current field selections
-		field.selectedTerms.clear();
-		window.removeChildren(field.selectedContainer);
-
-		// Add modal selections to field
-		this.selectedTerms.forEach((termData, id) => {
-			field.selectedTerms.add(id);
-			this.addTermToDisplay(fieldId, id, termData.name, termData.path);
-		});
-
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	/**
-	 * Remove selected term from field
-	 */
-	removeSelectedTerm(fieldId, termId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const id = parseInt(termId);
-		field.selectedTerms.delete(id);
-
-		// Remove from display
-		const selectedItem = field.selectedContainer.querySelector(`[data-id="${id}"]`);
-		if (selectedItem) {
-			selectedItem.remove();
-		}
-
-		// Update hidden input
-		const selectedIds = Array.from(field.selectedTerms);
-		field.input.value = selectedIds.join(',');
-		field.input.dispatchEvent(new Event('change', { bubbles: true }));
-	}
-
-	/**
-	 * Add term to field display
-	 */
-	addTermToDisplay(fieldId, id, name, path) {
-		const field = this.fields.get(fieldId);
-		if (!field || field.selectedContainer.querySelector(`[data-id="${id}"]`)) {
-			return; // Already displayed
-		}
-
-		const item = window.getTemplate('selectedTerm').cloneNode(true);
-		item.dataset.id = id;
-		item.dataset.path = path;
-		item.dataset.name = name;
-		item.dataset.taxonomy = field.taxonomy;
-		item.querySelector('span').textContent = path;
-		item.querySelector('button').title = `Remove ${name}`;
-
-		field.selectedContainer.appendChild(item);
-	}
-
-	/**
-	 * Update field from hidden input value
-	 */
-	updateFieldFromInput(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const value = field.input.value.trim();
-		field.selectedTerms.clear();
-		window.removeChildren(field.selectedContainer);
-
-		if (value !== '') {
-			const selectedIds = value.split(',')
-				.map(id => parseInt(id.trim()))
-				.filter(id => !isNaN(id));
-
-			this.updateFieldDisplay(fieldId, selectedIds);
-		}
-	}
-
-	/**
-	 * Handle search input
-	 */
-	handleSearch() {
-		let query = this.elements.searchInput.value.trim();
-
-		if (query !== this.searchQuery) {
-			this.searchQuery = query;
-			this.page = 1;
-			this.hasMore = true;
-			window.removeChildren(this.elements.termsList);
-
-			if (query.length >= 2 || query.length === 0) {
-				this.fetchTerms(this.activeField, false, true);
-			} else {
-				this.hideLoading();
-				this.showEmptyState('No Results. \nEnter at least 2 characters to search.');
-			}
-		}
-	}
-
-	/**
-	 * Navigate to parent term
-	 */
-	toParent() {
-		this.navigationPath.pop();
-		let parent = this.navigationPath[this.navigationPath.length - 1];
-		this.currentParent = parent ? parent.id : 0;
-		this.currentParentName = parent ? parent.name : '';
-		window.removeChildren(this.elements.termsList);
-		this.page = 1;
-		this.hasMore = true;
-		this.fetchTerms(this.activeField);
-	}
-
-	/**
-	 * Navigate to child term
-	 */
-	toChild(id, name) {
-		this.navigationPath.push({ id: id, name: name });
-		this.currentParent = id;
-		this.currentParentName = name;
-		window.removeChildren(this.elements.termsList);
-		this.page = 1;
-		this.hasMore = true;
-		this.fetchTerms(this.activeField);
-	}
-
-	/**
-	 * Navigate to specific path level
-	 */
-	navigateToPath(pathLevel) {
-		window.removeChildren(this.elements.termsList);
-		let level = parseInt(pathLevel.dataset.level);
-		this.navigationPath = this.navigationPath.slice(0, level + 1);
-		this.currentParent = parseInt(pathLevel.dataset.id);
-		this.currentParentName = pathLevel.textContent;
-		this.page = 1;
-		this.hasMore = true;
-		this.fetchTerms(this.activeField);
-	}
-
-	/**
-	 * Fetch terms for the active field
-	 */
-	async fetchTerms(fieldId, forceRefresh = false, isSearch = false) {
-
-		if (this.isLoading || !fieldId) return;
-
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		if (!this.stores.has(field.taxonomy)) {
-			this.stores.set(field.taxonomy, new window.jvbStore({
-				name: field.taxonomy,
-				endpoint: 'terms',
-				filters: {
-					taxonomy: field.taxonomy,
-					page: 1,
-					search: '',
-					parent: 0
-				}
-			}));
-		}
-
-		let store = this.stores.get(field.taxonomy);
-		store.fetch();
-
-		return;
-
-		try {
-			this.showLoading();
-
-			const requestUrl = `${jvbSettings.api}terms?${this.buildRequest(field.taxonomy)}`;
-
-			const response = await this.cache.fetchWithCache(requestUrl, {
-				method: 'GET',
-				headers: {
-					'Content-Type': 'application/json',
-					'X-WP-Nonce': jvbSettings.nonce
-				}
-			}, {
-				content: field.taxonomy,
-				forceRefresh: forceRefresh
-			});
-
-			// Handle specific term fetching
-			if (this.fetchSpecificTerms) {
-				this.fetchSpecificTerms = false;
-				return response.terms || [];
-			}
-
-			if (response && response.terms && response.terms.length !== 0) {
-				this.hasMore = response.pagination?.has_more || false;
-				this.renderTerms(response.terms, this.page > 1, isSearch);
-
-				if (this.hasMore) {
-					this.nextPage();
-				}
-			} else {
-				if (this.page === 1) {
-					this.showEmptyState();
-				}
-				this.hasMore = false;
-			}
-
-			return response.terms || [];
-		} catch (error) {
-			this.handleError(error);
-			return [];
-		} finally {
-			this.hideLoading();
-		}
-	}
-
-	/**
-	 * Build request parameters
-	 */
-	buildRequest(taxonomy = null) {
-		let params = new URLSearchParams({
-			taxonomy: taxonomy || this.currentConfig?.taxonomy || '',
-			parent: this.currentParent,
-			search: this.searchQuery,
-			page: this.page
-		});
-
-		if (this.fetchSpecificTerms) {
-			params.append('termIDs', this.fetchSpecificTerms);
-		}
-
-		return params.toString();
-	}
-
-	/**
-	 * Render terms list
-	 */
-	renderTerms(terms, append = false, showPath = false) {
-
-
-		if (!append) {
-			window.removeChildren(this.elements.termsList);
-		}
-
-		if (terms.length === 0) {
-			if (!append) {
-				this.showEmptyState();
-			}
-			this.a11y?.announce(0, append);
-			return;
-		}
-
-		this.updateBreadcrumbs();
-
-		for (const [index, term] of Object.entries(terms)) {
-			if (!term) continue;
-
-			// Store term in global cache
-			const taxonomy = this.currentConfig.taxonomy;
-			if (!this.terms.has(taxonomy)) {
-				this.terms.set(taxonomy, new Map());
-			}
-			this.terms.get(taxonomy).set(term.id, term);
-
-			this.createTermElement({
-				id: parseInt(term.id),
-				name: term.name,
-				hasChildren: term.hasChildren,
-				path: term.path || null,
-				show: showPath
-			});
-		}
-
-		this.a11y?.announce(terms.length, append);
-	}
-
-	/**
-	 * Create individual term element
-	 */
-	createTermElement(termData) {
-		if (!termData || !termData.name) return;
-
-		const listItem = window.getTemplate('termListItem');
-		listItem.dataset.id = termData.id;
-
-		const isSelected = this.selectedTerms.has(termData.id);
-		const checkbox = listItem.querySelector('input');
-		const label = listItem.querySelector('label');
-		const nameSpan = listItem.querySelector('span, .term-name');
-
-		if (checkbox && label && nameSpan) {
-			checkbox.id = `${this.currentConfig.container.id}${termData.id}`;
-			checkbox.name = `${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`;
-			checkbox.value = termData.id;
-			checkbox.disabled = !isSelected && this.disabled;
-			checkbox.checked = isSelected;
-
-			label.htmlFor = checkbox.id;
-			label.title = termData.path || termData.name;
-			label.dataset.path = termData.path;
-
-			nameSpan.textContent = termData.show ? termData.path : termData.name;
-		}
-
-		if (termData.hasChildren) {
-			const childrenToggle = window.getTemplate ?
-				window.getTemplate('termChildrenToggle') :
-				this.createChildrenToggle();
-
-			if (childrenToggle) {
-				childrenToggle.ariaLabel = `View sub-terms of ${termData.name}`;
-				listItem.appendChild(childrenToggle);
-			}
-		}
-
-		this.elements.termsList.appendChild(listItem);
-	}
-
-	/**
-	 * Create children toggle button
-	 */
-	createChildrenToggle() {
-		const button = document.createElement('button');
-		button.type = 'button';
-		button.className = 'toggle-children';
-		button.innerHTML = '→';
-		return button;
-	}
-
-	/**
-	 * Update breadcrumb navigation
-	 */
-	updateBreadcrumbs() {
-		window.removeChildren(this.elements.breadcrumbs);
-		this.elements.breadcrumbs.appendChild(this.elements.backButton);
-		this.elements.backButton.hidden = this.currentParent === 0;
-
-		this.navigationPath.forEach((pathItem, index) => {
-			const breadcrumb = window.getTemplate ?
-				window.getTemplate('termBreadcrumb') :
-				this.createBreadcrumbElement();
-
-			breadcrumb.dataset.level = index;
-			breadcrumb.dataset.id = pathItem.id;
-			breadcrumb.title = pathItem.path || pathItem.name;
-			breadcrumb.textContent = pathItem.name;
-
-			this.elements.breadcrumbs.appendChild(breadcrumb);
-		});
-	}
-
-	/**
-	 * Create breadcrumb element
-	 */
-	createBreadcrumbElement() {
-		const button = document.createElement('button');
-		button.type = 'button';
-		button.className = 'path-level';
-		return button;
-	}
-
-	/**
-	 * Show loading state
-	 */
-	showLoading() {
-		this.isLoading = true;
-		this.elements.loading.hidden = false;
-		this.modal.classList.add('loading');
-
-		let message = this.searchQuery !== '' ?
-			`searching for "${this.searchQuery}" items` :
-			this.currentParentName === '' ?
-				'loading items' :
-				`loading ${this.currentParentName} items`;
-
-		if (window.typeLoop) {
-			this.stopTyping = window.typeLoop(this.elements.loadingText, message);
-		} else {
-			this.elements.loadingText.textContent = message;
-		}
-	}
-
-	/**
-	 * Hide loading state
-	 */
-	hideLoading() {
-		this.isLoading = false;
-		this.elements.loading.hidden = true;
-		this.modal.classList.remove('loading');
-
-		if (this.stopTyping) {
-			this.stopTyping();
-		}
-	}
-
-	/**
-	 * Show empty state message
-	 */
-	showEmptyState(message = '') {
-		const emptyElement = window.getTemplate('noResults');
-
-		if (message && emptyElement.querySelector('span')) {
-			emptyElement.querySelector('span').textContent = message;
-		}
-
-		this.elements.termsList.appendChild(emptyElement);
-	}
-
-	/**
-	 * Move to next page
-	 */
-	nextPage() {
-		if (this.hasMore) {
-			this.page++;
-		}
-	}
-
-	/**
-	 * Handle API errors
-	 */
-	handleError(error) {
-		console.error('Taxonomy selector error:', error);
-
-		if (this.error && this.error.log) {
-			return this.error.log(error, {
-				component: 'TaxonomySelector',
-				action: 'fetchTerms'
-			}, () => this.fetchTerms(this.activeField));
-		} else {
-			this.showEmptyState('Error loading terms. Please try again.');
-		}
-	}
-
-	/**
-	 * Initialize prefetching system
-	 */
-	initPrefetching() {
-		if (document.readyState === 'complete') {
-			this.startPrefetching();
-		} else {
-			window.addEventListener('load', () => {
-				this.scheduleIdlePrefetch();
-			});
-		}
-	}
-
-	/**
-	 * Schedule prefetching during browser idle time
-	 */
-	scheduleIdlePrefetch() {
-		if ('requestIdleCallback' in window) {
-			requestIdleCallback((deadline) => {
-				this.startPrefetching(deadline);
-			}, { timeout: 5000 });
-		} else {
-			setTimeout(() => this.startPrefetching(), 1000);
-		}
-	}
-
-	/**
-	 * Start the prefetching process
-	 */
-	startPrefetching() {
-		const visibleTaxonomies = new Set();
-		document.querySelectorAll('button.taxonomy-toggle').forEach(button => {
-			visibleTaxonomies.add(button.dataset.taxonomy);
-		});
-
-		visibleTaxonomies.forEach(taxonomy => {
-			this.prefetchTaxonomyTerms(taxonomy);
-		});
-	}
-
-	/**
-	 * Prefetch terms for a specific taxonomy
-	 */
-	async prefetchTaxonomyTerms(taxonomy) {
-		try {
-			const response = await this.cache.fetchWithCache(
-				`${jvbSettings.api}terms?taxonomy=${taxonomy}&page=1`,
-				{
-					method: 'GET',
-					headers: {
-						'Content-Type': 'application/json',
-						'X-WP-Nonce': jvbSettings.nonce
-					}
-				},
-				{ content: taxonomy }
-			);
-
-			if (response.terms) {
-				if (!this.terms.has(taxonomy)) {
-					this.terms.set(taxonomy, new Map());
-				}
-				response.terms.forEach(term => {
-					this.terms.get(taxonomy).set(term.id, term);
-				});
-			}
-		} catch (error) {
-			console.warn(`Failed to prefetch terms for ${taxonomy}:`, error);
-		}
-	}
-
-	/**
-	 * Get field ID from any element within the field
-	 */
-	getFieldId(element) {
-		// Try multiple approaches to find the field ID
-		if (element.dataset.fieldId) {
-			return element.dataset.fieldId;
-		}
-
-		const fieldContainer = element.closest('[data-field-id]');
-		if (fieldContainer) {
-			return fieldContainer.dataset.fieldId ||
-				fieldContainer.dataset.field ||
-				fieldContainer.dataset.name;
-		}
-
-		return null;
-	}
-
-	/**
-	 * Utility: delay for prefetching
-	 */
-	delay(ms) {
-		return new Promise(resolve => setTimeout(resolve, ms));
-	}
-
-	/**
-	 * Clean up
-	 */
-	destroy() {
-		// Remove event listeners
-		document.removeEventListener('click', this.handleClick);
-		document.removeEventListener('change', this.handleChange);
-
-		// Clear intervals and cleanup
-		this.observer?.disconnect();
-
-		// Clear all maps
-		this.fields.clear();
-		this.terms.clear();
-		this.selectedTerms.clear();
-	}
-}
-
-/**
- * Initialize singleton
- */
-document.addEventListener('DOMContentLoaded', function() {
-	if (!window.jvbSelector) {
-		window.jvbSelector = new TaxonomySelector();
-	}
-});
-
diff --git a/assets/js/dash/TaxonomySelectorOld.js b/assets/js/dash/TaxonomySelectorOld.js
deleted file mode 100644
index e6e1491..0000000
--- a/assets/js/dash/TaxonomySelectorOld.js
+++ /dev/null
@@ -1,928 +0,0 @@
-
-class TaxonomySelectorOld
-{
-    constructor(container, options = {}) {
-        this.initialized = false;
-        this.container = container;
-        this.config = this.initializeConfig(options);
-        this.a11y = window.jvbA11y;
-        this.cache = window.jvbCache;
-		this.loading = window.jvbLoading;
-
-        this.taxonomy = container.dataset.taxonomy;
-        this.selectedItems = options.selected ?? {};
-
-        this.commonTerms = new Map();
-        this.pendingTerms = new Map();
-        this.page = 1;
-        this.loading = false;
-        this.hasMore = true;
-        this.searchQuery = '';
-        this.createNew = false;
-        this.currentTerms = new Map();
-        this.initialState = new Map();
-
-        this.init();
-
-        this.navigationPath = [];
-        this.currentParent = 0;
-        this.currentParentName = '';
-        this.currentPath = '';
-        this.limitWasReached = false;
-
-        if (this.config.selected) {
-            if (typeof this.config.selected === 'object' && !window.isEmptyObject(this.config.selected)) {
-                this.config.selected.forEach(item => {
-                    this.selectedItems[item.id] = item.name;
-                });
-            } else {
-                this.selectedItems = this.config.selected;
-            }
-
-            this.updateSelected();
-        }
-
-        if (this.selectedItems) {
-            for (const [id, name] of Object.entries(this.selectedItems)) {
-                this.initialState.set(id, name);
-            }
-        }
-
-        this.initialized = true;
-        this.boundTermListener = this.initializeToggleButtons.bind(this);
-    }
-
-    initializeConfig(options) {
-        const defaultConfig = this.container ?
-            JSON.parse(this.container.dataset.config || '{}') : {};
-
-        return {
-            ...defaultConfig,
-            ...options,
-            selectedItems: options.selected ?? {},
-            maxSelections: options.maxSelections || defaultConfig.maxSelections || 0,
-            hierarchical: options.hierarchical || defaultConfig.hierarchical || false,
-            base: options.base || defaultConfig.base || '',
-            onSuccess: options.onSuccess || defaultConfig.onSuccess || null,
-            onClose: options.onClose || defaultConfig.onClose || null,
-            onCreate: options.onCreate || defaultConfig.onCreate || null,
-            values: options.values || defaultConfig.values || null,
-        };
-    }
-
-    init() {
-        this.modal = this.container.querySelector('dialog');
-        this.modalSelected = this.container.querySelector('.selected-items');
-        this.searchInput = this.modal.querySelector('input[type=search]');
-        this.itemsContainer = this.modal.querySelector('.items-container');
-        this.itemsWrap = this.modal.querySelector('.items-wrap');
-        this.selectedContainer = this.container.querySelector('.selected-items');
-        this.breadcrumbNav = this.modal.querySelector('nav.term-navigation');
-        this.loadingText = this.modal.querySelector('p.loading');
-        this.noResultsText = this.modal.querySelector('p.no-results') || this.createNoResultsElement();
-        this.clearSearchButton = this.modal.querySelector('.clear-search');
-        this.modalSelectedItems = this.modal.querySelector('.selected-items .selected');
-
-        // Initialize common terms from config
-        if (this.config.common) {
-            Object.entries(this.config.common).forEach(([id, term]) => {
-                this.commonTerms.set(id, term);
-            });
-        }
-
-        this.initializeEventListeners();
-        this.initializeInfiniteScroll();
-        this.initializeTermCreation();
-        this.updateModalSelected();
-    }
-
-    updateModalSelected() {
-        if (!this.modalSelectedItems) return;
-
-        // Clear existing selected items
-        removeChildren(this.modalSelectedItems);
-
-        for (const [id, term] of Object.entries(this.selectedItems)) {
-            const itemDiv = window.getTemplate('selectedTerm');
-            itemDiv.dataset.id = id;
-            let name = itemDiv.querySelector('span');
-            let button = itemDiv.querySelector('button');
-            [name.textContent, button.ariaLabel] =
-                [escapeHtml(term), `Remove ${escapeHtml(term)}`];
-
-            this.modalSelectedItems.appendChild(itemDiv);
-        }
-
-        // Add event listeners for removal
-        this.modalSelectedItems.addEventListener('click', e => {
-            const removeBtn = e.target.closest('.remove-item');
-            if (!removeBtn) return;
-
-            const item = removeBtn.closest('.selected-item');
-            if (item) {
-                this.removeItem(item.dataset.id);
-            }
-        });
-    }
-
-    createNoResultsElement() {
-        const noResults = window.getTemplate('noResults');
-        noResults.className = 'no-results';
-        noResults.hidden = true;
-        this.itemsWrap.appendChild(noResults);
-        return noResults;
-    }
-
-    buildParams() {
-        let params = new URLSearchParams({
-            taxonomy: this.taxonomy,
-            parent: this.currentParent || 0,
-            search: this.searchQuery || '',
-            per_page: 20,
-            page: this.page,
-        });
-        if (this.config.feed) {
-            if (window.feedBlock && window.feedBlock.config && window.feedBlock.config.context) {
-                params.append('main_context', JSON.stringify({
-                    context: window.feedBlock.config.context,
-                    id: window.feedBlock.config.source
-                }));
-                params.append('content', window.feedBlock.filters.content);
-            }
-        }
-        return params;
-    }
-
-    showLoading() {
-        if (this.loading) return;
-        this.loading = true;
-        this.loadingText.hidden = false;
-        this.noResultsText.hidden = true;
-        this.modal.classList.add('loading');
-    }
-    hideLoading()
-    {
-        this.loading = false;
-        this.loadingText.hidden = true;
-        this.modal.classList.remove('loading');
-    }
-    async fetchTerms(forceRefresh = false) {
-        try {
-            const params = this.buildParams();
-
-            const data =  await this.cache.fetchWithCache(
-                `${jvbSettings.api}terms?` + params.toString(),
-                {
-                    method: 'GET',
-                    headers: {
-                        'Content-Type': 'application/json',
-                        'X-WP-Nonce': jvbSettings.nonce
-                    }
-                },
-                {
-                    content: this.taxonomy,
-                    forceRefresh: forceRefresh
-                }
-            );
-            return data;
-        } catch (e) {
-            console.error('Error fetching terms:', error);
-            this.noResultsText.hidden = false;
-            this.noResultsText.textContent = 'Error loading terms. Please try again.';
-            throw error;
-        }
-
-    }
-    async fetchAndRenderTerms(forceRefresh = false) {
-        if (this.loading || (!this.hasMore && !forceRefresh)) return;
-
-        try {
-			this.showLoading();
-			return;
-            const response = await this.fetchTerms(forceRefresh);
-
-            // Check if we have results
-            const hasResults = response.terms && Object.keys(response.terms).length > 0;
-
-            await this.renderTerms(response.terms, this.page === 1);
-
-            // Show no results message if needed
-            this.noResultsText.hidden = hasResults || !this.searchQuery;
-
-            // Update pagination info if available
-            const paginationInfo = this.modal.querySelector('.pagination-info');
-            if (paginationInfo && response.pagination) {
-                paginationInfo.textContent = `Showing ${Object.keys(response.terms).length} of ${response.pagination.total_terms} items`;
-            }
-
-            this.cache.setItem(this.taxonomy + 'List', response.terms, this.taxonomy);
-
-            this.page++;
-            this.hasMore = response.pagination.has_more;
-
-            return true;
-        } catch (error) {
-            console.error('Error fetching terms:', error);
-            this.noResultsText.hidden = false;
-            this.noResultsText.textContent = 'Error loading terms. Please try again.';
-            throw error;
-        } finally {
-			this.hideLoading();
-        }
-    }
-
-    async createTerm(name, parent = 0)
-    {
-        let loadingMessage = this.modal.querySelector('.loading-message.create-term');
-
-        let text = loadingMessage.querySelector('span');
-        const form = this.createNew.querySelector('form');
-        const submitButton = form.querySelector('button[type="submit"]');
-        try {
-            loadingMessage.hidden = false;
-            window.typeText(text, 'Checking term...');
-
-            this.searchQuery = name;
-            const check = await this.fetchTerms();
-
-            console.log(check);
-            if (check.terms.length > 0) {
-                this.showTermSuggestions(check.terms);
-                return false;
-            }
-
-            //Term doesn't already exist, let's continue
-            const response = await fetch(`${jvbSettings.api}terms`, {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                    'X-WP-Nonce': jvbSettings.nonce
-                },
-                body: JSON.stringify({
-                    taxonomy: this.taxonomy,
-                    name: name,
-                    parent: parent
-                })
-            })
-
-            if (!response.ok) {
-                throw new Error(`Server error: ${response.status}`);
-            }
-
-            const result = await response.json();
-
-            if (result.success) {
-                form.reset();
-                this.m.hasChanges = false;
-                this.m.handleClose();
-                if (this.config.onCreate) {
-                    this.config.onCreate(result);
-                }
-                window.addNotification('SUCCESS\n\nWe\'ve put your suggestion to the masses (well, verified artists). Upon approval, '+ name +' will be available for use.')
-            }
-
-            return result;
-        } catch (error) {
-            console.error('Error creating term:', error);
-            throw error;
-        } finally {
-            submitButton.disabled = false;
-            loadingMessage.hidden = true;
-        }
-    }
-
-    // Helper method to show term suggestions when similar terms exist
-    showTermSuggestions(suggestions) {
-        const suggestionContainer = this.createNew.querySelector('.term-suggestions') ||
-            this.createSuggestionContainer();
-
-        // Clear existing suggestions
-        removeChildren(suggestionContainer);
-
-        // Add heading
-        const heading = document.createElement('h4');
-        heading.textContent = 'Similar terms already exist:';
-        suggestionContainer.appendChild(heading);
-
-        // Create list of suggestions
-        const list = document.createElement('ul');
-        list.className = 'term-suggestion-list';
-
-        suggestions.forEach(term => {
-            const item = document.createElement('li');
-
-            // Create term path display if available
-            let termDisplay = term.name;
-            if (term.path) {
-                termDisplay = term.path;
-            }
-
-            const button = document.createElement('button');
-            button.type = 'button';
-            button.className = 'use-existing-term';
-            button.setAttribute('data-id', term.id);
-            button.textContent = termDisplay;
-
-            button.addEventListener('click', () => {
-                // Add this term to selected items
-                this.selectedItems[term.id] = term.name;
-                this.updateSelected();
-
-                // Close the create new section
-                this.createNew.hidden = true;
-
-                // Optionally, close the modal
-                this.m.handleClose();
-            });
-
-            item.appendChild(button);
-            list.appendChild(item);
-        });
-
-        suggestionContainer.appendChild(list);
-        suggestionContainer.hidden = false;
-    }
-
-// Create container for term suggestions if it doesn't exist
-    createSuggestionContainer() {
-        const container = document.createElement('div');
-        container.className = 'term-suggestions';
-        container.hidden = true;
-
-        // Insert after the form
-        this.createNew.querySelector('form').after(container);
-        return container;
-    }
-
-// Show message when term suggestion is pending approval
-    showApprovalPendingMessage(termName) {
-        const pendingContainer = this.createNew.querySelector('.term-pending-approval') ||
-            this.createPendingContainer();
-
-        // Clear and set content
-        removeChildren(pendingContainer);
-
-        const message = document.createElement('div');
-        message.className = 'approval-message';
-        message.innerHTML = `
-        <h4>Thank you for your suggestion!</h4>
-        <p>Your suggestion "<strong>${escapeHtml(termName)}</strong>" has been submitted for review.</p>
-        <p>Other verified artists will review and approve your suggestion.</p>
-        <p>You'll receive a notification when it's approved.</p>
-        <button type="button" class="close-pending-message">Got it</button>
-    `;
-
-        pendingContainer.appendChild(message);
-        pendingContainer.hidden = false;
-
-        // Add event listener to close button
-        pendingContainer.querySelector('.close-pending-message').addEventListener('click', () => {
-            pendingContainer.hidden = true;
-            this.createNew.hidden = true;
-        });
-    }
-
-    createTermElement(term) {
-        if (!term || !term.name) return null;
-
-        const li = window.getTemplate('termListItem');
-        li.dataset.id = term.id;
-
-        const isSelected = (term.id in this.selectedItems);
-        let input = li.querySelector('input');
-        let label = li.querySelector('label');
-        let name = label.querySelector('span');
-
-        [input.id, input.name, input.value, input.disabled, label.htmlFor, label.title, name.textContent] =
-            [`${this.config.base}${term.id}`, `${this.config.base}${this.taxonomy}`, term.id, isSelected ? false : this.disabled, `${this.config.base}${term.id}`, escapeHtml(term.path || term.name), (term.show) ? term.path : term.name];
-
-        input.checked = isSelected;
-
-        if (term.hasChildren) {
-            let button = window.getTemplate('termChildrenToggle');
-            button.ariaLabel = `View sub-categories of ${escapeHtml(term.name)}`;
-            li.append(button);
-        }
-
-        return li;
-    }
-
-    updateSelected() {
-        if (this.config.feed) {
-            return;
-        }
-
-        // Clear existing selected items
-        removeChildren(this.selectedContainer);
-
-        for (const [id, term] of Object.entries(this.selectedItems)) {
-            const itemDiv = window.getTemplate('selectedTerm');
-            itemDiv.dataset.id = id;
-            let name = itemDiv.querySelector('span');
-            let button = itemDiv.querySelector('button');
-            [name.textContent, button.ariaLabel] =
-                [escapeHtml(term), `Remove ${escapeHtml(term)}`];
-
-            this.selectedContainer.appendChild(itemDiv);
-        }
-
-        if (this.isSelectionLimitReached()) {
-            this.disabled = true;
-            this.disableCheckboxes();
-        } else {
-            this.disabled = false;
-            this.enableCheckboxes();
-        }
-
-        // Also update the modal selected terms list if it exists
-        this.updateModalSelected();
-
-        // Only trigger change if there are actual changes
-        if (this.hasSelectionChanged()) {
-            if (this.initialized && this.config.onSuccess) {
-                this.config.onSuccess();
-            }
-            // Update initial state
-            this.initialState = new Map(Object.entries(this.selectedItems));
-        }
-    }
-
-    hasSelectionChanged() {
-        // First check if sizes are different
-        if (Object.keys(this.selectedItems).length !== this.initialState.size) {
-            return true;
-        }
-
-        // Then check if any items differ
-        for (const [id, name] of Object.entries(this.selectedItems)) {
-            if (!this.initialState.has(id) || this.initialState.get(id) !== name) {
-                return true;
-            }
-        }
-
-        // Also check if any items were removed
-        for (const id of this.initialState.keys()) {
-            if (!(id in this.selectedItems)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-    initializeEventListeners() {
-        let o = this.container.closest('.field')?.querySelector('.add-item-btn');
-        if (!o) {
-            o = this.container.closest('.jvb-selector')?.querySelector('.filter-toggle');
-        }
-
-        this.modal.addEventListener('click', (e)=> {
-            if (e.target.classList.contains('new-term-toggle') ||
-                e.target.closest('.new-term-toggle')) {
-                const isHidden = this.createNew.hidden;
-                this.createNew.hidden = !isHidden;
-
-                if (!this.createNew.hidden) {
-                    this.createNew.querySelector('input[name="term_name"]').focus();
-                }
-                this.resetParentOptions();
-            }
-        });
-
-        this.m = new window.jvbModal(
-            this.modal,
-            {
-                save: null,
-                open: o,
-                openMessage: 'Opened ' + this.taxonomy + ' selection. Choose from checkboxes to filter results.',
-                onOpen: () => this.openModal(),
-                onClose: () => this.closeModal()
-            }
-        );
-
-        // Selected items handling
-        if (!this.config.feed) {
-            this.selectedContainer.addEventListener('click', e => {
-                const removeBtn = e.target.closest('.remove-item');
-                if (!removeBtn) return;
-
-                const item = removeBtn.closest('.selected-item');
-                if (item) {
-                    this.removeItem(item.dataset.id);
-                }
-            });
-        } else {
-            this.container.querySelector('.filter-toggle').addEventListener('click', e => {
-                this.m.handleOpen();
-            });
-        }
-
-        this.itemsContainer.addEventListener('change', e => {
-            const input = e.target;
-            e.preventDefault();
-            if (input.type === 'checkbox' || input.type === 'radio') {
-                const item = input.closest('li');
-                if (!item) return;
-
-                const termId = item.dataset.id;
-                const termName = item.querySelector('.term-name').textContent;
-
-                if (input.checked) {
-                    // Add to selected items
-                    this.selectedItems[termId] = termName;
-                } else {
-                    delete this.selectedItems[termId];
-                    // Remove from selected items
-                }
-
-                if (this.isSelectionLimitReached()) {
-                    this.disabled = true;
-                    this.disableCheckboxes();
-                } else if (!this.disabled && this.limitWasReached === true) {
-                    this.enableCheckboxes();
-                }
-
-                // Update the selection display
-                this.updateSelected();
-            }
-        });
-
-        // Clear search button
-        if (this.clearSearchButton) {
-            this.clearSearchButton.addEventListener('click', () => {
-                this.searchInput.value = '';
-                this.searchQuery = '';
-                this.page = 1;
-                this.hasMore = true;
-                this.fetchAndRenderTerms();
-            });
-        }
-
-        // Back to parent button
-        const backButton = this.breadcrumbNav.querySelector('.back-to-parent');
-        if (backButton) {
-            backButton.addEventListener('click', () => this.navigateToParent());
-        }
-    }
-
-    openModal() {
-        this.searchInput.value = '';
-
-        // Wait for the modal to be fully open before fetching the first page of terms
-        setTimeout(() => {
-            this.fetchAndRenderTerms();
-        }, 50);
-
-        this.searchInput.focus();
-
-        // Search handling with debounce
-        this.searchInput.addEventListener('input',
-            window.debounce(() => this.handleSearch(), 300));
-    }
-    closeModal() {
-        this.updateSelected();
-        if (this.searchQuery) {
-            this.searchQuery = '';
-            this.page = 1;
-            this.hasMore = true;
-        }
-        if (this.config.feed) {
-            if (this.config.onClose) {
-                this.config.onClose();
-            }
-        }
-    }
-
-    disableCheckboxes() {
-        this.limitWasReached = true;
-        this.itemsContainer.querySelectorAll('input').forEach(input => {
-            input.disabled = (!input.checked);
-        });
-    }
-
-    enableCheckboxes() {
-        this.limitWasReached = false;
-        this.itemsContainer.querySelectorAll('input:disabled').forEach(input => {
-            input.disabled = false;
-        });
-    }
-
-    isSelectionLimitReached() {
-        return this.config.maxSelections > 0 &&
-            Object.keys(this.selectedItems).length >= this.config.maxSelections;
-    }
-
-    removeItem(id) {
-        delete this.selectedItems[id];
-
-        // Update checkboxes in the modal
-        const input = this.modal.querySelector(`input[value="${id}"]`);
-        if (input) {
-            input.checked = false;
-        }
-
-        this.updateSelected();
-    }
-
-    async handleSearch() {
-        const query = this.searchInput.value.trim();
-
-        if (query === this.searchQuery) return;
-
-        this.searchQuery = query;
-
-        // Reset pagination
-        this.page = 1;
-        this.hasMore = true;
-
-        // Don't show loading if already showing (prevents flickers)
-        if (!this.loading) {
-			this.showLoading();
-        }
-
-        try {
-            // Store checked state before clearing
-            const checkedState = new Map();
-            this.itemsContainer.querySelectorAll('input:checked').forEach(input => {
-                checkedState.set(input.value, true);
-            });
-
-            // Clear existing content
-            removeChildren(this.itemsContainer);
-
-            // Only search if query is at least 2 chars or empty
-            if (query.length >= 2 || query.length === 0) {
-                this.loading = false;
-                await this.fetchAndRenderTerms();
-            } else {
-                // For very short queries, just clear and show message
-				this.hideLoading();
-                this.noResultsText.hidden = false;
-                this.noResultsText.textContent = 'Enter at least 2 characters to search';
-            }
-
-            // Restore checked state
-            checkedState.forEach((_, value) => {
-                const input = this.itemsContainer.querySelector(`input[value="${value}"]`);
-                if (input) input.checked = true;
-            });
-
-        } catch (error) {
-            console.error('Search error:', error);
-            this.showError('Search failed');
-        }
-    }
-
-// Term creation initialization
-    initializeTermCreation() {
-        if (!this.config.createNew) return;
-
-        this.createNew = this.modal.querySelector('.create-new-term-section');
-        if (!this.createNew) return;
-
-        const toggle = this.modal.querySelector('.new-term-toggle');
-        const form = this.createNew.querySelector('form');
-        if (!form) return;
-
-        // let submitButton = form.querySelector('button[type="submit"]');
-        // submitButton.addEventListener('click', (e)=> {
-        //     e.preventDefault();
-        // });
-
-        // Handle form submission
-        form.addEventListener('submit', (e) => {
-            e.preventDefault();
-
-            const termName = e.target.querySelector('input[name="term_name"]').value.trim();
-            const parentId = e.target.querySelector('input#select_parent')?.value;
-
-            try {
-                form.querySelector('button').disabled = true;
-                const response = this.createTerm(termName, parentId);
-
-                if (response.success) {
-                    this.showNotification(
-                        'Thank you! Your suggestion has been submitted for review.',
-                        'success'
-                    );
-                    e.target.reset();
-                    this.createNew.hidden = true;
-                }
-            } catch (error) {
-                console.error('Error creating term:', error);
-                this.showError('Failed to submit suggestion');
-            } finally {
-                form.querySelector('button').disabled = false;
-            }
-        });
-    }
-
-    resetParentOptions() {
-        let ids = Array.from(this.currentTerms);
-        let select = this.modal.querySelector('#select_parent');
-        if (!select) return;
-
-        let option = select.querySelector('option');
-        if (!option) return;
-
-        removeChildren(select);
-        select.append(option);
-
-        if (this.currentParentName !== '') {
-            let o = option.cloneNode(true);
-            [o.value, o.textContent] =
-                [this.currentParent, this.currentParentName];
-            select.append(o);
-        }
-
-        if (ids.length > 0) {
-            ids.forEach(id => {
-                let o = option.cloneNode(true);
-                [o.id, o.value, o.textContent] =
-                    [`select-parent-${id[0]}`, id[0], '  — ' + id[1]];
-                select.append(o);
-            });
-        }
-    }
-
-    renderTerms(terms, clearExisting = true, path = false) {
-        if (!terms) return;
-
-        const targetContainer = this.itemsContainer;
-
-        console.log(this.page, 'Current Page');
-        console.log(clearExisting, 'Clear Existing');
-
-        if (clearExisting) {
-            this.currentTerms.clear();
-            removeChildren(targetContainer);
-
-            if (this.itemsWrap.querySelector('details')) {
-                this.itemsWrap.querySelector('details').removeAttribute('open');
-            }
-
-            // Term Navigation
-            let backButton = this.breadcrumbNav.querySelector('.back-to-parent');
-            removeChildren(this.breadcrumbNav);
-            this.breadcrumbNav.append(backButton);
-
-            // Show navigation path if we're not at root
-            if (this.navigationPath.length > 0) {
-                backButton.hidden = false;
-
-                // Create the navigation path
-                this.navigationPath.forEach((level, index) => {
-                    let button = window.getTemplate('termBreadcrumb');
-                    [button.dataset.level, button.dataset.id, button.title, button.textContent] =
-                        [index, level.id, level.path || level.name, escapeHtml(level.name)];
-                    this.breadcrumbNav.append(button);
-                });
-            } else {
-                backButton.hidden = true;
-            }
-        }
-
-        // Check if we have any terms
-        const hasTerms = terms && typeof terms === 'object' && Object.keys(terms).length > 0;
-
-        // Show appropriate message if no terms
-        if (!hasTerms) {
-            this.noResultsText.hidden = false;
-            if (this.searchQuery) {
-                this.noResultsText.textContent = `No results found for "${this.searchQuery}"`;
-            } else if (this.currentParent !== 0) {
-                this.noResultsText.textContent = 'No subcategories found';
-            } else {
-                this.noResultsText.textContent = 'No categories available';
-            }
-            return;
-        } else {
-            this.noResultsText.hidden = true;
-        }
-
-        // Render terms
-        this.disabled = this.isSelectionLimitReached();
-
-        Object.entries(terms).forEach(([id, term]) => {
-            if (!term) return;
-            this.currentTerms.set(id, typeof term === 'string' ? term : term.name);
-
-            const termElement = this.createTermElement({
-                id: parseInt(id),
-                name: typeof term === 'string' ? term : term.name,
-                hasChildren: typeof term === 'object' ? term.hasChildren : false,
-                path: term.path || null,
-                show: path
-            });
-
-            if (termElement) targetContainer.appendChild(termElement);
-        });
-
-        this.resetParentOptions();
-
-        this.container.removeEventListener('click', this.boundTermListener);
-        this.container.addEventListener('click', this.boundTermListener);
-    }
-
-    async navigateToParent() {
-        // Pop current level from navigation path
-        this.navigationPath.pop();
-
-        // Get previous parent from path or default to root
-        const previousLevel = this.navigationPath[this.navigationPath.length - 1];
-        this.currentParent = previousLevel ? previousLevel.id : 0;
-        this.currentParentName = previousLevel ? previousLevel.name : '';
-
-        this.page = 1;
-        this.hasMore = true;
-
-        await this.fetchAndRenderTerms();
-    }
-
-
-    async navigateToChildren(termId, termName) {
-        // Add current level to navigation path
-        this.navigationPath.push({
-            id: termId,
-            name: termName
-        });
-
-        this.currentParent = termId;
-        this.currentParentName = termName;
-
-        this.page = 1;
-        this.hasMore = true;
-
-        await this.fetchAndRenderTerms();
-    }
-
-    initializeToggleButtons(e) {
-        if (e.target.classList.contains('toggle-children') || e.target.closest('.toggle-children')) {
-            const item = e.target.closest('li');
-            if (!item) return;
-
-            const termId = parseInt(item.dataset.id);
-            const termName = item.querySelector('.term-name').textContent;
-
-            this.navigateToChildren(termId, termName);
-        }
-
-        if (e.target.classList.contains('path-level') || e.target.closest('.path-level')) {
-            const btn = e.target.classList.contains('path-level') ? e.target : e.target.closest('.path-level');
-
-            // Skip if already on this level
-            if (btn.textContent === this.currentParentName) return;
-
-            const level = parseInt(btn.dataset.level);
-
-            // Navigate to the specific level
-            this.navigationPath = this.navigationPath.slice(0, level + 1);
-            this.currentParent = parseInt(btn.dataset.id);
-            this.currentParentName = btn.textContent;
-            this.page = 1;
-            this.hasMore = true;
-
-            this.fetchAndRenderTerms();
-        }
-
-        if (e.target.classList.contains('back-to-parent') || e.target.closest('.back-to-parent')) {
-            this.navigateToParent();
-        }
-    }
-
-    showNotification(message, type = 'info') {
-        window.addNotification(message, type);
-    }
-
-    showError(message) {
-        this.showNotification(message, 'error');
-    }
-
-    initializeInfiniteScroll() {
-        const observer = new IntersectionObserver(entries => {
-            entries.forEach(entry => {
-                if (entry.isIntersecting && !this.loading && this.hasMore) {
-                    this.fetchAndRenderTerms();
-                }
-            });
-        }, {
-            root: this.itemsWrap,
-            threshold: 0.5
-        });
-
-        // Observe the sentinel element
-        const sentinel = this.container.querySelector('.scroll-sentinel');
-        if (sentinel) {
-            observer.observe(sentinel);
-        }
-    }
-
-    cleanup() {
-        // Remove event listeners
-        this.modal?.remove();
-        this.searchInput = null;
-        this.itemsContainer = null;
-        this.selectedContainer = null;
-    }
-}
-
-window.jvbSelector = TaxonomySelectorOld;
diff --git a/assets/js/dash/TaxonomySelectorPrev.js b/assets/js/dash/TaxonomySelectorPrev.js
deleted file mode 100644
index 3fb702e..0000000
--- a/assets/js/dash/TaxonomySelectorPrev.js
+++ /dev/null
@@ -1,572 +0,0 @@
-class TaxonomySelectorPrev {
-	constructor (container, options = {}) {
-
-		this.a11y = window.jvbA11y;
-		this.cache = window.jvbCache;
-		this.error = window.jvbError;
-
-		this.taxonomy = container.dataset.taxonomy;
-
-		this.container = container;
-		this.initConfig(options);
-		this.initElements();
-		this.initListeners();
-
-		this.page = 1;
-		this.hasMore = true;
-		this.isLoading = false;
-		this.searchQuery = '';
-
-		this.terms = new Map();
-
-		this.navigationPath = [];
-		this.currentParent = 0;
-		this.currentParentName = '';
-		this.currentPath = '';
-		this.fetchSpecificTerms = false;
-
-		this.creator = false;
-
-		if ('jvbTaxCreator' in window && this.container.querySelector('.create-new-term-section')) {
-			this.creator = new window.jvbTaxCreator(this);
-		}
-
-		//Render the selected items
-		this.updateSelected();
-		this.modal.backButton.hidden = this.currentParent === 0;
-	}
-
-	initConfig(options) {
-		//Set up currently selected items
-		this.selectedTerms = {};
-		if (options.selected) {
-			if (typeof options.selected === 'object' && !window.isEmptyObject(options.selected)) {
-				this.options.selected.forEach(item => {
-					this.selectedTerms[item.id] = item.name;
-				});
-			} else {
-				this.selectedTerms = options.selected;
-			}
-		}
-		let embeddedOptions = JSON.parse(this.container.dataset.config??'{}');
-		this.maxSelections = options.maxSelections || embeddedOptions.maxSelections || 0;
-		this.isHierarchal = options.hierarchical || embeddedOptions.maxSelections || 0;
-		this.base = options.base || embeddedOptions.base || '';
-		this.onSuccess = options.onSuccess || embeddedOptions.onSuccess || null;
-		this.onClose = options.onClose || embeddedOptions.onClose || null;
-
-		this.isFeed = options.feed || embeddedOptions.feed || false;
-		if (this.isFeed) {
-			this.feedSelected = document.querySelector('.selected-items-section .selected-items')
-		}
-	}
-
-	initElements() {
-		//Separate items by container
-		this.elements = {
-			modal: this.container.querySelector('dialog'),
-			selectedTerms: this.container.querySelector('.selected-items'),
-		};
-		this.modal = {
-			searchInput: this.elements.modal.querySelector('input[type=search]'),
-			termsList: this.elements.modal.querySelector('.items-container'),
-			termsWrap: this.elements.modal.querySelector('.items-wrap'),
-			breadcrumbs: this.elements.modal.querySelector('nav.term-navigation'),
-			loading: this.elements.modal.querySelector('.loading'),
-			loadingText: this.elements.modal.querySelector('.loading span'),
-			clearSearch: this.elements.modal.querySelector('.clear-search'),
-			selectedTerms: this.elements.modal.querySelector('.selected-items'),
-			backButton: this.elements.modal.querySelector('.back-to-parent'),
-			sentinel: this.elements.modal.querySelector('.scroll-sentinel')
-		};
-	}
-
-	initListeners() {
-		this.container.addEventListener('click', this.handleClick.bind(this));
-		this.container.addEventListener('change', this.handleChange.bind(this));
-
-		let o = this.container.closest('.field')?.querySelector('.add-item-btn');
-		if (!o) {
-			o = '.filter-toggle';
-			// o = this.container.closest('.jvb-selector')?.querySelector('.filter-toggle');
-		} else {
-			o = '.add-item-btn';
-		}
-
-		this.observer = new IntersectionObserver(
-			entries => {
-				entries.forEach(entry => {
-					if (entry.isIntersecting && !this.isLoading && this.hasMore) {
-						this.fetchTerms();
-					}
-				});
-			}, {
-				root: this.modal.termsWrap,
-				threshold: 0.5
-			}
-		);
-
-		this.m = new window.jvbModal(
-			this.elements.modal,
-			{
-				save: null,
-				open: o,
-				openMessage: `Opened ${this.taxonomy} selection. Choose from checkboxes or search to filter results.`,
-				onOpen: () => this.openModal(),
-				onClose:() => this.closeModal()
-			}
-		);
-	}
-	handleClick(e) {
-
-		if (window.targetCheck(e, '.remove-item')) {
-			let item = window.targetCheck(e, '.selected-item');
-			if (item) {
-				this.removeSelectedTerm(item.dataset.id);
-			}
-		} else if (window.targetCheck(e, '.back-to-parent')) {
-			this.toParent();
-		} else if (window.targetCheck(e, '.toggle-children')) {
-			let item = e.target.closest('li');
-			this.toChild(parseInt(item.dataset.id), item.querySelector('.term-name').textContent);
-		} else if (window.targetCheck(e, '.path-level')) {
-			let btn = window.targetCheck(e, '.path-level');
-			if (btn.textContent !== this.currentParentName) {
-
-				window.removeChildren(this.modal.termsList);
-				let level = parseInt(btn.dataset.level);
-
-				this.navigatioPath = this.navigationPath.slice(0, level + 1);
-				this.currentParent = parseInt(btn.dataset.id);
-				this.currentParentName = btn.textContent;
-				this.page = 1;
-				this.hasMore = true;
-				this.fetchTerms();
-			}
-		}
-	}
-	handleChange(e) {
-		e.preventDefault();
-		e.stopPropagation();
-		let input = e.target;
-		if (e.target.checked) {
-			let label = e.target.closest('li').querySelector('label');
-			this.addSelectedTerm(input.id, label.title, label.dataset.path);
-		} else {
-			this.removeSelectedTerm(input.id);
-		}
-	}
-
-	handleSearch(e) {
-		let query = this.modal.searchInput.value.trim();
-		if (query === this.searchQuery) return;
-
-		this.searchQuery = query;
-
-		this.page = 1;
-		this.hasMore = true;
-
-		window.removeChildren(this.modal.termsList);
-		if (query.length >= 2 || query.length === 0) {
-			this.fetchTerms(false, true);
-		} else {
-			this.hideLoading();
-			this.showEmptyState('No Results. \nEnter at least 2 characters to search.')
-		}
-	}
-
-	openModal() {
-		this.observer.observe(this.modal.sentinel);
-		this.fetchTerms();
-		this.modal.searchInput.focus();
-		this.modal.searchInput.addEventListener('input',
-			window.debounce(() => this.handleSearch()));
-	}
-	closeModal() {
-		this.observer.unobserve(this.modal.sentinel);
-		window.removeChildren(this.modal.termsList);
-		this.updateSelected();
-		this.resetState();
-		if(this.isFeed && typeof this.onClose === 'function') {
-			this.onClose(this.taxonomy, this.selectedTerms);
-		}
-		this.modal.searchInput.removeEventListener('input', window.debounce(() => this.handleSearch()));
-	}
-
-	nextPage() {
-		if (this.hasMore) {
-			this.page++;
-		}
-	}
-
-	resetPage() {
-		this.page = 1;
-		this.hasMore = true;
-	}
-
-	resetState() {
-		this.resetPage();
-		this.searchQuery = '';
-		this.page = 1;
-		this.currentParent = 0;
-		this.currentParentName = '';
-		this.currentPath = '';
-		this.navigationPath = [];
-		this.hasMore = true;
-		this.isLoading = false;
-		this.retries = {
-			count: 0,
-			max: 3,
-			delay: 1000
-		};
-	}
-
-	toParent() {
-		this.navigationPath.pop();
-
-		let prv = this.navigationPath[this.navigationPath.length - 1];
-		this.currentParent = prv ? prv.id : 0;
-		this.currentParentName = prv ? prv.name : '';
-
-		window.removeChildren(this.modal.termsList);
-		this.page = 1;
-		this.hasMore = true;
-		this.fetchTerms();
-	}
-
-	toChild(termId, termName) {
-		this.navigationPath.push({
-			id: termId,
-			name: termName
-		});
-		this.currentParent = termId;
-		this.currentParentName = termName;
-		window.removeChildren(this.modal.termsList);
-
-		this.page = 1;
-		this.hasMore = true;
-		this.fetchTerms();
-	}
-
-	buildRequest() {
-		let params = new URLSearchParams({
-			taxonomy: this.taxonomy,
-			parent: this.currentParent,
-			search: this.searchQuery,
-			page: this.page
-		});
-
-		if (this.isFeed && this.fetchSpecificTerms) {
-			params.append('termIDs', this.fetchSpecificTerms);
-		}
-
-		if (this.isFeed && window.feedBlock?.config?.context) {
-			params.append('main_context', JSON.stringify({
-				context: window.feedBlock.config.context,
-				id: window.feedBlock.config.source
-			}));
-			params.append('content', window.feedBlock.filters.content);
-		}
-
-		return params.toString();
-	}
-	showLoading() {
-		this.isLoading = true;
-		this.modal.loading.hidden = false;
-		this.elements.modal.classList.add('loading');
-
-		let text = (this.searchQuery !== '') ? 'searching for "'+this.searchQuery+'" items' : ((this.currentParentName === '') ? 'loading items' : 'loading '+this.currentParentName+' items');
-		this.stopTyping = window.typeLoop(this.modal.loadingText, text);
-
-	}
-	hideLoading() {
-		this.isLoading = false;
-		this.modal.loading.hidden = true;
-		this.elements.modal.classList.remove('loading');
-		this.stopTyping();
-	}
-	async fetchTerms(forceRefresh = false, showPath = false) {
-		if (this.isLoading) {
-			return;
-		}
-
-		try {
-			this.showLoading();
-
-			const data = await this.cache.fetchWithCache(
-				`${jvbSettings.api}terms?`+this.buildRequest(),
-				{
-					method: 'GET',
-					headers: {
-						'Content-Type': 'application/json',
-						'X-WP-Nonce': jvbSettings.nonce,
-					}
-				},
-				{
-					content: this.taxonomy,
-					// forceRefresh: true
-					forceRefresh: forceRefresh
-				}
-			);
-
-			if (this.fetchSpecificTerms) {
-				this.fetchSpecificTerms = false;
-				return data.terms;
-			}
-
-
-			if (!data || !data.terms || data.terms.length === 0) {
-				if (this.page === 1) {
-					this.showEmptyState();
-				}
-				this.hasMore = false;
-			} else {
-				this.hasMore = data.pagination['has_more'];
-				this.renderTerms(data.terms, this.page > 1, showPath);
-				if (this.hasMore) {
-					this.nextPage();
-				}
-			}
-		} catch (e) {
-			this.handleError(e);
-		} finally {
-			this.hideLoading();
-		}
-	}
-
-	handleError(error){
-		return this.error.log(
-			error,
-			{
-				component: 'Taxonomy Selector',
-				action: 'fetchTerms'
-			},
-			() => this.fetchTerms()
-		);
-	}
-
-	addPlaceholders() {
-
-	}
-
-	renderTerms(terms, append = false, path = false) {
-		if (!append) {
-			window.removeChildren(this.modal.termsList);
-			this.addPlaceholders();
-		}
-		if (terms.length === 0) {
-			this.a11y.announceUpdate(0, append);
-			return;
-		}
-
-		this.updateBreadcrumbs();
-
-
-
-		this.disabled = this.isLimitReached();
-		for (let [id, term] of Object.entries(terms)) {
-			if (!term) return;
-			this.terms.set(term.id, term.name);
-			this.createTermElement({
-				id: parseInt(term.id),
-				name: term.name,
-				hasChildren: term.hasChildren,
-				path: term.path || null,
-				show: path
-			});
-		}
-	}
-
-
-
-	createTermElement(term) {
-		if (!term || !term.name) return;
-
-		let item = window.getTemplate('termListItem');
-		item.dataset.id = term.id;
-		const isSelected = (term.id in this.selectedTerms);
-		let input = item.querySelector('input');
-		let label = item.querySelector('label');
-		let name = item.querySelector('span');
-
-		[
-			input.id,
-			input.name,
-			input.value,
-			input.disabled,
-			input.checked,
-			label.htmlFor,
-			label.title,
-			label.dataset.path,
-			name.textContent
-		] = [
-			`${this.base}${term.id}`,
-			`${this.base}${this.taxonomy}-select`,
-			term.id,
-			isSelected ? false : this.disabled,
-			isSelected,
-			`${this.base}${term.id}`,
-			term.path || term.name,
-			term.path,
-			term.show ? term.path : term.name,
-		];
-
-		if (term.hasChildren) {
-			let button = window.getTemplate('termChildrenToggle');
-			button.ariaLabel = `View sub-terms of ${term.name}`;
-			item.append(button);
-		}
-
-		this.modal.termsList.append(item);
-	}
-
-	updateBreadcrumbs() {
-		window.removeChildren(this.modal.breadcrumbs);
-		this.modal.breadcrumbs.append(this.modal.backButton);
-
-		this.modal.backButton.hidden = this.currentParent === 0;
-		this.navigationPath.forEach((level, index) => {
-			let button = window.getTemplate('termBreadcrumb');
-			[
-				button.dataset.level,
-				button.dataset.id,
-				button.title,
-				button.textContent
-			] = [
-				index,
-				level.id,
-				level.path || level.name,
-				level.name
-			];
-			this.modal.breadcrumbs.append(button);
-		});
-	}
-
-	showEmptyState(message = ''){
-		let template = window.getTemplate('noResults');
-		if (message !== '') {
-			template.querySelector('span').textContent = message;
-		}
-		this.modal.termsList.append(template);
-	}
-
-	updateSelected() {
-		if (this.isFeed) {
-			return;
-		}
-
-		let checks = this.getSpacesToUpdate();
-		checks.forEach(check => {
-			window.removeChildren(check);
-		});
-
-		for (const [id, term] of Object.entries(this.selectedTerms)) {
-			this.addTermToBoxes(id, term.name, term.path);
-		}
-
-		this.disabled = this.isLimitReached();
-		this.setCheckboxes(this.disabled);
-	}
-
-	async addTermsFromURL(ids) {
-		this.fetchSpecificTerms = ids;
-		let terms = await this.fetchTerms();
-		terms.forEach(term => {
-			this.addSelectedTerm(term.id, term.name, term.path);
-		});
-	}
-
-	addSelectedTerm(id, name, path) {
-		this.selectedTerms[id] = {
-			name: name,
-			path: path
-		};
-
-		this.addTermToBoxes(id, name, path);
-	}
-
-	getSpacesToUpdate() {
-		let checks = [
-			this.modal.selectedTerms,
-			this.elements.selectedTerms
-		];
-
-		if (this.isFeed) {
-			checks.push(this.feedSelected);
-		}
-		return checks;
-	}
-
-	addTermToBoxes(id, name, path) {
-		let checks = this.getSpacesToUpdate();
-
-		checks.forEach(check => {
-			if (!check.querySelector(`[data-id="${id}"]`)) {
-				let item = window.getTemplate('selectedTerm');
-				[
-					item.dataset.id,
-					item.dataset.path,
-					item.dataset.name,
-					item.dataset.taxonomy,
-					item.querySelector('span').textContent,
-					item.querySelector('button').title
-				] = [
-					id,
-					path,
-					name,
-					this.taxonomy,
-					path,
-					`Remove ${name}`
-				];
-				if (check === this.feedSelected) {
-					item.prepend(window.getIcon(this.taxonomy));
-				}
-				check.append(item);
-			}
-		});
-
-		let input = this.modal.termsList.querySelector(`input[value="${id}"]`);
-		if (input) {
-			input.checked = true;
-		}
-	}
-	removeSelectedTerm(id) {
-		delete this.selectedTerms[id];
-
-		let checks = [
-			this.modal.selectedTerms,
-			this.elements.selectedTerms
-		];
-		if (this.isFeed) {
-			checks.push(this.feedSelected);
-		}
-		checks.forEach(check => {
-			check.querySelector(`[data-id="${id}"]`)?.remove();
-		});
-
-
-		let input = this.modal.termsList.querySelector(`input[value="${id}"]`);
-		if (input) {
-			input.checked = false;
-		}
-	}
-
-	setCheckboxes(disabled) {
-		this.modal.termsList.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
-			if (!checkbox.checked) {
-				checkbox.disabled = disabled;
-			}
-		});
-	}
-
-	isLimitReached() {
-		return this.maxSelections > 0 && this.getSelectedTerms().length > this.maxSelections;
-	}
-
-	getSelectedTerms() {
-		return this.modal.termsList.querySelectorAll('input:checked');
-	}
-
-}
-
-window.jvbSelector = TaxonomySelectorPrev;
diff --git a/assets/js/dash/UploadManager.js b/assets/js/dash/UploadManager.js
deleted file mode 100644
index 138b070..0000000
--- a/assets/js/dash/UploadManager.js
+++ /dev/null
@@ -1,4365 +0,0 @@
-	/**
-	 * Centralized Upload Manager
-	 * Handles all file inputs on the page from a single location
-	 */
-	class UploadManager {
-		constructor(store = null) {
-			this.store = store || new window.jvbStore({
-				name: 'uploads',
-				cacheTTL: 604800, // 7 days
-				useIndexedDB: true
-			});
-
-			this.queue = window.jvbQueue;
-			this.a11y = window.jvbA11y;
-			this.error = window.jvbError;
-			this.notifications = window.jvbNotifications;
-
-			// Central state management
-			this.fields = new Map();           // fieldId -> field configuration
-			this.uploads = new Map();          // uploadId -> upload state
-			this.subscribers = new Set();
-
-			this.performanceMonitor = new UploadPerformanceMonitor();
-			this.compressionWorker = null;
-
-
-			// Global settings
-			this.settings = {
-				allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
-				maxFileSize: 5242880,          // 5MB
-
-				// Field type configurations
-				fieldTypes: {
-					'single': { maxFiles: 1, allowMultiple: false },
-					'gallery': { maxFiles: 20, allowMultiple: true },
-					'groupable': {maxFiles: 20, allowMultiple: true }
-				},
-				smartCompression: true,
-			};
-
-			this.statusMapping = {
-				'queued': { status: 'queued', message: 'Waiting in queue...' },
-				'pending': { status: 'pending', message: 'Waiting for server...' },
-				'processing': { status: 'processing', message: 'Processing on server...' },
-				'uploading': { status: 'uploading', message: 'Uploading files...' },
-				'completed': { status: 'completed', message: 'Upload complete!' },
-				'failed': { status: 'failed', message: 'Upload failed (will retry)' },
-				'failed_permanent': { status: 'failed_permanent', message: 'Upload failed permanently' }
-			};
-
-			//Groups!
-			this.groups = new Map();           // groupId -> Set of uploadIds
-			this.groupMetadata = new Map();    // groupId -> group metadata (name, etc.)
-			this.initializeDragState();
-
-			this.init();
-		}
-
-		/**
-		 * Initialize the upload manager
-		 */
-		async init() {
-			// Check for unfinished uploads from previous session
-			await this.checkUnfinishedUploads();
-
-			// Set up event listeners
-			this.initListeners();
-
-			// Scan for existing fields
-			this.scanFields();
-		}
-
-		/**
-		 * Check for unfinished uploads using DataStore
-		 */
-		async checkUnfinishedUploads() {
-			try {
-				console.log('Checking for unfinished uploads...');
-
-				// Get all forms that might contain upload data
-				const allForms = this.store.getAllForms();
-				const unfinishedUploads = new Map();
-
-				// Look for upload-related form data
-				for (const [formId, formData] of allForms) {
-					if (formData.status === 'pending' && formData.uploadData) {
-						const fieldId = formData.uploadData.fieldId;
-						if (!unfinishedUploads.has(fieldId)) {
-							unfinishedUploads.set(fieldId, []);
-						}
-						unfinishedUploads.get(fieldId).push(formData);
-					}
-				}
-
-				// Show restore notifications for fields with unfinished uploads
-				for (const [fieldId, uploads] of unfinishedUploads) {
-					await this.showRestoreNotification(fieldId, uploads);
-				}
-
-			} catch (error) {
-				console.error('Failed to check unfinished uploads:', error);
-			}
-		}
-
-		/**
-		 * Store upload progress and data using DataStore
-		 */
-		cacheUploadProgress(fieldId, uploadId, data) {
-			const cacheKey = `upload_${fieldId}_${uploadId}`;
-
-			const uploadData = {
-				formId: cacheKey,
-				fieldId,
-				uploadId,
-				uploadData: {
-					...data,
-					fieldId,
-					timestamp: Date.now()
-				},
-				status: 'pending',
-				operationId: data.operationId || null
-			};
-
-			// Store using DataStore's form methods
-			this.store.storeForm(cacheKey, uploadData);
-		}
-
-		/**
-		 * Get cached upload data
-		 */
-		getCachedUpload(fieldId, uploadId) {
-			const cacheKey = `upload_${fieldId}_${uploadId}`;
-			return this.store.getForm(cacheKey);
-		}
-
-		/**
-		 * Clear upload cache for a specific field
-		 */
-		async clearFieldCache(fieldId) {
-			try {
-				console.log(`Clearing cache for field: ${fieldId}`);
-
-				// Get all forms and filter for this field's uploads
-				const allForms = this.store.getAllForms();
-				const keysToDelete = [];
-
-				for (const [formId, formData] of allForms) {
-					if (formData.uploadData && formData.uploadData.fieldId === fieldId) {
-						keysToDelete.push(formId);
-					}
-				}
-
-				// Clear all related form data
-				keysToDelete.forEach(key => {
-					this.store.clearForm(key);
-				});
-
-				// Clear any cached field data
-				this.clearFieldMemoryCache(fieldId);
-
-				console.log(`Cleared cache for field ${fieldId}: ${keysToDelete.length} items removed`);
-
-			} catch (error) {
-				console.error(`Failed to clear field cache for ${fieldId}:`, error);
-				throw error;
-			}
-		}
-
-		/**
-		 * Clear memory cache data related to a field
-		 */
-		clearFieldMemoryCache(fieldId) {
-			// Clear uploads related to this field
-			const uploadsToRemove = [];
-
-			for (const [uploadId, upload] of this.uploads) {
-				if (upload.fieldId === fieldId) {
-					// Clean up blob URLs
-					if (upload.preview && upload.preview.startsWith('blob:')) {
-						URL.revokeObjectURL(upload.preview);
-					}
-					uploadsToRemove.push(uploadId);
-				}
-			}
-
-			// Remove uploads from memory
-			uploadsToRemove.forEach(uploadId => {
-				this.uploads.delete(uploadId);
-			});
-
-			console.log(`Cleared ${uploadsToRemove.length} uploads from memory for field: ${fieldId}`);
-		}
-
-		/**
-		 * Show restore notification for unfinished uploads
-		 */
-		async showRestoreNotification(fieldId, cachedUploads) {
-			const field = this.fields.get(fieldId);
-			if (!field) {
-				console.warn(`Cannot show restore for unknown field: ${fieldId}`);
-				return;
-			}
-
-			// Create restore notification
-			const notification = this.createSelectiveRestoreNotification(fieldId, cachedUploads);
-
-			// Insert into field container
-			field.container.insertBefore(notification, field.container.firstChild);
-		}
-
-		/**
-		 * Create selective restore notification UI
-		 */
-		createSelectiveRestoreNotification(fieldId, cachedUploads) {
-			const template = window.getTemplate('restoreNotification');
-			if (!template) {
-				console.error('restoreNotification template not found');
-				return null;
-			}
-
-			const notification = template.cloneNode(true);
-			const details = notification.querySelector('.restore-details');
-			const container = notification.querySelector('.item-grid.restore');
-
-			// Update message
-			details.textContent = `Found ${cachedUploads.length} unfinished upload(s) for this field.`;
-
-			// Create restore items
-			cachedUploads.forEach(upload => {
-				const item = this.createRestoreItem(upload);
-				container.append(item);
-			});
-
-			// Attach event listeners
-			this.attachRestoreEventListeners(notification, fieldId, cachedUploads);
-
-			return notification;
-		}
-
-		/**
-		 * Create individual restore item
-		 */
-		createRestoreItem(uploadData) {
-			const template = window.getTemplate('restoreItem');
-			if (!template) {
-				console.error('restoreItem template not found');
-				return null;
-			}
-
-			const item = template.cloneNode(true);
-			const checkbox = item.querySelector('.restore-checkbox');
-			const img = item.querySelector('img');
-			const name = item.querySelector('.item-name');
-
-			// Set up item data
-			checkbox.id = `restore-${uploadData.uploadId}`;
-			checkbox.value = uploadData.uploadId;
-
-			// Use cached preview or create placeholder
-			if (uploadData.uploadData.preview) {
-				img.src = uploadData.uploadData.preview;
-				img.alt = uploadData.uploadData.originalName || 'Upload preview';
-			} else {
-				img.style.display = 'none';
-				item.querySelector('.image-placeholder').style.display = 'block';
-			}
-
-			if (name) {
-				name.textContent = uploadData.uploadData.originalName || 'Untitled upload';
-			}
-
-			return item;
-		}
-
-		/**
-		 * Attach event listeners to restore notification
-		 */
-		attachRestoreEventListeners(notification, fieldId, cachedUploads) {
-			const selectAll = notification.querySelector('.select-all-restore');
-			const selectNone = notification.querySelector('.select-none-restore');
-			const restoreSelected = notification.querySelector('.restore-selected');
-			const clearCache = notification.querySelector('.restart-uploads');
-			const dismiss = notification.querySelector('.dismiss-cache-check');
-
-			// Select all/none functionality
-			selectAll?.addEventListener('click', () => {
-				notification.querySelectorAll('.restore-checkbox').forEach(cb => cb.checked = true);
-			});
-
-			selectNone?.addEventListener('click', () => {
-				notification.querySelectorAll('.restore-checkbox').forEach(cb => cb.checked = false);
-			});
-
-			// Restore selected items
-			restoreSelected?.addEventListener('click', async () => {
-				const selectedCheckboxes = notification.querySelectorAll('.restore-checkbox:checked');
-				const selectedIds = Array.from(selectedCheckboxes).map(cb => cb.value);
-
-				if (selectedIds.length === 0) {
-					this.notify('No items selected for restore', 'warning');
-					return;
-				}
-
-				// Restore selected uploads
-				const selectedUploads = cachedUploads.filter(upload =>
-					selectedIds.includes(upload.uploadId)
-				);
-
-				await this.restoreSelectedUploads(fieldId, selectedUploads);
-				notification.remove();
-				this.notify(`Restored ${selectedIds.length} item(s)`, 'success');
-			});
-
-			// Clear cache
-			clearCache?.addEventListener('click', async () => {
-				if (confirm('This will permanently delete all cached data. Are you sure?')) {
-					await this.clearFieldCache(fieldId);
-					notification.remove();
-					this.notify('Cache cleared', 'info');
-				}
-			});
-
-			// Dismiss notification
-			dismiss?.addEventListener('click', () => {
-				notification.remove();
-			});
-		}
-
-		/**
-		 * Restore selected uploads
-		 */
-		async restoreSelectedUploads(fieldId, selectedUploads) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			let operations = new Set();
-
-			for (const cachedUpload of selectedUploads) {
-				try {
-					const upload = await this.restoreUploadFromCache(cachedUpload);
-
-					if (upload) {
-						operations.add(upload.operationId);
-
-						// Add to field
-						if (!field.uploads) field.uploads = new Set();
-						field.uploads.add(upload.id);
-
-						// Add to main preview
-						this.addImageToPost(upload.id, field.previewGrid, true);
-					}
-
-				} catch (error) {
-					console.error(`Failed to restore upload ${cachedUpload.uploadId}:`, error);
-				}
-			}
-
-			field.operationId = operations;
-			this.fields.set(fieldId, field);
-
-			// Update field UI
-			this.maybeLockUploads(fieldId);
-
-			if (field.type === 'groupable') {
-				field.container.querySelector('.group-display').hidden = false;
-			}
-		}
-
-		/**
-		 * Restore individual upload from cached data
-		 */
-		async restoreUploadFromCache(cachedUpload) {
-			try {
-				const upload = {
-					id: cachedUpload.uploadId,
-					fieldId: cachedUpload.fieldId,
-					status: 'cached',
-					progress: { percent: 100, message: 'Restored from cache' },
-					meta: cachedUpload.uploadData.meta || {},
-					preview: cachedUpload.uploadData.preview || null,
-					createdAt: cachedUpload.uploadData.timestamp || Date.now(),
-					operationId: cachedUpload.operationId
-				};
-
-				// If we have processed file data, restore it
-				if (cachedUpload.uploadData.processedFile) {
-					upload.processedFile = cachedUpload.uploadData.processedFile;
-				}
-
-				if (cachedUpload.uploadData.originalFile) {
-					upload.originalFile = cachedUpload.uploadData.originalFile;
-				}
-
-				// Store upload in memory
-				this.uploads.set(upload.id, upload);
-
-				return upload;
-
-			} catch (error) {
-				console.error('Failed to restore upload from cache:', error);
-				throw error;
-			}
-		}
-
-		/**
-		 * Save upload progress during processing
-		 */
-		saveUploadProgress(fieldId, uploadId, progressData) {
-			const upload = this.uploads.get(uploadId);
-			if (!upload) return;
-
-			// Update upload progress
-			upload.progress = progressData.progress || upload.progress;
-			upload.status = progressData.status || upload.status;
-
-			// Cache the updated data
-			this.cacheUploadProgress(fieldId, uploadId, {
-				...upload,
-				originalFile: upload.originalFile ? {
-					name: upload.originalFile.name,
-					type: upload.originalFile.type,
-					size: upload.originalFile.size,
-					lastModified: upload.originalFile.lastModified
-				} : null,
-				processedFile: null, // Don't store the actual file data in cache
-				operationId: upload.operationId,
-				meta: upload.meta,
-				originalName: upload.originalFile?.name
-			});
-		}
-
-		/**
-		 * Mark upload as completed and clean up cache
-		 */
-		completeUpload(fieldId, uploadId) {
-			const cacheKey = `upload_${fieldId}_${uploadId}`;
-
-			// Remove from cache since upload is complete
-			this.store.clearForm(cacheKey);
-
-			// Update upload status in memory
-			const upload = this.uploads.get(uploadId);
-			if (upload) {
-				upload.status = 'completed';
-			}
-		}
-
-		/**
-		 * Event system
-		 */
-		subscribe(callback) {
-			this.subscribers.add(callback);
-			return () => this.subscribers.delete(callback);
-		}
-
-		notify(message, type = 'info') {
-			this.subscribers.forEach(cb => {
-				if (typeof cb === 'function') {
-					cb('notification', { message, type });
-				}
-			});
-
-			// Also log to console
-			console.log(`[BatchFileUploader] ${message}`);
-		}
-
-
-		/**************************************************************
-		 EVENTS
-		**************************************************************/
-
-
-		initializeDragState() {
-			this.dragState = {
-				// What's being dragged
-				primaryItem: null,
-				draggedItems: [],
-				isDragging: false,
-				isMultiDrag: false,
-
-				// Drag context
-				fieldId: null,
-				sourceType: null, // 'drag' or 'touch'
-				startTime: null,
-
-				// Position tracking
-				startPosition: null, // { x, y }
-				currentPosition: null, // { x, y }
-
-				// Target tracking
-				currentTarget: null,
-				validTarget: null,
-
-				// Visual elements
-				dragPreview: null,
-
-				// Touch-specific
-				touchId: null,
-				touchMoved: false
-			};
-		}
-
-		/**
-		 * Set up global event delegation
-		 */
-		initEventListeners() {
-			this.paste = window.debouncer.schedule(
-				'image-paste',
-				() => this.handlePaste.bind(this),
-				300
-			);
-
-			this.clickHandler = this.handleClick.bind(this);
-			this.changeHandler = this.handleChange.bind(this);
-			this.dragStartHandler = this.handleDragStart.bind(this);
-			this.dragEndHandler = this.handleDragEnd.bind(this);
-			this.dragEnterHandler = this.handleDragEnter.bind(this);
-			this.dragOverHandler = this.handleDragOver.bind(this);
-			this.dragLeaveHandler = this.handleDragLeave.bind(this);
-			this.dropHandler = this.handleDrop.bind(this);
-			this.touchStartHandler = this.handleTouchStart.bind(this);
-			this.touchMoveHandler = this.handleTouchMove.bind(this);
-			this.touchEndHandler = this.handleTouchEnd.bind(this);
-			this.touchCancelHandler = this.handleTouchCancel.bind(this);
-
-
-			document.addEventListener('click', this.clickHandler);
-			document.addEventListener('change', this.changeHandler);
-			document.addEventListener('paste', this.paste);
-			window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
-			//Groups
-			document.addEventListener('dragstart', this.dragStartHandler);
-			document.addEventListener('dragend', this.dragEndHandler);
-			document.addEventListener('dragenter', this.dragEnterHandler);
-			document.addEventListener('dragover', this.dragOverHandler);
-			document.addEventListener('dragleave', this.dragLeaveHandler);
-			document.addEventListener('drop', this.dropHandler);
-
-			document.addEventListener('touchstart', this.touchStartHandler, { passive: false });
-			document.addEventListener('touchmove', this.touchMoveHandler, { passive: false });
-			document.addEventListener('touchend', this.touchEndHandler, { passive: false });
-			document.addEventListener('touchcancel', this.touchCancelHandler, { passive: false });
-		}
-
-		/****************************************************************
-		 *
-		 * Scanning, registering, and validating page uploaders
-		 *
-		 ***************************************************************/
-		/**
-		 * Scan page for existing upload fields and register them
-		 */
-		scanExistingFields() {
-			const uploaders = document.querySelectorAll('.field.image');
-			uploaders.forEach(uploader => {
-				try {
-					this.registerUploader(uploader);
-				} catch (error) {
-					this.error.log(error, {
-						component: 'UploadManager',
-						action: 'scanExistingFields',
-						container: uploader.dataset.name
-					});
-				}
-			});
-		}
-
-		registerUploader(uploader, options = {}) {
-			let input = uploader.querySelector('input[type=file]');
-			if (!input) {
-				return;
-			}
-
-			if (!('fieldId' in uploader.dataset)) {
-				uploader.dataset.fieldId = this.createFieldId(uploader);
-			}
-			let fieldId = uploader.dataset.fieldId;
-
-			let typeConfig = this.settings.fieldTypes[uploader.dataset.type] || this.settings.fieldTypes['single'];
-			let config = {
-				id: fieldId,
-				input: input,
-				container: uploader,
-				type: uploader.dataset.type,
-				name: uploader.dataset.name,
-				operationId: new Set(),
-
-				posts: new Map(),
-
-				maxFiles: typeConfig.maxFiles,
-				allowMultiple: typeConfig.allowMultiple,
-				groupsContainer: uploader.querySelector('.item-grid.groups')??false,
-				groupDisplay: uploader.querySelector('.group-display')??false,
-				selectAll: uploader.querySelector('#select-all-uploads'),
-				selectActions: uploader.querySelector('.selection-actions'),
-				selectInfo: uploader.querySelector('.selection-controls .info'),
-				selectCount: uploader.querySelector('.selection-count'),
-
-				content: uploader.dataset.content || 'options',
-				contentFields: uploader.dataset.fields ?? {}, //If this is a content creation uploader, we can add its fields here
-				postId: uploader.dataset.postId??false,
-				termId: uploader.dataset.termId??false,
-
-				mode: uploader.dataset.mode??'direct',
-				hiddenValue: uploader.querySelector('input[type="hidden"]'),
-				fields: {
-					title: { label: 'Image Title', type: 'text', required: false },
-					alt: { label: 'Alt Text', type: 'text', required: true },
-					caption: { label: 'Image Caption', type: 'textarea', required: false }
-				},
-				dropZone: uploader.querySelector('.file-upload-container'),
-				previewGrid: uploader.querySelector('.item-grid.preview'),
-
-				uploads: new Set(),
-				status: 'ready',
-				... options
-			};
-
-			this.fields.set(fieldId, config);
-
-			return fieldId;
-		}
-
-		/*******************************************************************
-		 *
-		 * Event Listeners
-		 *
-		 ******************************************************************/
-		/**
-		 * Handle file input changes
-		 */
-		handleChange(e) {
-
-			//Only run on uploader changes
-			if (!window.targetCheck(e, '.field.image')) {
-				return;
-			}
-			e.preventDefault();
-
-			if (window.targetCheck(e, 'input[type="file"]')) {
-				const fieldId = this.getFieldId(e.target);
-				const field = this.fields.get (fieldId);
-
-				if (!field) {
-					console.warn('File change on unregistered field: ', fieldId);
-					return;
-				}
-
-				const files = Array.from(e.target.files);
-				if (files.length === 0) return;
-
-				this.processFiles(fieldId, files);
-
-				e.target.value = '';
-			} else if (e.target.closest('.upload-group') || e.target.name === 'featured') {
-
-				this.addMetaToPost(e.target);
-			} else if (e.target.closest('.upload-meta')) {
-
-				this.addMetaToImage(e.target);
-				this.maybeUpdateImageMeta();
-			}
-		}
-
-		handleClick(e) {
-			if (!window.targetCheck(e, '.image.field')) return;
-
-			let [
-				restart,
-				dismissCacheCheck,
-				selectAll,
-				selectOne,
-				createFromSelection,
-				removeSelection,
-				addToPost,
-				removePost,
-				remove,
-				submitUploads,
-				retry,
-			] = [
-				window.targetCheck(e, '.restart-uploads'),
-				window.targetCheck(e, '.dismiss-cache-check'),
-				window.targetCheck(e, '#select-all-uploads'),
-				window.targetCheck(e, '.upload-select'),
-				window.targetCheck(e, '.create-from-selection'),
-				window.targetCheck(e, '.remove-selection'),
-				window.targetCheck(e, '.add-to-group')??window.targetCheck(e,'.add-selection-to-group'),
-				window.targetCheck(e, '.remove-group'),
-				window.targetCheck(e, '.remove'), //handle remove from group and removal
-				window.targetCheck(e, '.submit-uploads'),
-				window.targetCheck(e, '.retry-upload'),
-			];
-
-			if (this.isUploadTrigger(e.target)) {
-				this.triggerFileSelection(this.getFieldId(e.target));
-			} else if (restart) {
-				this.clearCache(this.getFieldId(restart));
-			} else if (dismissCacheCheck) {
-				this.dismissCacheCheck(this.getFieldId(dismissCacheCheck));
-			} else if (selectAll) {
-				this.handleSelectAll(selectAll);
-			} else if (selectOne) {
-				if (e.shiftKey && this.lastClickedUpload) {
-					this.handleRangeSelection(selectOne, e);
-				} else {
-					this.handleUploadSelection(selectOne);
-					// Track last clicked upload for range selection
-					this.lastClickedUpload = this.getUploadId(selectOne);
-				}
-			} else if (createFromSelection) {
-				this.createPostFromSelection(createFromSelection);
-			}else if (removeSelection) {
-				this.removeSelection(removeSelection);
-			} else if (addToPost) {
-				let group = addToPost.closest('.upload-group')?.querySelector('.item-grid')??false;
-				if (!group) {
-					group = this.createPostElement(this.getFieldId(addToPost));
-				}
-				this.getSelectedUploads(addToPost).forEach(upload => {
-					this.addImageToPost(upload, group);
-				});
-			} else if (removePost) {
-				this.removePost(removePost);
-			} else if (remove) {
-				this.removeImageFromPost(remove, this.getUploadId(remove));
-			} else if (submitUploads) {
-				this.submitPostData(submitUploads);
-			} else if (retry) {
-				this.retryUpload(this.getFieldId(retry), this.getUploadId(retry));
-			}
-		}
-
-		async retryUpload(fieldId, uploadId) {
-			const upload = this.uploads.get(uploadId);
-			if (!upload || upload.status !== 'error') return;
-
-			this.a11y.announce(`Retrying upload for ${upload.originalFile.name}`);
-			await this.queueUpload(uploadId);
-		}
-
-		/**
-		 * Handle page unload
-		 */
-		handleBeforeUnload(e) {
-			const activeUploads = Array.from(this.uploads.values())
-				.filter(upload => ['processing', 'uploading'].includes(upload.status));
-
-			if (activeUploads.length > 0) {
-				e.preventDefault();
-				e.returnValue = `You have ${activeUploads.length} upload(s) in progress. Are you sure you want to leave?`;
-				return e.returnValue;
-			}
-
-			//TODO: Check for unsaved field changes
-		}
-
-		/**
-		 * Handle paste events (for image paste support)
-		 */
-		handlePaste(e) {
-			const activeField = document.activeElement?.closest('[data-upload-field]');
-			if (!activeField) return;
-
-			const items = Array.from(e.clipboardData.items);
-			const imageItems = items.filter(item => item.type.startsWith('image/'));
-
-			if (imageItems.length === 0) return;
-
-			e.preventDefault();
-
-			const fieldId = this.getFieldId(activeField);
-			if (!fieldId) return;
-
-			// Convert clipboard items to files
-			const files = [];
-			imageItems.forEach((item, index) => {
-				const file = item.getAsFile();
-				if (file) {
-					// Rename for clarity
-					const newFile = new File([file], `pasted_image_${index + 1}.png`, {
-						type: file.type,
-						lastModified: Date.now()
-					});
-					files.push(newFile);
-				}
-			});
-
-			if (files.length > 0) {
-				this.processFiles(fieldId, files);
-			}
-		}
-		/***************************************************************
-		 *
-		 *  Information Handling
-		 *
-		 ***************************************************************/
-		/**
-		 *
-		 * @param {string} uploadId the referred uploadId, as set by the this.uploads logic
-		 * @param element the target element the image is 'dropping' to
-		 * @param {boolean} isPreviewGrid
-		 * @returns {string|boolean}
-		 */
-		addImageToPost(uploadId, element, isPreviewGrid = true) {
-			let field = this.checkField(element);
-			if (!field) return false;
-
-			let upload = this.uploads.get(uploadId);
-			if (!upload) {
-				return false;
-			}
-
-			const previousLocation = this.removeImageFromCurrentLocation(uploadId, field);
-
-			if (isPreviewGrid) {
-				const postId = this.generateID();
-				const post = {
-					id: postId,
-					images: new Set([uploadId]), // This post only contains this one image
-					fields: {}
-				};
-
-				this.addImageElementTo(uploadId, element, false, postId);
-
-				// Store the individual post
-				field.posts.set(postId, post);
-				this.fields.set(field.id, field);
-
-				// Announce the move
-				if (previousLocation) {
-					this.a11y.announce(`Image moved from ${previousLocation} back to main area`);
-				}
-
-				// Cache the field data
-				this.cachePostData(field.id);
-				return postId;
-			} else {
-				let post = this.getPostDataFromElement(element);
-
-				post.images.add(uploadId);
-
-				// Handle .empty-group drops by creating the group element here
-				if (element.classList.contains('empty-group')) {
-					element = this.createPostElement(field.id, post.id);
-				}
-				this.addImageElementTo(uploadId, element);
-				element.dataset.postId = post.id;
-
-				// Announce the move
-				const groupNumber = Array.from(field.posts.keys()).indexOf(post.id) + 1;
-				if (previousLocation === 'preview') {
-					this.a11y.announce(`Image moved from main area to group ${groupNumber}`);
-				} else if (previousLocation) {
-					this.a11y.announce(`Image moved from ${previousLocation} to group ${groupNumber}`);
-				} else {
-					this.a11y.announce(`Image added to group ${groupNumber}`);
-				}
-
-				//update field data
-				field.posts.set(post.id, post);
-				this.fields.set(field.id, field);
-				this.cachePostData(field.id);
-
-				return post.id;
-			}
-		}
-
-		/**
-		 * Remove an image from its current location (preview grid or another group)
-		 * @param {string} uploadId - The upload ID to remove
-		 * @param {Object} field - The field object
-		 * @returns {string|null} - Description of where image was removed from
-		 */
-		removeImageFromCurrentLocation(uploadId, field) {
-			// Find which post/group currently contains this image
-			let currentPostId = null;
-			let currentPost = null;
-			for (const [postId, post] of field.posts) {
-				if (post.images.has(uploadId)) {
-					currentPostId = postId;
-					currentPost = post;
-					break;
-				}
-			}
-
-			console.log('current post id: ', currentPostId);
-			console.log('current post: ', currentPost);
-
-			if (currentPostId && currentPost) {
-				// Remove from the current post
-				currentPost.images.delete(uploadId);
-
-				// Find the DOM element
-				let item = document.querySelector(`[data-upload-id="${uploadId}"]`);
-
-				if (!item) {
-					console.warn(`Element not found for upload ${uploadId} - may have been moved already`);
-					// Still clean up the data structure even if DOM element is missing
-					if (currentPost.images.size === 0) {
-						this.removeEmptyGroup(currentPostId, field);
-					} else {
-						field.posts.set(currentPostId, currentPost);
-					}
-					this.cachePostData(field.id);
-					return 'unknown location';
-				}
-
-				let parent = item.closest('.upload-group, .item-grid.preview');
-				let type = (parent && parent.classList.contains('preview')) ? 'preview' : 'group ' + (Array.from(field.posts.keys()).indexOf(currentPostId) + 1);
-
-				if (currentPost.images.size === 0) {
-					this.removeEmptyGroup(currentPostId, field);
-				} else {
-					field.posts.set(currentPostId, currentPost);
-				}
-
-				// Remove the DOM element
-				item.remove();
-				this.cachePostData(field.id);
-
-				return type;
-			}
-
-			return null; // Image wasn't found in any location
-		}
-
-		/**
-		 * Remove an empty group from the field
-		 * @param {string} postId - The post ID of the group to remove
-		 * @param {Object} field - The field object
-		 */
-		removeEmptyGroup(postId, field) {
-			// Remove from data structure
-			field.posts.delete(postId);
-
-			// Remove DOM element
-			const groupElement = field.container.querySelector(`[data-post-id="${postId}"]`);
-			if (groupElement) {
-				groupElement.remove();
-				this.a11y.announce('Empty group removed');
-			}
-
-			// Update field
-			this.fields.set(field.id, field);
-			this.cachePostData(field.id);
-		}
-
-
-		checkField(element) {
-			let fieldId = this.getFieldId(element);
-			return this.fields.get(fieldId);
-		}
-
-		getPostDataFromElement(element) {
-			let field = this.checkField(element);
-			if (!field) return;
-
-			let postId = element.dataset.postId??element.closest('.upload-group').dataset.postId??field.postId??field.termId;
-			let post;
-
-			//If this isn't a groupable post, we can just add the information to  the post id
-			if (postId && !field.posts.has(postId)) {
-				post = {
-					id: postId,
-					images: new Set(),
-					fields: {}
-				};
-			} else if (postId && field.posts.has(postId)) {
-				post = field.posts.get(postId);
-			} else {
-				post = this.createPost(element);
-			}
-
-			return post;
-		}
-
-		removeImageFromPost(element, uploadId) {
-			let field = this.checkField(element);
-			if (!field) return;
-
-			let postId = element.dataset.postId;
-			if (!postId) {
-				postId = this.getPostIdFromUpload(field, uploadId);
-				if (!postId) {
-					console.warn('Could not find post for upload:', uploadId);
-					return;
-				}
-			}
-
-			const post = field.posts.get(postId);
-			if (!post) return;
-
-			// Remove from data structure
-			post.images.delete(uploadId);
-
-			// Remove DOM element
-			const imageElement = element.closest('.upload-group, .item-grid.preview').querySelector(`[data-upload-id="${uploadId}"]`);
-			if (imageElement) {
-				imageElement.remove();
-			}
-
-			// If group is empty, remove it; otherwise update it
-			if (post.images.size === 0) {
-				this.removeEmptyGroup(postId, field);
-			} else {
-				// Update the post data and move image back to preview
-				field.posts.set(postId, post);
-				this.addImageToPost(uploadId, field.previewGrid, true);
-				this.fields.set(field.id, field);
-				this.cachePostData(field.id);
-			}
-
-			return true;
-		}
-
-		removePost(element, cache = true) {
-			element = element.closest('.upload-group, .upload-item') ?? element;
-			let field = this.getField(element);
-			let postId = element.dataset.postId;
-
-			if (field.posts.has(postId)) {
-				let post = field.posts.get(postId);
-				post.images.forEach(uploadId => {
-					let upload = this.uploads.get(uploadId);
-					if (upload) {
-						this.addImageToPost(uploadId, field.previewGrid, true);
-					}
-				});
-				field.posts.delete(postId);
-				this.fields.set(field.id, field);
-				if (cache) {
-					this.cachePostData(field.id);
-				}
-			}
-			element.remove();
-			this.a11y.announce('Group deleted and images moved back to main area');
-		}
-
-		getPostIdFromUpload(field, uploadId) {
-			for (const [id, post] of field.posts) {
-				if (post.images.has(uploadId)) {
-					return id;
-				}
-			}
-			return null;
-		}
-
-		addMetaToImage(element) {
-			let field = this.checkField(element);
-			if (!field) return;
-
-			let item = element.closest('.upload-item');
-			let uploadId = item?.dataset.uploadId;
-
-			if (!uploadId) return;
-
-			const upload = this.uploads.get(uploadId);
-			if (!upload) return;
-			if (!upload.meta) {
-				upload.meta = {};
-			}
-
-			upload.meta[element.name] = element.value;
-			this.uploads.set(uploadId, upload);
-
-			this.cacheUpload(upload);
-		}
-
-		addMetaToPost(element) {
-			console.log('Adding meta to post: ');
-			let field = this.checkField(element);
-			console.log('Field:', field);
-			if (!field || field.type !== 'groupable') return;
-
-			let post = this.getPostDataFromElement(element);
-			console.log('Post: ',post);
-
-			console.log('element: ', element);
-
-			post.fields[element.name] = element.value;
-			field.posts.set(post.id, post);
-			this.fields.set(field.id, field);
-			this.cachePostData(field.id);
-			return post.id;
-		}
-
-		createPost(element) {
-			let id = this.generateID();
-
-			if (!element.classList.contains('empty-group')) {
-				element.dataset.postId = id;
-			}
-
-			const post = {
-				id: id,
-				images: new Set(),
-				fields: {}
-			};
-
-			// Cache the new post immediately
-			const field = this.checkField(element);
-			if (field) {
-				field.posts.set(id, post);
-				this.fields.set(field.id, field);
-				this.cachePostData(field.id);
-			}
-
-			return post;
-		}
-
-		createPostFromSelection(element) {
-			const fieldId = this.getFieldId(element);
-			const selected = this.getSelectedUploads(element);
-
-			if (selected.length === 0) {
-				this.notify('No uploads selected', 'warning');
-				return;
-			}
-
-			// Create new post element
-			const postElement = this.createPostElement(fieldId);
-
-			selected.forEach(uploadId => {
-				this.addImageToPost(uploadId, postElement, false);
-				// Clear selection
-				const uploadItem = document.querySelector(`[data-upload-id="${uploadId}"]`);
-				const checkbox = uploadItem?.querySelector('[name*="select-item"]');
-				if (checkbox) checkbox.checked = false;
-			});
-
-			this.updateSelectAll(element);
-			this.a11y.announce(`Created new group with ${selected.length} images.`);
-		}
-
-		async submitPostData(element) {
-			let field = this.checkField(element);
-			if (!field) return;
-
-			let length = field.posts.size;
-
-			const operation = {
-				endpoint: 'uploads/groups',
-				method: "POST",
-				title: `Uploading ${length} ${field.plural}`,
-				popup: `Sending ${length} ${field.plural} to server...`,
-				canMerge: true,
-				type: 'image_groups',
-				headers: { 'action_nonce': jvbSettings.dash },
-				user: jvbSettings.currentUser,
-				field: field.id,
-				onComplete: (operation) => this.handleFinalCompletion(operation)
-			};
-
-			// Convert Map to plain object with proper structure
-			let data = {};
-			let dependencies = new Set();
-			let index = 0;
-			data.posts = {};
-			for (const [postId, post] of field.posts) {
-				let images = [];
-				for (const uploadId of post.images) {
-					const upload = this.uploads.get(uploadId);
-					if (upload) {
-						images.push({
-							upload_id: uploadId,
-							meta: upload.meta,
-							operationId: upload.operationId
-						});
-						dependencies.add(upload.operationId);
-					}
-				}
-
-				data.posts[index] = {
-					...post,
-					images: images
-				};
-				index++;
-			}
-
-			data.content = field.content;
-
-			operation.data = data;
-			operation['depends_on'] = [...dependencies];
-
-			try {
-				await this.queue.addToQueue(operation);
-				this.notify(`Sent ${field.plural} to server`);
-			} catch (error) {
-				throw error;
-			}
-		}
-
-		handleFinalCompletion	(operation) {
-			if(operation.field) {
-				let field = this.fields.get(operation.field);
-				if (field.onGroupingComplete && typeof field.onGroupingComplete === "function") {
-					field.onGroupingComplete(operation);
-				}
-				field.container.querySelector('.group-display').hidden = true;
-				field.container.closest('details').open = false;
-				this.cleanField(operation.field);
-			}
-
-		}
-		cleanField(fieldId) {
-			let field = this.fields.get(fieldId);
-			if(!field) return;
-			if (field.previewGrid) {
-				window.removeChildren(field.previewGrid);
-			}
-			if (field.groupsContainer) {
-				window.removeChildren(field.groupsContainer);
-			}
-
-			this.clearFieldCache(fieldId);
-		}
-		/***************************************************************
-		 *
-		 * 	UI HANDLING
-		 *
-		***************************************************************/
-		addImageElementTo(upload, element, prepend = true, postId = null) {
-			upload = (typeof upload === 'string') ? this.uploads.get(upload) : upload;
-			if (!upload) {
-				console.warn('Upload not found:', upload);
-				return;
-			}
-
-			let image = window.getTemplate('uploadItem');
-			if (!image) {
-				console.error('uploadItem template not found');
-				return;
-			}
-
-			image.dataset.uploadId = upload.id;
-			if (postId) {
-				image.dataset['postId'] = postId;
-			} else {
-				image.querySelector('.item-actions').remove();
-				let groupActions = window.getTemplate('groupActions');
-				groupActions.querySelector('input[name="featured"]').value = upload.id;
-				image.querySelector('.actions').append(groupActions);
-
-			}
-
-			const img = image.querySelector('img');
-			if (img) {
-				img.src = upload.preview;
-				img.alt = upload.originalFile?.name ?? upload.meta?.originalName ?? 'Unknown File';
-			}
-
-			// Safely add metadata template
-			const details = image.querySelector('details');
-			if (details) {
-				const metaTemplate = window.getTemplate('uploadMeta');
-				if (metaTemplate) {
-					details.append(metaTemplate);
-				}
-			}
-
-			let field = this.getField(element);
-			if (field && field.type === 'groupable') {
-				image.draggable = true;
-			}
-
-
-
-			// Update input IDs safely
-			image.querySelectorAll('input').forEach(input => {
-				let id = input.id;
-				if (id) {
-					let newId = id + upload.id;
-					let label = input.parentNode.querySelector(`label[for="${id}"]`);
-					input.id = newId;
-					if (label) {
-						label.htmlFor = newId;
-					}
-				}
-			});
-
-			if (prepend) {
-				element.prepend(image);
-			} else {
-				element.append(image);
-			}
-
-			this.updateImageUI(upload.id, element);
-		}
-		removeImageElementFrom(uploadId, element, moveBackToPreview = null) {
-			element = (typeof element === 'string') ? field.container.querySelector(element) : element;
-			element.querySelector(`[data-upload-id=${uploadId}]`).remove();
-
-			if (moveBackToPreview) {
-				this.addImageToPost(uploadId, this.getField(element).previewGrid, true);
-			}
-		}
-		updateImageUI(uploadId, element = null) {
-			const upload = this.uploads.get(uploadId);
-			if (!upload) return;
-
-			element = (element) ? element : this.getField(document.querySelector(`[data-upload-id="${uploadId}"]`))?.container;
-			if (!element) return;
-
-			const item = element.querySelector(`[data-upload-id="${uploadId}"]`);
-			if (!item) return;
-
-			item.dataset.status = upload.status;
-
-			// Safely update progress elements
-			if (upload.progress) {
-				const fillElement = item.querySelector('.fill');
-				const textElement = item.querySelector('.details');
-
-				if (fillElement) {
-					fillElement.style.width = `${upload.progress.percent || 0}%`;
-				}
-
-				if (textElement) {
-					textElement.textContent = upload.progress.message ?? '';
-				}
-			}
-
-			// Safely update status indicator
-			const statusElement = item.querySelector('.status');
-			if (statusElement && !statusElement.classList.contains(upload.status)) {
-				window.removeChildren(statusElement);
-				statusElement.className = `status ${upload.status}`;
-				statusElement.append(this.getStatusIcon(upload.status));
-			}
-
-			// Safely hide/show progress
-			const progressElement = item.querySelector('.progress');
-			if (progressElement) {
-				progressElement.hidden = ['completed'].includes(upload.status);
-			}
-		}
-
-		createPostElement(fieldId, id = null) {
-			let field = this.fields.get(fieldId);
-			if (!field) return;
-
-			let post = window.getTemplate('imageGroup');
-			const postId = id || this.generateID();
-
-			post.dataset.fieldId = fieldId;
-			post.dataset.postId = postId;
-
-			let meta = post.querySelector('.fields');
-			let fields = window.getTemplate('groupMetadata');
-			meta.append(fields);
-
-			field.groupsContainer.insertBefore(post, field.groupsContainer.querySelector('.empty-group').nextElementSibling);
-
-			// Return the grid element, not the post container
-			return field.container.querySelector(`[data-post-id="${postId}"] .item-grid`);
-		}
-		/**
-		 * Hide the uploader drop zone if we have reached our limit
-		 */
-		maybeLockUploads(fieldId) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			// Hide/show drop zone based on file count
-			if (field.dropZone) {
-				const isAtCapacity = field.uploads && field.uploads.size >= field.maxFiles;
-				field.dropZone.style.hidden = isAtCapacity;
-			}
-		}
-
-		/***************************************************************
-		 *
-		 * Image Processing
-		 *
-		 **************************************************************/
-		/**
-		 * Process files for a specific field
-		 */
-		async processFiles(fieldId, files) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			// Validate files
-			const validFiles = files.filter(file => this.validateFile(file, field));
-			if (validFiles.length === 0) return;
-
-			// Check field limits
-			if (!this.checkFieldLimits(fieldId, validFiles.length)) {
-				this.notify(`Cannot add ${validFiles.length} files. Field limit exceeded.`, 'warning');
-				return;
-			}
-
-			const processedUploads = await this.processBatch(fieldId, validFiles, {
-				batchSize: 3, // Process 3 files simultaneously
-				showProgress: true,
-			});
-			this.maybeLockUploads(fieldId);
-
-			if (field.groupDisplay) {
-				field.groupDisplay.hidden = false;
-			}
-			await this.queueUpload(fieldId);
-
-
-
-			this.a11y.announce(`Processed ${processedUploads.length} of ${validFiles.length} files`);
-		}
-
-		/**
-		 * Process a single file
-		 */
-		async processFile(fieldId, file) {
-			const field = this.fields.get(fieldId);
-
-			try {
-				let upload = this.storeUpload(fieldId, file);
-				let uploadId = upload.id;
-				if (!field.uploads) field.uploads = new Set();
-				field.uploads.add(uploadId);
-
-				this.addImageToPost(uploadId, field.previewGrid, true);
-
-				// Process image with better error context
-				upload.processedFile = await this.processImage(file, {
-					uploadId,
-					retries: 2
-				});
-
-				upload.status = 'processed';
-
-				if (upload.preview && upload.preview.startsWith('blob:')) {
-					URL.revokeObjectURL(upload.preview);
-				}
-
-				// Create new preview URL for processed file
-				upload.preview = URL.createObjectURL(upload.processedFile);
-
-				this.cacheUpload(upload);
-				this.updateImageUI(uploadId);
-
-				// Announce completion (only for small batches)
-				if (this.uploads.size <= 3) {
-					this.a11y.announce(`${file.name} processed and ready`);
-				}
-
-				return upload;
-
-			} catch (error) {
-				this.updateUploadStatus(uploadId, 'error', 'Processing failed');
-
-				// Enhanced error context for batch processing
-				this.error.log(error, {
-					component: 'UploadManager',
-					action: 'processFile',
-					uploadId,
-					fileName: file.name,
-					fileSize: file.size,
-					batchProcessing: true
-				});
-
-				// Don't spam error announcements for large batches
-				if (this.uploads.size <= 3) {
-					this.a11y.announce(`${file.name} processing failed`);
-				}
-
-				return null;
-			}
-		}
-
-		/**
-		 * Stores file in our uploads Map
-		 * @param fieldId
-		 * @param file
-		 * @returns {object}
-		 */
-		storeUpload(fieldId, file) {
-			let uploadId = this.generateUploadId();
-			const upload = {
-				id: uploadId,
-				fieldId: fieldId,
-				originalFile: file,
-				status: 'processing',
-				progress: { percent: 0, message: 'Processing...' },
-				preview: URL.createObjectURL(file),
-				createdAt: Date.now(),
-				meta: {
-					originalName: file.name,
-					originalType: file.type,
-					originalSize: file.size
-				}
-			};
-
-			this.uploads.set(uploadId, upload);
-			return upload;
-		}
-
-		async processImage(file, options = {}) {
-			if (!file.type.startsWith('image/')) {
-				return file;
-			}
-
-			const startTime = performance.now();
-			this.performanceMonitor?.startTiming(options.uploadId, 'processing');
-
-			try {
-				const maxDimension = this.getMaxDimension();
-				const quality = options.quality || 0.85;
-
-				let processedFile;
-
-				if (this.shouldUseWorker(file) && this.compressionWorker) {
-					processedFile = await this.processWithWorker(file, maxDimension, quality);
-				} else {
-					processedFile = await this.processOnMainThread(file, maxDimension, quality);
-				}
-
-				if (!this.isValidProcessedFile(processedFile, file)) {
-					console.warn(`Processing failed for ${file.name}, using original`);
-					return file;
-				}
-
-				const processingTime = performance.now() - startTime;
-
-				this.performanceMonitor?.endTiming(options.uploadId, 'processing');
-				return processedFile;
-
-			} catch (error) {
-				this.performanceMonitor?.endTiming(options.uploadId, 'processing');
-
-				// Let ErrorHandler deal with the error classification and user notification
-				this.error.log(error, {
-					component: 'UploadManager',
-					action: 'processImage',
-					fileName: file.name,
-					fileSize: file.size,
-					fileType: file.type,
-					uploadId: options.uploadId
-				});
-
-				return file; // Fallback to original
-			}
-		}
-
-		/**
-		 * Validate processed file
-		 */
-		isValidProcessedFile(processedFile, originalFile) {
-			if (!processedFile || !(processedFile instanceof File)) {
-				return false;
-			}
-
-			if (processedFile.size === 0) {
-				return false;
-			}
-
-			// Check if processing actually reduced file size (for large files)
-			if (originalFile.size > 2 * 1024 * 1024 && processedFile.size >= originalFile.size) {
-				console.warn(`Processing didn't reduce file size: ${originalFile.size} → ${processedFile.size}`);
-				// Still valid, just not optimal
-			}
-
-			return true;
-		}
-
-
-		/**
-		 * Process image on main thread with better error handling
-		 */
-		async processOnMainThread(file, maxDimension, quality) {
-			return new Promise((resolve, reject) => {
-				const img = new Image();
-				const canvas = document.createElement('canvas');
-				const ctx = canvas.getContext('2d');
-				let objectUrl = null;
-
-				const cleanup = () => {
-					img.onload = null;
-					img.onerror = null;
-					if (objectUrl) {
-						URL.revokeObjectURL(objectUrl);
-						objectUrl = null;
-					}
-					// Explicitly clean up canvas
-					canvas.width = 1;
-					canvas.height = 1;
-					ctx.clearRect(0, 0, 1, 1);
-				};
-
-				img.onload = () => {
-					try {
-						const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
-						canvas.width = width;
-						canvas.height = height;
-
-						// Enhanced image smoothing
-						ctx.imageSmoothingEnabled = true;
-						ctx.imageSmoothingQuality = 'high';
-						ctx.drawImage(img, 0, 0, width, height);
-
-						const outputFormat = this.getOptimalFormat(file);
-						const outputQuality = this.getOptimalQuality(file, quality);
-
-						canvas.toBlob(
-							(blob) => {
-								cleanup();
-								if (blob) {
-									const processedFile = new File(
-										[blob],
-										this.getProcessedFileName(file, outputFormat),
-										{ type: outputFormat, lastModified: Date.now() }
-									);
-									resolve(processedFile);
-								} else {
-									reject(new Error('Canvas toBlob failed'));
-								}
-							},
-							outputFormat,
-							outputQuality
-						);
-
-					} catch (error) {
-						cleanup();
-						reject(new Error(`Canvas processing failed: ${error.message}`));
-					}
-				};
-
-				img.onerror = () => {
-					cleanup();
-					reject(new Error(`Failed to load image: ${file.name}`));
-				};
-
-				try {
-					objectUrl = URL.createObjectURL(file);
-					img.src = objectUrl;
-				} catch (error) {
-					cleanup();
-					reject(new Error(`Failed to create object URL: ${error.message}`));
-				}
-			});
-		}
-
-		/**
-		 * Get optimal output format
-		 */
-		getOptimalFormat(file) {
-			// Keep original format for certain types
-			if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
-				return file.type;
-			}
-
-			// Use WebP if supported, otherwise JPEG
-			return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
-		}
-
-		/**
-		 * Get optimal quality setting
-		 */
-		getOptimalQuality(file, requestedQuality) {
-			// Higher quality for smaller files
-			if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
-			if (file.size < 2 * 1024 * 1024) return requestedQuality;
-
-			// Lower quality for very large files
-			return Math.min(requestedQuality, 0.8);
-		}
-
-		/**
-		 * Generate processed file name
-		 */
-		getProcessedFileName(originalFile, outputFormat) {
-			const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
-
-			const extensions = {
-				'image/webp': '.webp',
-				'image/jpeg': '.jpg',
-				'image/png': '.png',
-				'image/gif': '.gif'
-			};
-
-			return baseName + (extensions[outputFormat] || '.jpg');
-		}
-
-		/**
-		 * Get maximum dimension based on device capabilities
-		 */
-		getMaxDimension() {
-			const screenWidth = window.screen.width;
-			const devicePixelRatio = window.devicePixelRatio || 1;
-
-			// Scale based on device capabilities
-			if (screenWidth * devicePixelRatio > 2560) return 2400;
-			if (screenWidth * devicePixelRatio > 1920) return 1920;
-			return 1200;
-		}
-
-		/**
-		 * Determine if we should use Web Worker
-		 */
-		shouldUseWorker(file) {
-			// Use worker for large files or when available
-			return this.compressionWorker &&
-				file.size > 1024 * 1024 && // > 1MB
-				typeof OffscreenCanvas !== 'undefined';
-		}
-
-		async processWithWorker(file, maxDimension, quality) {
-			if (!this.compressionWorker) {
-				throw new Error('Worker not available');
-			}
-
-			return new Promise((resolve, reject) => {
-				const timeout = setTimeout(() => {
-					reject(new Error('Worker processing timeout'));
-				}, 30000); // 30 second timeout
-
-				this.compressionWorker.onmessage = (e) => {
-					clearTimeout(timeout);
-
-					if (e.data.success) {
-						const processedFile = new File(
-							[e.data.blob],
-							this.getProcessedFileName(file, e.data.format || 'image/webp'),
-							{ type: e.data.format || 'image/webp', lastModified: Date.now() }
-						);
-						resolve(processedFile);
-					} else {
-						reject(new Error(e.data.error || 'Worker processing failed'));
-					}
-				};
-
-				this.compressionWorker.onerror = (error) => {
-					clearTimeout(timeout);
-					reject(new Error(`Worker error: ${error.message}`));
-				};
-
-				// Send file to worker
-				this.compressionWorker.postMessage({
-					file: file,
-					maxDimension: maxDimension,
-					quality: quality,
-					outputFormat: this.getOptimalFormat(file)
-				});
-			});
-		}
-
-		/**
-		 * Initialize Web Worker for image compression
-		 */
-		initCompressionWorker() {
-			if (this.compressionWorker || typeof Worker === 'undefined') return;
-
-			try {
-				const workerScript = `
-				self.onmessage = async function(e) {
-					const { file, maxDimension, quality, outputFormat } = e.data;
-
-					try {
-						// Create ImageBitmap from file
-						const bitmap = await createImageBitmap(file);
-
-						// Calculate dimensions
-						const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
-						const width = Math.round(bitmap.width * scale);
-						const height = Math.round(bitmap.height * scale);
-
-						// Create OffscreenCanvas
-						const canvas = new OffscreenCanvas(width, height);
-						const ctx = canvas.getContext('2d');
-
-						// Draw and resize
-						ctx.imageSmoothingEnabled = true;
-						ctx.imageSmoothingQuality = 'high';
-						ctx.drawImage(bitmap, 0, 0, width, height);
-
-						// Convert to blob
-						const blob = await canvas.convertToBlob({
-							type: outputFormat,
-							quality: quality
-						});
-
-						self.postMessage({
-							success: true,
-							blob: blob,
-							format: outputFormat
-						});
-
-					} catch (error) {
-						self.postMessage({
-							success: false,
-							error: error.message
-						});
-					}
-				};
-			`;
-
-				const blob = new Blob([workerScript], { type: 'application/javascript' });
-				this.compressionWorker = new Worker(URL.createObjectURL(blob));
-
-			} catch (error) {
-				console.warn('Failed to initialize compression worker:', error);
-				this.compressionWorker = null;
-			}
-		}
-
-		async processBatch(fieldId, files, options = {}) {
-			const {
-				batchSize = 3,
-				showProgress = true,
-				onBatchComplete = null,
-				delayBetweenBatches = 100
-			} = options;
-
-			const results = [];
-			const totalFiles = files.length;
-			let processedCount = 0;
-
-			// Show initial progress
-			if (showProgress) {
-				this.updateUploadProgress(fieldId, 0, totalFiles, 'Starting batch processing...');
-			}
-
-			// Process files in batches
-			for (let i = 0; i < files.length; i += batchSize) {
-				const batch = files.slice(i, i + batchSize);
-
-				// Process current batch (parallel processing within batch)
-				const batchPromises = batch.map(async (file, index) => {
-					try {
-						const upload = await this.processFile(fieldId, file);
-
-						// Update progress for individual file
-						processedCount++;
-						if (showProgress) {
-							this.updateUploadProgress(
-								fieldId,
-								processedCount,
-								totalFiles,
-								`Processed ${file.name}`
-							);
-						}
-
-						return upload;
-					} catch (error) {
-						console.error(`Failed to process file ${file.name}:`, error);
-						processedCount++; // Still count as processed (failed)
-
-						if (showProgress) {
-							this.updateUploadProgress(
-								fieldId,
-								processedCount,
-								totalFiles,
-								`Failed: ${file.name}`
-							);
-						}
-
-						return null; // Return null for failed files
-					}
-				});
-
-				// Wait for current batch to complete
-				const batchResults = await Promise.all(batchPromises);
-
-				// Filter out null results (failed files)
-				const successfulUploads = batchResults.filter(upload => upload !== null);
-				results.push(...successfulUploads);
-
-				// Call batch completion callback
-				if (onBatchComplete) {
-					onBatchComplete(successfulUploads);
-				}
-
-				// Small delay between batches to prevent browser overload
-				if (i + batchSize < files.length && delayBetweenBatches > 0) {
-					await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
-				}
-			}
-
-			// Final progress update
-			if (showProgress) {
-				this.updateUploadProgress(
-					fieldId,
-					totalFiles,
-					totalFiles,
-					`Completed! ${results.length}/${totalFiles} files processed successfully`
-				);
-
-				// Hide progress after a delay
-				setTimeout(() => {
-					this.hideUploadProgress(fieldId);
-				}, 2000);
-			}
-
-			return results;
-		}
-
-		updateUploadProgress(fieldId, current, total, message) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			let progressBar = field.container.querySelector('.progress');
-
-			// Create progress bar if it doesn't exist
-			if (!progressBar) {
-				progressBar = window.getTemplate('imageProgress');
-
-				// Insert after drop zone or at top of container
-				const insertAfter = field.dropZone || field.container.firstElementChild;
-				if (insertAfter) {
-					insertAfter.insertAdjacentElement('afterend', progressBar);
-				} else {
-					field.container.prepend(progressBar);
-				}
-			}
-
-			// Update progress bar
-			const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
-			const progressFill = progressBar.querySelector('.fill');
-			const progressMessage = progressBar.querySelector('.details');
-			const progressCount = progressBar.querySelector('.count');
-
-			if (progressFill) {
-				progressFill.style.width = `${progressPercent}%`;
-			}
-
-			if (progressMessage) {
-				progressMessage.textContent = message;
-			}
-
-			if (progressCount) {
-				progressCount.textContent = `${current}/${total}`;
-			}
-
-			// Add completion styling
-			if (current === total) {
-				progressBar.classList.add('completed');
-			}
-		}
-
-		hideUploadProgress(fieldId) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			const progressBar = field.container.querySelector('.progress');
-			if (progressBar) {
-				progressBar.style.opacity = '0';
-				setTimeout(() => {
-					progressBar.remove();
-				}, 300);
-			}
-		}
-
-		/**
-		 * Calculate optimal dimensions with aspect ratio preservation
-		 */
-		calculateOptimalDimensions(img, maxDimension) {
-			let { width, height } = img;
-
-			// Don't upscale
-			if (width <= maxDimension && height <= maxDimension) {
-				return { width, height };
-			}
-
-			// Calculate scale factor
-			const scale = Math.min(maxDimension / width, maxDimension / height);
-
-			return {
-				width: Math.round(width * scale),
-				height: Math.round(height * scale)
-			};
-		}
-
-
-		/**
-		 * Check WebP support
-		 */
-		supportsWebP() {
-			const canvas = document.createElement('canvas');
-			return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
-		}
-
-
-		/*****************************************
-		 *
-		 * TOUCH, DRAG, and DROP
-		 *
-		 * Shared handlers, then individual listener handlers
-		 *
-		 ****************************************/
-		startDragOperation(config) {
-			const {
-				primaryElement,
-				sourceType,
-				startPosition,
-				event
-			} = config;
-
-			const uploadId = this.getUploadId(primaryElement);
-			const fieldId = this.getFieldId(primaryElement);
-
-			// Determine what items to drag
-			const draggedItems = this.getDraggedItems(primaryElement);
-
-			// Initialize drag state
-			this.dragState = {
-				primaryItem: uploadId,
-				draggedItems: draggedItems,
-				isDragging: true,
-				isMultiDrag: draggedItems.length > 1,
-				fieldId: fieldId,
-				sourceType: sourceType,
-				startTime: Date.now(),
-				startPosition: startPosition,
-				currentPosition: startPosition,
-				currentTarget: null,
-				validTarget: null,
-				dragPreview: null,
-				touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
-				touchMoved: false
-			};
-
-			// Create drag preview
-			this.createDragPreview(primaryElement);
-
-			// Apply dragging state
-			this.applyDraggingState(true);
-
-			const announceText = this.dragState.isMultiDrag
-				? `Started dragging ${draggedItems.length} items`
-				: 'Started dragging item';
-
-			this.a11y.announce(announceText);
-			this.provideDragFeedback('start');
-
-			return true;
-		}
-
-		updateDragOperation(position, elementUnderPointer) {
-			if (!this.dragState.isDragging) return;
-
-			const { sourceType, startPosition } = this.dragState;
-
-			// Update position
-			this.dragState.currentPosition = position;
-
-			// Check for significant movement (touch)
-			if (sourceType === 'touch' && !this.dragState.touchMoved) {
-				const deltaX = Math.abs(position.x - startPosition.x);
-				const deltaY = Math.abs(position.y - startPosition.y);
-
-				if (deltaX > 10 || deltaY > 10) {
-					this.dragState.touchMoved = true;
-				}
-			}
-
-			// Update preview and target
-			this.updateDragPreview(position);
-			this.updateDropTarget(elementUnderPointer);
-		}
-
-		endDragOperation(elementUnderPointer = null) {
-			if (!this.dragState.isDragging) return;
-
-			const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
-				this.dragState.validTarget;
-
-			// Process drop if valid - but only here, not in handleDrop
-			if (wasSuccessful && this.dragState.validTarget) {
-				this.processItemDrop({
-					itemIds: this.dragState.draggedItems,
-					targetElement: this.dragState.validTarget,
-					fieldId: this.dragState.fieldId,
-					dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
-					sourceType: this.dragState.sourceType
-				});
-			}
-
-			// Cleanup
-			this.cleanupDragOperation();
-
-			const announceText = wasSuccessful
-				? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
-				: 'Drag cancelled';
-
-			this.a11y.announce(announceText);
-		}
-
-		/**
-		 * Shared method to process any drop operation (drag or touch)
-		 * @param {Object} dropData - Standardized drop data
-		 * @returns {boolean} Success status
-		 */
-		processItemDrop(dropData) {
-			const {
-				itemIds,
-				targetElement,
-				fieldId,
-				dropType,
-				sourceType
-			} = dropData;
-
-			if (!itemIds?.length || !targetElement || !fieldId) {
-				return false;
-			}
-
-			// Determine if it's a preview drop
-			let isPreviewDrop = targetElement.classList.contains('item-grid') && targetElement.classList.contains('preview');
-
-			// Handle empty group drops by creating the group element
-			let actualTarget = targetElement;
-			if (targetElement.classList.contains('empty-group')) {
-				actualTarget = this.createPostElement(fieldId);
-				isPreviewDrop = false;
-			}
-
-			// Use existing addImageToPost method for each item
-			// This method already handles:
-			// - removeImageFromCurrentLocation (cleanup of old location)
-			// - Adding to new location
-			// - Updating field.posts data structure
-			// - Caching the data
-			itemIds.forEach(uploadId => {
-				this.addImageToPost(uploadId, actualTarget, isPreviewDrop);
-			});
-
-			// Clear selections for multi-drops
-			if (dropType === 'multiple') {
-				const field = this.fields.get(fieldId);
-				this.clearAllSelections(field);
-			}
-
-			// Announce completion
-			const announceText = dropType === 'multiple'
-				? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
-				: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
-
-			this.a11y.announce(announceText);
-			this.provideFeedback(sourceType, 'success', {
-				count: itemIds.length,
-				isMultiple: dropType === 'multiple'
-			});
-
-			return true;
-		}
-
-		clearAllSelections(field) {
-			// Clear all selection checkboxes in the entire field container
-			const allCheckboxes = field.container.querySelectorAll('[name*="select-item"]');
-			allCheckboxes.forEach(checkbox => {
-				checkbox.checked = false;
-			});
-
-			// Update the select all state
-			if (field.selectAll) {
-				field.selectAll.checked = false;
-				const label = field.selectAll.nextElementSibling;
-				if (label) {
-					label.textContent = 'Select All';
-				}
-			}
-
-			// Hide selection controls
-			if (field.selectActions) field.selectActions.hidden = true;
-			if (field.selectInfo) field.selectInfo.hidden = true;
-		}
-
-		cleanupDragOperation() {
-			if (this.dragState.dragPreview) {
-				this.dragState.dragPreview.remove();
-			}
-
-			this.applyDraggingState(false);
-			this.clearDropTargetStates();
-
-			// Reset state
-			this.dragState.isDragging = false;
-			this.dragState.dragPreview = null;
-			this.dragState.draggedItems = [];
-		}
-
-		/**
-		 * Validate if drag can start
-		 */
-		validateDragStart(element) {
-			//TODO: Likely remove. We are already only listening to [draggable] items
-			return { canDrag: true };
-		}
-
-		/**
-		 * Determine what items to drag (single or multiple selection)
-		 */
-		getDraggedItems(element) {
-			const selectedUploads = this.getSelectedUploads(element);
-			const primaryUploadId = element.dataset.uploadId;
-
-			// If we have multiple selections and primary is selected, drag all
-			if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
-				return selectedUploads;
-			}
-
-			// Otherwise, just drag the primary item
-			return [primaryUploadId];
-		}
-
-		/**
-		 * Apply/remove dragging visual state to items
-		 */
-		applyDraggingState(isDragging) {
-			this.dragState.draggedItems.forEach(uploadId => {
-				const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
-				if (element) {
-					element.classList.toggle('dragging', isDragging);
-				}
-			});
-		}
-
-		/**
-		 * Create drag preview element
-		 */
-		createDragPreview(originalElement) {
-			const { isMultiDrag, draggedItems } = this.dragState;
-
-			if (isMultiDrag) {
-				this.dragState.dragPreview = this.createMultiDragPreview(originalElement, draggedItems);
-			} else {
-				this.dragState.dragPreview = this.createSingleDragPreview(originalElement);
-			}
-
-			this.updateDragPreview(this.dragState.startPosition);
-			document.body.appendChild(this.dragState.dragPreview);
-		}
-
-		/**
-		 * Create single item drag preview
-		 */
-		createSingleDragPreview(originalElement) {
-			const preview = originalElement.cloneNode(true);
-			preview.dataset.uploadId = preview.dataset.uploadId+'-dragging';
-			this.styleDragPreview(preview, false);
-			return preview;
-		}
-
-		/**
-		 * Create multiple items drag preview
-		 */
-		createMultiDragPreview(originalElement, draggedItems) {
-			const container = document.createElement('div');
-			container.className = 'drag-preview multi-item';
-
-			// Create stacked effect with up to 3 items
-			const displayCount = Math.min(draggedItems.length, 3);
-
-			for (let i = 0; i < displayCount; i++) {
-				const uploadId = draggedItems[i];
-				const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
-
-				if (uploadElement) {
-					const stackedItem = uploadElement.cloneNode(true);
-					stackedItem.dataset.uploadId = uploadId + '_dragging';
-
-					stackedItem.style.cssText = `
-				position: absolute;
-				top: ${i * 4}px;
-				left: ${i * 4}px;
-				width: calc(100% - ${i * 4}px);
-				height: calc(100% - ${i * 4}px);
-				opacity: ${1 - (i * 0.15)};
-				transform: rotate(${(i - 1) * 2}deg);
-				z-index: ${10 - i};
-				border-radius: 4px;
-				overflow: hidden;
-			`;
-					container.appendChild(stackedItem);
-				}
-			}
-
-			// Add count badge
-			if (draggedItems.length > 1) {
-				const badge = this.createCountBadge(draggedItems.length);
-				container.appendChild(badge);
-			}
-
-			this.styleDragPreview(container, true);
-			return container;
-		}
-
-		isElementInViewport(element) {
-			const rect = element.getBoundingClientRect();
-			return (
-				rect.top >= 0 &&
-				rect.left >= 0 &&
-				rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
-				rect.right <= (window.innerWidth || document.documentElement.clientWidth)
-			);
-		}
-
-
-		/**
-		 * Style drag preview element
-		 */
-		styleDragPreview(element, isMulti) {
-			const primaryItem = document.querySelector(`[data-upload-id="${this.dragState.primaryItem}"]`);
-			const rect = primaryItem?.getBoundingClientRect();
-
-			element.className = `drag-preview${isMulti ? ' multi-item' : ''}`;
-
-			// For multi-item previews, use a consistent size rather than original element size
-			const previewSize = isMulti ? { width: 120, height: 120 } : {
-				width: rect?.width || 100,
-				height: rect?.height || 100
-			};
-
-			element.style.cssText = `
-		position: fixed;
-		top: ${rect?.top || 0}px;
-		left: ${rect?.left || 0}px;
-		width: ${previewSize.width}px;
-		height: ${previewSize.height}px;
-		pointer-events: none;
-		z-index: 9999;
-		opacity: 0.8;
-		transform: scale(1.05);
-		transition: none;
-		box-shadow: 0 8px 25px rgba(0,0,0,0.3);
-		border-radius: 8px;
-	`;
-		}
-		/**
-		 * Update drag preview position
-		 */
-		updateDragPreview(position) {
-			if (!this.dragState.dragPreview) return;
-
-			// Calculate offset based on preview type and source
-			let offset;
-			if (this.dragState.sourceType === 'touch') {
-				offset = this.dragState.isMultiDrag ? { x: -60, y: -80 } : { x: -50, y: -60 };
-			} else {
-				offset = this.dragState.isMultiDrag ? { x: 15, y: 15 } : { x: 10, y: 10 };
-			}
-
-			const deltaX = position.x - this.dragState.startPosition.x;
-			const deltaY = position.y - this.dragState.startPosition.y;
-
-			this.dragState.dragPreview.style.transform = `translate(${deltaX + offset.x}px, ${deltaY + offset.y}px) scale(1.05)`;
-		}
-
-		/**
-		 * Update drop target highlighting
-		 */
-		updateDropTarget(elementUnderPointer) {
-			// Clear previous target
-			if (this.dragState.currentTarget) {
-				this.clearDropTargetState(this.dragState.currentTarget);
-			}
-
-			// Find valid drop target
-			const validTarget = this.findValidDropTarget(elementUnderPointer);
-
-			// Update state
-			this.dragState.currentTarget = elementUnderPointer;
-			this.dragState.validTarget = validTarget;
-
-			// Apply visual feedback
-			if (validTarget) {
-				this.applyDropTargetState(validTarget);
-
-				// Haptic feedback for touch
-				if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
-					const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
-					navigator.vibrate(pattern);
-				}
-			}
-		}
-
-		/**
-		 * Find valid drop target from element
-		 */
-		findValidDropTarget(element) {
-			if (!element) return null;
-
-			const postContainer = element.closest('.item-grid.group, .empty-group, .item-grid.preview');
-			if (postContainer) {
-				const fieldId = this.getFieldId(postContainer);
-				if (fieldId === this.dragState.fieldId) {
-					return postContainer;
-				}
-			}
-
-			return null;
-		}
-
-		/**
-		 * Apply drop target visual state
-		 */
-		applyDropTargetState(target) {
-			target.classList.add('dragover');
-
-			if (this.dragState.isMultiDrag) {
-				target.classList.add('multi-drop');
-				target.setAttribute('data-item-count', this.dragState.draggedItems.length);
-			}
-		}
-
-		/**
-		 * Clear drop target state from element
-		 */
-		clearDropTargetState(target) {
-			target.classList.remove('dragover', 'multi-drop');
-			target.removeAttribute('data-item-count');
-		}
-
-		/**
-		 * Clear all drop target states
-		 */
-		clearDropTargetStates() {
-			document.querySelectorAll('.dragover').forEach(el => {
-				el.classList.remove('dragover', 'multi-drop');
-				el.removeAttribute('data-item-count');
-			});
-		}
-
-		/**
-		 * Create count badge for multi-item preview
-		 */
-		createCountBadge(count) {
-			const badge = document.createElement('div');
-			badge.className = 'selection-count-badge';
-			badge.textContent = count.toString();
-			badge.style.cssText = `
-			position: absolute;
-			top: -8px;
-			right: -8px;
-			background: var(--accent-primary);
-			color: white;
-			border-radius: 50%;
-			width: 24px;
-			height: 24px;
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			font-size: 12px;
-			font-weight: bold;
-			box-shadow: 0 2px 8px rgba(0,0,0,0.3);
-			z-index: 20;
-		`;
-			return badge;
-		}
-
-		/**
-		 * Provide feedback for drag operations
-		 */
-		provideDragFeedback(type) {
-			const hapticPatterns = {
-				start: [50],
-				success: this.dragState.isMultiDrag ? [50, 25, 50, 25, 50] : [50, 25, 50],
-				cancel: [100]
-			};
-
-			if (this.dragState.sourceType === 'touch' && navigator.vibrate && hapticPatterns[type]) {
-				navigator.vibrate(hapticPatterns[type]);
-			}
-		}
-
-		/**
-		 * Provide consistent feedback for different input methods
-		 */
-		provideFeedback(sourceType, feedbackType, data = {}) {
-			const hapticPatterns = {
-				success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
-				error: [100, 50, 100]
-			};
-
-			if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
-				navigator.vibrate(hapticPatterns[feedbackType]);
-			}
-		}
-
-		handleExternalFileDrop(e, files) {
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer && files.length > 0) {
-				const fieldId = this.getFieldId(uploadContainer);
-				if (fieldId) {
-					this.processFiles(fieldId, files);
-					this.a11y.announce(`${files.length} file(s) dropped for upload`);
-				}
-			}
-		}
-
-		clearDragoverStates() {
-			document.querySelectorAll('.dragover').forEach(el => {
-				el.classList.remove('dragover', 'multi-drop');
-				el.removeAttribute('data-item-count');
-			});
-		}
-		/*********
-		 *  DRAG HANDLERS
-		 ********/
-		handleDragEnter(e) {
-			if (!window.targetCheck(e, '.image.field')) return;
-
-			// Only handle external files
-			if (e.dataTransfer.types.includes('Files')) {
-				e.preventDefault();
-				const uploadContainer = e.target.closest('.file-upload-container');
-				if (uploadContainer) {
-					uploadContainer.classList.add('dragover');
-				}
-			}
-		}
-		handleDragLeave(e) {
-			if (!window.targetCheck(e, '.image.field')) return;
-
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
-				uploadContainer.classList.remove('dragover');
-			}
-		}
-		handleDragStart(e) {
-			if (!window.targetCheck(e, '.image.field')) return;
-
-			const uploadItem = e.target.closest('[data-upload-id]');
-			if (!uploadItem) return;
-
-			const result = this.startDragOperation({
-				primaryElement: uploadItem,
-				sourceType: 'drag',
-				startPosition: { x: e.clientX, y: e.clientY },
-				event: e
-			});
-
-			if (result) {
-				e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
-				e.dataTransfer.effectAllowed = 'move';
-			} else {
-				e.preventDefault();
-			}
-		}
-
-		handleDragOver(e) {
-			if (!this.dragState.isDragging) return;
-			if (!window.targetCheck(e, '.image.field')) return;
-
-			e.preventDefault();
-			this.updateDragOperation({ x: e.clientX, y: e.clientY }, e.target);
-		}
-
-		handleDrop(e) {
-			if (!window.targetCheck(e, '.image.field')) return;
-
-			e.preventDefault();
-			this.clearDragoverStates();
-
-			// Handle external files (new uploads)
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer) {
-				const files = Array.from(e.dataTransfer.files);
-				if (files.length > 0) {
-					const fieldId = this.getFieldId(uploadContainer);
-					if (fieldId) {
-						this.processFiles(fieldId, files);
-						this.a11y.announce(`${files.length} file(s) dropped for upload`);
-					}
-				}
-				return;
-			}
-
-			// DON'T handle internal drops here - let endDragOperation handle them
-			// This prevents double processing
-		}
-
-		handleDragEnd(e) {
-			if (!this.dragState.isDragging) return;
-
-			// Find the element under the final drop position
-			const elementUnderDrop = document.elementFromPoint(
-				this.dragState.currentPosition?.x || e.clientX,
-				this.dragState.currentPosition?.y || e.clientY
-			);
-
-			this.endDragOperation(elementUnderDrop);
-		}
-		/*********
-		 * TOUCH HANDLERS
-		 ********/
-		handleTouchStart(e) {
-			if (!window.targetCheck(e, '.image.field')) return;
-			if (this.isTouchOnFormElement(e.target)) {
-				return;
-			}
-
-			const uploadItem = e.target.closest('[data-upload-id]');
-			if (!uploadItem) return;
-
-			const touch = e.touches[0];
-
-			const result = this.startDragOperation({
-				primaryElement: uploadItem,
-				sourceType: 'touch',
-				startPosition: { x: touch.clientX, y: touch.clientY },
-				event: e
-			});
-
-			if (result) {
-				e.preventDefault(); // Prevent scrolling
-			}
-		}
-
-		isTouchOnFormElement(target) {
-			// Check if target is a form element or inside one
-			const formElements = [
-				'input', 'button', 'label', 'select', 'textarea',
-				'.upload-select', '.item-select', '[type="checkbox"]',
-				'[type="radio"]', '.form-control'
-			];
-
-			return formElements.some(selector => {
-				return target.matches(selector) || target.closest(selector);
-			});
-		}
-
-		handleTouchMove(e) {
-			if (!this.dragState.isDragging) return;
-
-			e.preventDefault();
-			const touch = e.touches[0];
-			const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-			this.updateDragOperation({ x: touch.clientX, y: touch.clientY }, elementUnderTouch);
-		}
-
-		handleTouchEnd(e) {
-			if (!this.dragState.isDragging) return;
-
-			e.preventDefault();
-			const touch = e.changedTouches[0];
-			const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-			this.endDragOperation(elementUnderTouch);
-		}
-
-		handleTouchCancel(e) {
-			if (this.dragState.isDragging) {
-				this.cleanupDragOperation();
-				this.a11y.announce('Drag cancelled');
-			}
-		}
-
-
-		/*****************************************************
-		 *
-		 * Handle upload selection
-		 *
-		 ****************************************************/
-		/**
-		 * Handle select all functionality
-		 */
-		handleSelectAll(element, checked = null) {
-			const field = this.getField(element);
-			if (!field) return;
-
-			// Use element's checked state if not provided
-			if (checked === null) {
-				checked = element.checked;
-			}
-
-			const target = field.previewGrid;
-			const previewItems = target.querySelectorAll('[data-upload-id]') || [];
-
-			previewItems.forEach(item => {
-				const checkbox = item.querySelector('[name*="select-item"]');
-				if (checkbox) {
-					checkbox.checked = checked;
-				}
-			});
-
-			this.updateSelectAll(element);
-			this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
-
-			// Clear last clicked since we're selecting/deselecting all
-			this.lastClickedUpload = null;
-		}
-
-		updateSelectAll(element) {
-			const field = this.getField(element);
-			if (!field) return;
-
-			const selected = this.getSelectedUploads(element);
-			if (selected.length > 0 ) {
-				field.selectActions.hidden = false;
-				field.selectInfo.hidden = false;
-				field.selectCount.textContent = `${selected.length}`;
-			} else {
-				field.selectActions.hidden = true;
-				field.selectInfo.hidden = true;
-			}
-			let selectAll = selected.length === field.container.querySelectorAll('.item-grid.preview .upload-item').length;
-			field.selectAll.checked = selectAll;
-			field.selectAll.nextElementSibling.textContent = (selectAll) ? 'Clear Selection' : 'Select All';
-		}
-
-		getSelectedUploads(element) {
-			// Starting from the provided element, we get the closest container of items
-			let grid = element.closest(':has(.item-grid)')?.querySelector('.item-grid') ??
-				(element.classList.contains('item-grid') ? element : this.getField(element).previewGrid);
-
-			// We check for any selected checkboxes, and return an array of uploadIds
-			let uploads = [];
-			grid.querySelectorAll('[name*="select-item"]:checked').forEach(checkbox => {
-				let uploadItem = checkbox.closest('[data-upload-id]'); // FIX 6: Get the element, not ID
-				if (uploadItem) {
-					uploads.push(uploadItem.dataset.uploadId); // FIX 7: Get the ID from dataset
-				}
-			});
-			return uploads;
-		}
-
-		/**
-		 * Handle individual upload selection
-		 */
-		handleUploadSelection(element) {
-			// Update the select all state
-			this.updateSelectAll(element);
-
-			// Track this as the last clicked upload
-			this.lastClickedUpload = this.getUploadId(element);
-
-		}
-
-		handleRangeSelection(currentElement, event) {
-			const field = this.getField(currentElement);
-			if (!field) return;
-
-			const currentUploadId = this.getUploadId(currentElement);
-			if (!currentUploadId || !this.lastClickedUpload) return;
-
-			// Get all upload items in the preview grid
-			const previewGrid = field.previewGrid;
-			const allItems = Array.from(previewGrid.querySelectorAll('[data-upload-id]'));
-
-			// Find indices of first and current items
-			const firstIndex = allItems.findIndex(item =>
-				item.dataset.uploadId === this.lastClickedUpload
-			);
-			const currentIndex = allItems.findIndex(item =>
-				item.dataset.uploadId === currentUploadId
-			);
-
-			if (firstIndex === -1 || currentIndex === -1) return;
-
-			// Determine range (handle both directions)
-			const startIndex = Math.min(firstIndex, currentIndex);
-			const endIndex = Math.max(firstIndex, currentIndex);
-
-			// Select all items in range (including the clicked one!)
-			for (let i = startIndex; i <= endIndex; i++) {
-				const item = allItems[i];
-				const checkbox = item.querySelector('[name*="select-item"]');
-				if (checkbox) {
-					checkbox.checked = true;
-				}
-			}
-
-			currentElement.checked = true;
-			// Update selection UI
-			this.updateSelectAll(currentElement);
-
-			// Announce the range selection
-			const selectedCount = endIndex - startIndex + 1;
-			this.a11y.announce(`Selected ${selectedCount} items in range`);
-
-			// Update the last clicked item to the current one
-			this.lastClickedUpload = currentUploadId;
-		}
-
-		removeSelection(button) {
-			let fieldId = this.getFieldId(button);
-
-			const selectedUploads = this.getSelectedUploads(button);
-			if (selectedUploads.length === 0) {
-				this.notify('No uploads selected', 'warning');
-				return;
-			}
-
-			selectedUploads.forEach(upload => {
-				this.removeUpload(fieldId, upload);
-			});
-		}
-
-
-		/*****************************************
-		 *
-		 *		META
-		 *
-		 ****************************************/
-		/**
-		 * Schedule debounced metadata update
-		 */
-
-		/**
-		 * Send metadata update to server
-		 */
-		getUploadMeta() {
-			let meta = {};
-			this.uploads.forEach(upload => {
-				let item = {
-					id: upload.id,		//either the generated ID, or the attachment id
-					meta: upload.meta
-				};
-				if (upload.operationId) {
-					item.operationId = upload.operationId;
-				}
-				meta[upload.id] = item;
-			});
-			return meta;
-		}
-		async maybeUpdateImageMeta() {
-			let metaData = this.getUploadMeta();
-			let changes = window.getDifferences.map(this.oldUploads, metaData);
-
-
-			if (Object.keys(changes).length === 0) return;
-
-			try {
-				const operation = {
-					endpoint: 'uploads/meta',
-					method: 'POST',
-					title: 'Saving image meta',
-					popup: `Sending ${changes.length} changes to server...`,
-					canMerge: true,
-					type: 'image_meta',
-					headers: { 'action_nonce': jvbSettings.dash},
-					user: jvbSettings.currentUser,
-					field: field.id,
-					data: changes,
-				}
-
-				let dependencies = new Set();
-				for (const [uploadId, settings] of metaData) {
-					if (settings.operationId) {
-						dependencies.add(settings.operationId);
-					}
-				}
-				if (dependencies.size > 0) {
-					operation['depends_on'] = [ ... dependencies];
-				}
-
-				let operationId = this.queue.addToQueue(operation);
-
-			} catch (error) {
-				throw error;
-			} finally {
-				this.oldUploads = this.uploads;
-			}
-		}
-
-		/**
-		 * Process pending metadata when upload completes
-		 */
-		processPendingMetadata(uploadId) {
-			if (!this.metadataPending.has(uploadId)) return;
-
-			const pendingMetadata = this.metadataPending.get(uploadId);
-			if (Object.keys(pendingMetadata).length === 0) return;
-
-			// Send all pending metadata at once
-			this.sendMetadataUpdate(uploadId, pendingMetadata);
-			this.metadataPending.delete(uploadId);
-		}
-		/********************************************************************
-		 *
-		 * Queueing and Updates
-		 *
-		 *******************************************************************/
-
-		async queueUpload(fieldId) {
-			// Cache data before queuing
-			this.cachePostData(fieldId);
-
-			const field = this.fields.get(fieldId);
-			if (!field?.uploads) return;
-
-			const operationData = this.prepareUploadData(fieldId);
-
-			if (!operationData || !operationData.data) {
-				console.warn('No operation data prepared for field:', fieldId);
-				return;
-			}
-
-			// At this point, we're in the initial upload phase - just use field.uploads
-			let uploadIds = [];
-
-			if (field.uploads && field.uploads.size > 0) {
-				uploadIds = Array.from(field.uploads);
-			} else {
-				console.warn('No uploads found in field.uploads for field:', fieldId);
-			}
-
-			// Validate we have uploads to process
-			if (uploadIds.length === 0) {
-				console.warn('No uploads found to queue for field:', fieldId);
-				return;
-			}
-
-			uploadIds.forEach(upload => {
-				this.updateUploadStatus(upload, 'uploading', 'Uploading image');
-			});
-			this.a11y.announce(`Queuing for upload`);
-
-			const operation = {
-				endpoint: "uploads",
-				method: "POST",
-				data: operationData.data,
-				title: this.getOperationTitle(field, uploadIds),
-				popup: `Uploading ${uploadIds.length} file(s)...`,
-				canMerge: false,
-				type: field.groupable ? 'batch_creation' : 'image_upload',
-				headers: { 'action_nonce': jvbSettings.dash },
-				user: jvbSettings.currentUser,
-				append: '_upload',
-				onUpdate: (operation) => this.handleUpdate(operation),
-				onComplete: (operation) => this.handleCompletion(operation)
-			};
-
-			try {
-				const operationId = await this.queue.addToQueue(operation);
-
-				this.activeOperations.set(operationId, {
-					uploadIds: uploadIds,
-					fieldId: field.id,
-					status: 'queued',
-					type: field.groupable ? 'batch_creation' : 'image_upload',
-					startTime: Date.now(),
-					retryCount: 0,
-				});
-
-				// Store operation ID in uploads
-				for (const uploadId of uploadIds) {
-					const upload = this.uploads.get(uploadId);
-					upload.operationId = operationId;
-					upload.attachmentId = null;
-					this.cacheUpload(upload);
-				}
-
-				if (!field.operationId) {
-					field.operationId = new Set();
-				}
-				field.operationId.add(operationId);
-				this.fields.set(field.id, field);
-				this.cachePostData(fieldId);
-
-				this.notify(`Queued ${uploadIds.length} file(s) for upload`, 'info');
-				return operationId;
-			} catch (error) {
-				throw error;
-			}
-		}
-
-		prepareUploadData(fieldId) {
-			const field = this.fields.get(fieldId);
-			const formData = new FormData();
-
-			// Standard field metadata
-			formData.append('content', field.content);
-			formData.append('mode', field.mode);
-			formData.append('field_name', field.container.dataset.field);
-			formData.append('field_id', field.id);
-			formData.append('type', field.type);
-			if (field.container.dataset.postId) {
-				formData.append('post_id', field.container.dataset.postId);
-			} else if (field.container.dataset.termId) {
-				formData.append('term_id', field.container.dataset.termId);
-			}
-
-			let index = 0;
-
-			let uploadMap = [];
-			if (field.uploads && field.uploads.size > 0) {
-				field.uploads.forEach(uploadId => {
-					const upload = this.uploads.get(uploadId);
-					if (upload) {
-						const file = upload.processedFile || upload.originalFile;
-						formData.append(`files[${index}]`, file);
-						uploadMap.push(uploadId);
-
-						index++;
-					}
-				});
-			} else {
-				console.warn('No uploads found in field.uploads for field:', fieldId);
-			}
-
-			formData.append('upload_map', uploadMap);
-			console.log('Gathered data:');
-			for (var [key, value] of formData.entries()) {
-				console.log(key, value);
-			}
-			return {data: formData};
-		}
-
-		/**
-		 * Handle queue updates
-		 */
-		handleUpdate(queueItem) {
-			console.log('Updating item in Uploader...', queueItem);
-
-			const operation = this.activeOperations.get(queueItem.id);
-
-			if (queueItem.status !== operation.status) {
-				const mapping = this.statusMapping[queueItem.status] || {
-					status: queueItem.status,
-					message: queueItem.status
-				};
-
-				// Update progress calculation
-				const progress = this.calculateProgress(queueItem);
-				operation.status = queueItem.status;
-				operation.uploadIds.forEach(id => {
-					const upload = this.uploads.get(id);
-					upload.status = mapping.status;
-					upload.progress = {
-						percent: progress,
-						message: mapping.message,
-						serverStatus: queueItem.status
-					};
-
-					this.uploads.set(id, upload);
-					this.cacheUpload(upload);
-					this.updateImageUI(id);
-				});
-
-				if (operation.type === 'image_upload') {
-					//TODO: Gather any metadata changes
-					//TODO: Add changes to queue
-					//TODO: Remove preview items
-					//TODO: Send onUpdate to caller
-				}
-			}
-		}
-
-
-		/**
-		 * Handle operation completion
-		 */
-		handleCompletion(queueItem) {
-			console.log('Completion item: ', queueItem);
-			const operation = this.activeOperations.get(queueItem.id);
-			const mapping = this.statusMapping['completed'] || {
-				status: 'completed',
-				message: 'Everything\'s ready!'
-			};
-
-			console.log('Grabbed operation: ', operation);
-			// Update progress calculation
-			const progress = this.calculateProgress(queueItem);
-			operation.status = 'completed';
-			operation.uploadIds.forEach(id => {
-				const upload = this.uploads.get(id);
-
-				upload.status = mapping.status;
-				upload.progress = {
-					percent: progress,
-					message: mapping.message,
-					serverStatus: 'completed'
-				};
-
-				this.uploads.set(id, upload);
-				this.cacheUpload(upload);
-				this.updateImageUI(id);
-			});
-
-
-			if (operation.type === 'image_upload') {
-				//TODO: Gather any metadata changes
-				//TODO: Add changes to queue
-				//TODO: Remove preview items
-				//TODO: Send onUpdate to caller
-			}
-			// const operation = this.activeOperations.get(item.id);
-			// if (!operation || operation.type !== 'batch') return;
-			//
-			// if (item.status === 'completed' && item.result?.success) {
-			// 	const responseData = item.result.data || [];
-			// 	const isArray = Array.isArray(responseData);
-			//
-			// 	operation.uploadIds.forEach((uploadId, index) => {
-			// 		const upload = this.uploads.get(uploadId);
-			// 		if (upload) {
-			// 			const fileData = isArray ? responseData[index] : responseData;
-			//
-			// 			if (fileData) {
-			// 				upload.uploadedData = {
-			// 					attachment_id: fileData.attachment_id,
-			// 					url: fileData.url,
-			// 					file_path: fileData.file_path,
-			// 					metadata: fileData.metadata || {}
-			// 				};
-			//
-			// 				// Store attachment ID and clear operation ID
-			// 				upload.attachmentId = fileData.attachment_id;
-			// 				upload.operationId = null; // Clear since upload is complete
-			//
-			// 				this.updateUploadStatus(upload.id, 'uploaded', 'Upload complete!');
-			// 				this.updateFieldValue(operation.fieldId, upload);
-			//
-			// 				// Process any pending metadata updates
-			// 				this.processPendingMetadata(uploadId);
-			// 			} else {
-			// 				this.updateUploadStatus(upload.id, 'error', 'No data received from server');
-			// 			}
-			// 		}
-			// 	});
-			//
-			// 	const successCount = operation.uploadIds.filter(id =>
-			// 		this.uploads.get(id)?.status === 'uploaded'
-			// 	).length;
-			//
-			// 	this.a11y.announce(`Successfully uploaded ${successCount} of ${operation.uploadIds.length} files`);
-			// 	this.notify(`Successfully uploaded ${successCount} file(s)`, 'success');
-			//
-			// 	const field = this.fields.get(operation.fieldId);
-			// 	if (field?.onUploadComplete) {
-			// 		field.onUploadComplete({
-			// 			...item.result,
-			// 			uploadedFiles: responseData,
-			// 			fieldId: operation.fieldId
-			// 		});
-			// 	}
-			// }
-			//
-			this.activeOperations.delete(queueItem.id);
-			// this.updateFieldUI(operation.fieldId);
-		}
-
-
-
-		/**
-		 * Calculate progress from queue item
-		 */
-		calculateProgress(queueItem) {
-			const progressMap = {
-				'queued': 5,
-				'pending': 15,
-				'processing': 50,
-				'uploading': 75,
-				'completed': 100,
-				'failed': 0,
-				'failed_permanent': 0
-			};
-
-			let baseProgress = progressMap[queueItem.status] || 0;
-
-			// Add incremental progress if available
-			if (queueItem.progress) {
-				const increment = queueItem.progress.percentage || 0;
-				baseProgress = Math.min(100, baseProgress + (increment * 0.8));
-			}
-
-			return Math.round(baseProgress);
-		}
-
-		/**
-		 * Generate operation title for UI
-		 */
-		getOperationTitle(field, uploads) {
-			const fileCount = uploads.length;
-			const fieldTypeNames = {
-				'single': 'Image',
-				'gallery': 'Gallery',
-			};
-
-			const typeName = fieldTypeNames[field.type] || 'File';
-
-			if (fileCount === 1) {
-				return `Uploading ${typeName}`;
-			} else {
-				return `Uploading ${fileCount} ${typeName}s`;
-			}
-		}
-
-
-		/******************************************************************
-		 *
-		 * UI
-		 *
-		 *****************************************************************/
-
-
-
-		/**
-		 * Update upload status
-		 */
-		updateUploadStatus(uploadId, status, message = '') {
-			console.log('Updating upload status');
-			const upload = this.uploads.get(uploadId);
-			if (!upload) return;
-			console.log('Updating ', upload);
-			console.log('Status: ', status);
-			console.log('Message: ', message);
-
-			upload.status = status;
-			upload.progress = upload.progress || {};
-			upload.progress.message = message;
-
-			if (status === 'uploaded') {
-				upload.progress.percent = 100;
-			} else if (status === 'error') {
-				upload.error = message;
-			}
-
-			this.updateImageUI(uploadId);
-		}
-
-		/**
-		 * Get status icon
-		 */
-		getStatusIcon(status) {
-			return window.getIcon(this.queue.icons[status]);
-		}
-
-		/*******************************************************************
-		 *
-		 * Cache Handling
-		 *
-		 ******************************************************************/
-
-		/**
-		 * Resume pending uploads from previous session
-		 */
-		async resumePendingUploads() {
-			return;
-			//TODO: Old system
-			try {
-				console.log('Checking for pending uploads...');
-				const pendingUploads = await this.cache.getImagesPendingByOperation();
-
-				if (pendingUploads.length > 0) {
-					console.log(`Found ${pendingUploads.length} pending uploads`);
-
-					// Group by field
-					const fieldGroups = new Map();
-					pendingUploads.forEach(upload => {
-						const fieldId = upload.metadata?.uploadConfig?.fieldId;
-						if (fieldId) {
-							if (!fieldGroups.has(fieldId)) {
-								fieldGroups.set(fieldId, []);
-							}
-							fieldGroups.get(fieldId).push(upload);
-						}
-					});
-
-					// Show selective restore for each field
-					for (const [fieldId, uploads] of fieldGroups) {
-						await this.showRestoreNotification(fieldId, uploads);
-					}
-				}
-			} catch (error) {
-				console.error('Failed to resume pending uploads:', error);
-			}
-		}
-
-		async showRestoreNotification(fieldId, cachedUploads) {
-			const field = this.fields.get(fieldId);
-			if (!field) {
-				console.warn(`Cannot show restore for unknown field: ${fieldId}`);
-				return;
-			}
-
-			// Pre-fetch preview images for all cached uploads
-			console.log(`Pre-fetching ${cachedUploads.length} preview images...`);
-			const uploadsWithPreviews = await this.fetchPreviewsForCachedUploads(cachedUploads);
-
-			// Create restore notification with actual preview images
-			const notification = this.createSelectiveRestoreNotification(fieldId, uploadsWithPreviews);
-
-			// Insert into field container
-			field.container.insertBefore(notification, field.container.firstChild);
-
-			// Auto-hide after 60 seconds if no interaction
-			// setTimeout(() => {
-			// 	if (notification.parentNode) {
-			// 		this.dismissCacheCheck(fieldId);
-			// 	}
-			// }, 60000);
-		}
-
-		async fetchPreviewsForCachedUploads(cachedUploads) {
-			const uploadsWithPreviews = [];
-
-			for (const upload of cachedUploads) {
-				const uploadWithPreview = { ...upload };
-
-				try {
-					// Get cached image data
-					const cachedFile = await this.cache.getImagePending(upload.id);
-
-					if (cachedFile && cachedFile.imageData) {
-						// Create blob from cached data
-						const blob = new Blob([cachedFile.imageData], {
-							type: cachedFile.metadata?.type || 'image/jpeg'
-						});
-
-						// Create preview URL
-						uploadWithPreview.previewUrl = URL.createObjectURL(blob);
-
-						console.log(`Created preview for ${upload.metadata?.originalName || upload.id}`);
-					} else {
-						console.warn(`No cached image data found for upload ${upload.id}`);
-						uploadWithPreview.previewUrl = null;
-					}
-				} catch (error) {
-					console.error(`Failed to fetch preview for upload ${upload.id}:`, error);
-					uploadWithPreview.previewUrl = null;
-				}
-
-				uploadsWithPreviews.push(uploadWithPreview);
-			}
-
-			return uploadsWithPreviews;
-		}
-
-		createSelectiveRestoreNotification(fieldId, cachedUploads) {
-			let notification = window.getTemplate('restoreNotification');
-			notification.dataset.fieldId = fieldId;
-
-			notification.querySelector('.restore-details').textContent = `We found ${cachedUploads.length} image(s) from your previous session. You can restore them here if you'd like, or you can start over.`;
-
-			let field = this.fields.get(fieldId);
-
-			let container = notification.querySelector('.item-grid');
-			// ${cachedUploads.map((upload, index) => this.createRestoreItemHTML(upload, index)).join('')}
-			cachedUploads.forEach((upload, index) => {
-				let item = window.getTemplate('restoreItem');
-
-				const fileName = upload.metadata?.originalName || `Upload ${index + 1}`;
-				const fileSize = upload.metadata?.originalSize || 0;
-				const hasPreview = upload.previewUrl;
-
-				let preview = item.querySelector('.preview');
-				if (hasPreview) {
-					preview.querySelector('div').remove();
-					let img = preview.querySelector('img');
-					[
-						img.src,
-						img.alt
-					] = [
-						upload.previewUrl,
-						fileName
-					];
-				} else {
-					preview.querySelector('img').remove();
-				}
-
-				[
-					item.dataset.id,
-					item.querySelector('.name').textContent,
-					item.querySelector('input').id,
-					item.querySelector('input').name,
-					item.htmlFor
-				] = [
-					upload.id,
-					fileName,
-					`restore-${upload.id}`,
-					`restore-${upload.id}`,
-					`restore-${upload.id}`,
-				];
-
-				container.append(item);
-			});
-
-			// Attach event listeners
-			this.attachRestoreEventListeners(notification, fieldId, cachedUploads);
-
-			return notification;
-		}
-
-
-		attachRestoreEventListeners(notification, fieldId, cachedUploads) {
-			const selectAll = notification.querySelector('.select-all-restore');
-			const selectNone = notification.querySelector('.select-none-restore');
-			const restoreSelected = notification.querySelector('.restore-selected');
-			const deleteCache = notification.querySelector('.delete-cache');
-			const dismiss = notification.querySelector('.dismiss-cache-check');
-
-			// Cleanup function for preview URLs
-			const cleanupPreviewUrls = () => {
-				cachedUploads.forEach(upload => {
-					if (upload.previewUrl && upload.previewUrl.startsWith('blob:')) {
-						URL.revokeObjectURL(upload.previewUrl);
-					}
-				});
-			};
-
-			// Select all/none functionality
-			selectAll?.addEventListener('click', () => {
-				notification.querySelectorAll('.restore-checkbox').forEach(cb => cb.checked = true);
-			});
-
-			selectNone?.addEventListener('click', () => {
-				notification.querySelectorAll('.restore-checkbox').forEach(cb => cb.checked = false);
-			});
-
-			// Restore selected items
-			restoreSelected?.addEventListener('click', async () => {
-				const selectedCheckboxes = notification.querySelectorAll('.restore-checkbox:checked');
-				const selectedIds = Array.from(selectedCheckboxes).map(cb =>
-					cb.id.replace('restore-', '')
-				);
-
-				if (selectedIds.length === 0) {
-					this.notify('No items selected for restore', 'warning');
-					return;
-				}
-
-				// Restore selected uploads
-				const selectedUploads = cachedUploads.filter(upload =>
-					selectedIds.includes(upload.id)
-				);
-
-				await this.restoreSelectedUploads(fieldId, selectedUploads);
-
-				// Cleanup preview URLs
-				cleanupPreviewUrls();
-
-				// Remove notification
-				notification.remove();
-
-				this.notify(`Restored ${selectedIds.length} item(s)`, 'success');
-			});
-
-			// Delete all cache
-			deleteCache?.addEventListener('click', async () => {
-				if (confirm('This will permanently delete all cached data. Are you sure?')) {
-					await this.clearFieldCache(fieldId);
-
-					// Cleanup preview URLs
-					cleanupPreviewUrls();
-
-					notification.remove();
-					this.notify('Cache cleared', 'info');
-				}
-			});
-
-			// Dismiss notification
-			dismiss?.addEventListener('click', () => {
-				// Cleanup preview URLs
-				cleanupPreviewUrls();
-
-				notification.remove();
-			});
-		}
-
-		async restoreSelectedUploads(fieldId, selectedUploads) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			let operations = new Set();
-			for (const cachedUpload of selectedUploads) {
-				try {
-					console.log('upload', cachedUpload);
-					const upload = await this.getCachedUpload(fieldId, cachedUpload);
-					operations.add(upload.operationId);
-
-					// Add to field
-					if (!field.uploads) field.uploads = new Set();
-					field.uploads.add(upload.id);
-
-
-					// Add to main preview (not auto-grouped)
-					this.addImageToPost(upload.id, field.previewGrid, true);
-
-				} catch (error) {
-					console.error(`Failed to restore upload ${cachedUpload.id}:`, error);
-				}
-			}
-			field.operationId = operations;
-			this.fields.set(fieldId, field);
-
-			// Update field UI
-			this.maybeLockUploads(fieldId);
-
-			if (field.type === 'groupable') {
-				field.container.querySelector('.group-display').hidden = false;
-			}
-		}
-
-		async clearFieldCache(fieldId) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			try {
-				// 1. Clear IndexedDB stores for this field
-				await this.clearFieldIndexedDB(fieldId);
-
-				// 2. Clear memory cache for field-related data
-				this.clearFieldMemoryCache(fieldId);
-
-				// 3. Clear HTTP headers related to field uploads
-				this.clearFieldHttpHeaders(fieldId);
-
-				// 4. Clear any pending metadata updates
-				this.clearFieldMetadata(fieldId);
-
-				// 5. Clear performance monitoring data
-				this.clearFieldPerformanceData(fieldId);
-
-				console.log(`Comprehensive cache clearing completed for field: ${fieldId}`);
-
-			} catch (error) {
-				console.error(`Failed to clear field cache for ${fieldId}:`, error);
-				throw error;
-			}
-		}
-
-		/**
-		 * Clear IndexedDB data specific to a field
-		 */
-		async clearFieldIndexedDB(fieldId) {
-			if (!this.cache.imageDB) return;
-
-			try {
-				// Clear pending images for this field
-				const pendingImages = await this.cache.getImagesPendingByField(fieldId);
-
-				for (const image of pendingImages) {
-					await this.cache.removeImagePending(image.id);
-				}
-
-				// Clear group data for this field
-				await this.cache.clearGroupsForField(fieldId);
-
-				// Clear any cached field groups
-				const cacheKey = `field_groups_${fieldId}`;
-				await this.cache.removeCacheItem(cacheKey);
-
-				console.log(`Cleared IndexedDB data for field: ${fieldId}`);
-
-			} catch (error) {
-				console.error(`Failed to clear IndexedDB for field ${fieldId}:`, error);
-			}
-		}
-
-		/**
-		 * Clear memory cache data related to a field
-		 */
-		clearFieldMemoryCache(fieldId) {
-			if (!this.cache._memoryCache) return;
-
-			const keysToRemove = [];
-
-			// Find all memory cache keys related to this field
-			for (const [key, value] of this.cache._memoryCache) {
-				// Check for field-specific cache keys
-				if (key.includes(fieldId) ||
-					key.startsWith(`pending_image_`) ||
-					key.startsWith(`post_${fieldId}_`) ||
-					key.startsWith(`upload_${fieldId}_`) ||
-					(value && value.fieldId === fieldId)) {
-					keysToRemove.push(key);
-				}
-			}
-
-			// Remove identified keys
-			keysToRemove.forEach(key => {
-				this.cache._memoryCache.delete(key);
-			});
-
-			console.log(`Cleared ${keysToRemove.length} memory cache entries for field: ${fieldId}`);
-		}
-
-		/**
-		 * Clear HTTP headers related to field operations
-		 */
-		clearFieldHttpHeaders(fieldId) {
-			if (!this.cache.httpHeaders) return;
-
-			const headersToRemove = [];
-
-			// Find HTTP headers related to upload operations for this field
-			for (const [key, headerData] of this.cache.httpHeaders) {
-				if (key.includes('uploads') ||
-					key.includes(fieldId) ||
-					headerData.url?.includes('uploads')) {
-					headersToRemove.push(key);
-				}
-			}
-
-			// Remove identified headers
-			headersToRemove.forEach(key => {
-				this.cache.httpHeaders.delete(key);
-			});
-
-			// Force save the updated headers
-			if (headersToRemove.length > 0) {
-				this.cache.saveHttpHeaders();
-				console.log(`Cleared ${headersToRemove.length} HTTP headers for field: ${fieldId}`);
-			}
-		}
-
-		/**
-		 * Clear pending metadata updates for a field
-		 */
-		clearFieldMetadata(fieldId) {
-			if (!this.metadataUpdateTimers || !this.metadataPending || !this.metadataQueue) return;
-
-			const field = this.fields.get(fieldId);
-			if (!field || !field.uploads) return;
-
-			// Clear metadata timers for uploads in this field
-			for (const uploadId of field.uploads) {
-				// Clear update timers
-				for (const [timerKey, timer] of this.metadataUpdateTimers) {
-					if (timerKey.startsWith(uploadId)) {
-						clearTimeout(timer);
-						this.metadataUpdateTimers.delete(timerKey);
-					}
-				}
-
-				// Clear pending metadata
-				this.metadataPending.delete(uploadId);
-				this.metadataQueue.delete(uploadId);
-			}
-
-			console.log(`Cleared metadata updates for field: ${fieldId}`);
-		}
-
-		/**
-		 * Clear performance monitoring data for a field
-		 */
-		clearFieldPerformanceData(fieldId) {
-			if (!this.performanceMonitor || !this.performanceMonitor.metrics) return;
-
-			const field = this.fields.get(fieldId);
-			if (!field || !field.uploads) return;
-
-			// Clear performance metrics for uploads in this field
-			for (const uploadId of field.uploads) {
-				this.performanceMonitor.metrics.delete(uploadId);
-			}
-
-			console.log(`Cleared performance data for field: ${fieldId}`);
-		}
-
-		/**
-		 * Reset field to its initial state
-		 */
-		resetFieldToInitialState(fieldId) {
-			const field = this.fields.get(fieldId);
-			if (!field) return;
-
-			// Reset field properties to initial values
-			field.uploads = new Set();
-			field.posts = new Map(); // Replace groups with posts
-			field.status = 'ready';
-
-			// Clear any operation IDs
-			for (const [operationId, operation] of this.activeOperations) {
-				if (operation.fieldId === fieldId) {
-					this.activeOperations.delete(operationId);
-				}
-			}
-
-			// Reset file input
-			if (field.input) {
-				field.input.value = '';
-			}
-
-			// Reset hidden value input
-			if (field.hiddenValue) {
-				field.hiddenValue.value = '';
-			}
-
-			// Clear any progress indicators
-			const progressBars = field.container.querySelectorAll('.progress');
-			progressBars.forEach(bar => bar.remove());
-
-			// Reset drag state for this field
-			this.initializeDragState();
-
-			console.log(`Reset field ${fieldId} to initial state`);
-		}
-
-
-		/**
-		 * Handle start over action
-		 */
-		async clearCache(fieldId) {
-			// Show confirmation dialog
-			const confirmed = await this.showStartOverConfirmation();
-
-			if (!confirmed) return;
-
-			try {
-				this.a11y.announce('Starting over - clearing all cached data');
-
-				// Clear all uploads for this field (existing functionality)
-				this.cleanField(fieldId);
-
-				// Remove notification
-				this.dismissCacheCheck(fieldId);
-
-				// Hide group display
-				const field = this.fields.get(fieldId);
-				const groupDisplay = field?.container.querySelector('.group-display');
-				if (groupDisplay) {
-					groupDisplay.hidden = true;
-				}
-
-				// Reset field to initial state
-				this.resetFieldToInitialState(fieldId);
-
-				this.notify('Started fresh - all cached data cleared', 'success');
-
-			} catch (error) {
-				this.error.log(error, {
-					component: 'UploadManager',
-					action: 'clearCache',
-					fieldId: fieldId
-				});
-
-				this.notify('Failed to clear cache - please try again', 'error');
-			}
-		}
-
-		/**
-		 * Show confirmation dialog for start over
-		 */
-		async showStartOverConfirmation() {
-			return new Promise((resolve) => {
-				let dialog;
-
-				if (window.getTemplate) {
-					dialog = window.getTemplate('startOverConfirmation');
-				} else {
-					// Fallback if template system not available
-					dialog = document.createElement('dialog');
-					dialog.className = 'start-over-confirmation';
-					dialog.innerHTML = `
-					<div class="confirmation-content">
-						<h3>Start Over?</h3>
-						<p>This will permanently delete:</p>
-						<ul>
-							<li>All uploaded images</li>
-							<li>All created groups</li>
-							<li>All metadata and settings</li>
-						</ul>
-						<p><strong>This cannot be undone!</strong></p>
-						<div class="confirmation-actions">
-							<button type="button" class="cancel-start-over">Cancel</button>
-							<button type="button" class="confirm-start-over danger">Yes, Start Over</button>
-						</div>
-					</div>
-				`;
-				}
-
-				document.body.appendChild(dialog);
-				dialog.showModal();
-
-				// Handle buttons
-				dialog.querySelector('.confirm-start-over').addEventListener('click', () => {
-					dialog.close();
-					dialog.remove();
-					resolve(true);
-				});
-
-				dialog.querySelector('.cancel-start-over').addEventListener('click', () => {
-					dialog.close();
-					dialog.remove();
-					resolve(false);
-				});
-
-				// Handle escape key and backdrop click
-				dialog.addEventListener('close', () => {
-					dialog.remove();
-					resolve(false);
-				});
-
-				// Focus the cancel button by default (safer option)
-				dialog.querySelector('.cancel-start-over').focus();
-			});
-		}
-
-		/**
-		 * Dismiss restore notification
-		 */
-		dismissCacheCheck(fieldId) {
-			const field = this.fields.get(fieldId);
-			const notification = field?.container.querySelector('.restore-notification');
-
-			if (notification) {
-				// Cleanup any preview URLs before removing
-				const previewImages = notification.querySelectorAll('.restore-preview-image');
-				previewImages.forEach(img => {
-					if (img.src && img.src.startsWith('blob:')) {
-						URL.revokeObjectURL(img.src);
-					}
-				});
-
-				// Fade out animation
-				notification.style.opacity = '0';
-				notification.style.transform = 'translateY(-10px)';
-
-				setTimeout(() => {
-					notification.remove();
-				}, 300);
-			}
-		}
-		/**
-		 * Cache upload
-		 */
-		async cacheUpload(upload) {
-			return;
-			//TODO: from old cache
-			try {
-				// Store group relationships alongside upload
-				const fieldId = upload.fieldId;
-				const field = this.fields.get(fieldId);
-
-				const cacheData = {
-					...upload.metadata,
-					uploadConfig: {
-						fieldId: upload.fieldId,
-						content: field?.content,
-						isGroupable: field?.type === 'groupable'
-					},
-					originalName: upload.originalFile?.name || 'unknown_file',
-					originalType: upload.originalFile?.type || 'image/jpeg',
-					originalSize: upload.originalFile?.size || 0
-				};
-
-				return await this.cache.storeImagePending(
-					upload.id,
-					upload.processedFile,
-					cacheData,
-					upload.operationId
-				);
-			} catch (error) {
-				console.error('Failed to cache upload:', error);
-				return false;
-			}
-		}
-		async getCachedUpload(fieldId, cachedUpload) {
-			return;
-			//TODO: from old cache
-			try {
-				let operation;
-				if (this.queue.queue.has(cachedUpload.operationId)) {
-					operation = this.queue.queue.get(cachedUpload.operationId);
-				} else {
-					operation = await this.queue.fetchOperation(cachedUpload.operationId);
-				}
-
-				// Recreate upload object with proper structure
-				const upload = {
-					id: cachedUpload.id,
-					fieldId: fieldId,
-					originalFile: null, // Will be set below
-					status: operation?.status ?? 'cached',
-					progress: { percent: 100, message: 'Restored from cache' },
-					meta: cachedUpload.meta || {}, // Use 'meta' instead of 'metadata'
-					preview: null, // Will be regenerated
-					createdAt: cachedUpload.timestamp || Date.now(),
-					cachedData: cachedUpload,
-					operationId: cachedUpload.operationId
-				};
-
-				// Retrieve actual file data from cache
-				const cachedFile = await this.cache.getImagePending(cachedUpload.id);
-
-				if (cachedFile && cachedFile.imageData) {
-					const fileName = cachedUpload.metadata?.originalName ||
-						cachedFile.metadata?.originalName ||
-						'restored_file';
-					const fileType = cachedFile.metadata?.type || 'image/jpeg';
-
-					// Convert cached data back to File object
-					upload.processedFile = new File(
-						[cachedFile.imageData],
-						fileName,
-						{
-							type: fileType,
-							lastModified: cachedUpload.timestamp || Date.now()
-						}
-					);
-
-					// Create a minimal originalFile object for UI compatibility
-					upload.originalFile = {
-						name: fileName,
-						type: fileType,
-						size: cachedFile.imageData.byteLength || cachedFile.imageData.size || 0,
-						lastModified: cachedUpload.timestamp || Date.now()
-					};
-
-					// Create preview URL for immediate display
-					upload.preview = URL.createObjectURL(upload.processedFile);
-				} else {
-					// If we can't get the cached file, create minimal fallback
-					const fileName = cachedUpload.metadata?.originalName || 'restored_file';
-					upload.originalFile = {
-						name: fileName,
-						type: 'image/jpeg',
-						size: 0,
-						lastModified: cachedUpload.timestamp || Date.now()
-					};
-
-					// Create a placeholder preview
-					upload.preview = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2Y1ZjVmNSIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+Tm8gUHJldmlldz48L3RleHQ+PC9zdmc+';
-				}
-
-				// Store upload in the uploads map
-				this.uploads.set(upload.id, upload);
-
-				return upload;
-			} catch (error) {
-				console.error('Failed to get cached upload:', error);
-				throw error;
-			}
-		}
-
-		async cachePostData(fieldId) {
-			return;
-			//TODO: From old cache
-			let field = this.fields.get(fieldId);
-			if (!field || field.type !== 'groupable') return;
-
-			const key = `image_field_${fieldId}`;
-			await this.cache.setItem(key, field.posts);
-		}
-
-		async getCachedPostData(fieldId) {
-			const key = `image_field_${fieldId}`;
-			return await this.cache.getItem(key);
-		}
-
-		async deleteCachedPostData(fieldId) {
-			const key = `image_field_${fieldId}`;
-			return await this.cache.removeCacheItem(key);
-		}
-
-		/**
-		 * Cleanup upload
-		 */
-		async cleanupUpload(upload) {
-			try {
-				if (this.cache) {
-					await this.cache.removeImagePending(upload.id);
-
-					// Clear field groups cache if needed
-					if (upload.fieldId) {
-						const field = this.fields.get(upload.fieldId);
-						if (field?.type === 'groupable') {
-							await this.cacheFieldGroups(upload.fieldId);
-						}
-					}
-				}
-			} catch (error) {
-				console.error('Failed to cleanup upload:', error);
-			}
-		}
-
-		/******************************************************************
-		 *
-		 * Utility Methods
-		 *
-		 *****************************************************************/
-
-		/**
-		 * Helper: Check if an upload item is currently selected
-		 */
-		isUploadSelected(uploadId) {
-			return document.querySelector(`[data-upload-id="${uploadId}"]`).checked;
-		}
-
-		getField(element) {
-			return this.fields.get(this.getFieldId(element));
-		}
-
-		/**
-		 * Get field ID from any element within the field
-		 */
-		getFieldId(element) {
-			// Try multiple approaches to find the field ID
-
-			// 1. Direct data attribute
-			if (element.dataset.fieldId) {
-				return element.dataset.fieldId;
-			}
-
-			// 2. Look up the DOM tree for field containers
-			const fieldContainer = element.closest('[data-field-id]');
-			if (fieldContainer) {
-				return fieldContainer.dataset.fieldId ||
-					fieldContainer.dataset.field ||
-					fieldContainer.dataset.name;
-			}
-			return null;
-		}
-
-		getFieldStatus(fieldId) {
-			const field = this.fields.get(fieldId);
-			if (!field) return null;
-
-			const uploads = Array.from(field.uploads || []).map(id => this.uploads.get(id));
-
-			return {
-				fieldId,
-				type: field.type,
-				uploadCount: uploads.length,
-				ready: uploads.filter(u => u.status === 'processed').length,
-				uploading: uploads.filter(u => u.status === 'uploading').length,
-				completed: uploads.filter(u => u.status === 'uploaded').length,
-				failed: uploads.filter(u => u.status === 'error').length
-			};
-		}
-
-		/**
-		 * Get upload ID from element
-		 */
-		getUploadId(element) {
-			const uploadItem = element.closest('[data-upload-id]');
-			return uploadItem?.dataset.uploadId;
-		}
-
-		createFieldId(field) {
-			let input = field.querySelector('input[type=file]');
-			return input.id || input.name || `img_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
-		}
-
-		generateUploadId() {
-			return `upload_${this.generateID()}`;
-		}
-		generateID() {
-			return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-		}
-
-		validateFile(file, field) {
-			// Type validation
-			if (!this.settings.allowedTypes.includes(file.type)) {
-				this.notify(`Invalid file type: ${file.type}`, 'error');
-				return false;
-			}
-
-			// Size validation
-			if (file.size > this.settings.maxFileSize) {
-				this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
-				return false;
-			}
-
-			return true;
-		}
-
-		checkFieldLimits(fieldId, newFileCount) {
-			const field = this.fields.get(fieldId);
-			const currentCount = field.uploads ? field.uploads.size : 0;
-
-			return (currentCount + newFileCount) <= field.maxFiles;
-		}
-
-		/**
-		 * Show notification
-		 */
-		notify(message, type = 'info') {
-			this.notifications.showToast(message, type);
-		}
-
-		formatBytes(bytes) {
-			const sizes = ['Bytes', 'KB', 'MB'];
-			if (bytes === 0) return '0 Bytes';
-			const i = Math.floor(Math.log(bytes) / Math.log(1024));
-			return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
-		}
-
-		/**
-		 * Check if element is an upload trigger
-		 */
-		isUploadTrigger(element) {
-			if (element.type === 'file' || element.tagName === 'LABEL') {
-				return false;
-			}
-
-			if (element.matches('.upload-trigger, .file-upload-button, [data-upload-trigger]')) {
-				return true;
-			}
-
-			const uploadContainer = element.closest('.file-upload-container');
-			if (uploadContainer) {
-				return !element.closest('input, label, button, a');
-			}
-
-			return false;
-		}
-
-
-		/**
-		 * Trigger file selection for a field
-		 */
-		triggerFileSelection(fieldId) {
-			const field = this.fields.get(fieldId);
-			if (!field) {
-				console.error('Field not found:', fieldId);
-				return;
-			}
-
-			if (!field.input) {
-				console.error('Field input not found for field:', fieldId, field);
-				return;
-			}
-
-			// Check if field is at capacity
-			if (field.uploads && field.uploads.size >= field.maxFiles) {
-				this.notify(`Maximum ${field.maxFiles} files allowed for this field`, 'warning');
-				return;
-			}
-
-			console.log('Triggering file selection for field:', fieldId, field.input);
-
-			// Make sure the input is not disabled or hidden
-			if (field.input.disabled) {
-				console.warn('Cannot trigger disabled input:', field.input);
-				return;
-			}
-
-			// Trigger the click
-			try {
-				field.input.click();
-			} catch (error) {
-				console.error('Failed to trigger file input click:', error);
-			}
-		}
-
-		/**
-		 * Remove an upload
-		 */
-		removeUpload(fieldId, uploadId) {
-			const field = this.fields.get(fieldId);
-			const upload = this.uploads.get(uploadId);
-
-			if (!field || !upload) return;
-
-			// Remove from field's upload set
-			field.uploads.delete(uploadId);
-
-			// CRITICAL FIX: Clean up ALL object URLs
-			if (upload.preview && upload.preview.startsWith('blob:')) {
-				URL.revokeObjectURL(upload.preview);
-			}
-
-			// Remove from cache
-			this.cleanupUpload(upload);
-
-			// Remove from uploads map
-			this.uploads.delete(uploadId);
-
-			// Remove from UI
-			this.removeUploadFromUI(uploadId);
-
-			// Update field UI
-			this.maybeLockUploads(fieldId);
-
-			console.log(`Removed upload ${uploadId} from field ${fieldId}`);
-		}
-
-		/********************************************************************
-		 *
-		 * Cleanup
-		 *
-		 *******************************************************************/
-		destroy() {
-			// Remove event listeners
-			document.removeEventListener('click', this.handleClick);
-			document.removeEventListener('change', this.handleChange);
-			document.removeEventListener('paste', this.paste);
-			document.removeEventListener('dragstart', this.handleDragStart);
-			document.removeEventListener('dragend', this.handleDragEnd);
-			document.removeEventListener('dragenter', this.handleDragEnter);
-			document.removeEventListener('dragover', this.handleDragOver);
-			document.removeEventListener('dragleave', this.handleDragLeave);
-			document.removeEventListener('drop', this.handleDrop);
-			document.removeEventListener('touchstart', this.handleTouchStart);
-			document.removeEventListener('touchmove', this.handleTouchMove);
-			document.removeEventListener('touchend', this.handleTouchEnd);
-			document.removeEventListener('touchcancel', this.handleTouchCancel);
-			window.removeEventListener('beforeunload', this.handleBeforeUnload);
-
-			this.uploads.forEach(upload => {
-				if (upload.preview && upload.preview.startsWith('blob:')) {
-					URL.revokeObjectURL(upload.preview);
-				}
-			});
-
-			// Terminate and cleanup worker
-			if (this.compressionWorker) {
-				this.compressionWorker.terminate();
-				this.compressionWorker = null;
-			}
-
-			// Clear performance monitor metrics to prevent memory accumulation
-			if (this.performanceMonitor && this.performanceMonitor.metrics) {
-				this.performanceMonitor.metrics.clear();
-			}
-
-			// Clear drag state
-			this.initializeDragState();
-
-			// Clear all maps
-			this.fields.clear();
-			this.uploads.clear();
-			this.activeOperations.clear();
-			this.oldUploads.clear();
-
-			// Force garbage collection if available
-			if (window.gc) {
-				window.gc();
-			}
-		}
-
-		recordUploadAnalytics(uploadId, success, processingTime = 0) {
-			if (!this.settings.analyticsEnabled) return;
-
-			const upload = this.uploads.get(uploadId);
-			if (!upload) return;
-
-			const analytics = {
-				fieldType: this.fields.get(upload.fieldId)?.type,
-				fileName: upload.originalFile.name,
-				fileSize: upload.originalFile.size,
-				processedSize: upload.processedFile?.size || upload.originalFile.size,
-				processingTime,
-				success,
-				compressionRatio: upload.processedFile ?
-					upload.originalFile.size / upload.processedFile.size : 1,
-				timestamp: Date.now()
-			};
-
-			// Send analytics (could be batched)
-			this.sendUploadAnalytics(analytics);
-		}
-
-		async sendUploadAnalytics(analytics) {
-			try {
-				await fetch(`${jvbSettings.api}analytics/uploads`, {
-					method: 'POST',
-					headers: {
-						'Content-Type': 'application/json',
-						'action_nonce': jvbSettings.dash
-					},
-					body: JSON.stringify({ analytics })
-				});
-			} catch (error) {
-				console.warn('Failed to send upload analytics:', error);
-			}
-		}
-
-	}
-
-	// Initialize singleton
-	document.addEventListener('DOMContentLoaded', () => {
-		if (!window.jvbUploadManager) {
-			window.jvbUploadManager = new UploadManager();
-			console.log('Centralized Upload Manager initialized');
-		}
-	});
-
-	// Export for use
-	window.UploadManager = UploadManager;
-
-
-	class UploadPerformanceMonitor {
-		constructor() {
-			this.metrics = new Map();
-			this.averages = {
-				processingTime: 0,
-				uploadSpeed: 0,
-				successRate: 95
-			};
-		}
-
-		startTiming(uploadId, phase) {
-			if (!this.metrics.has(uploadId)) {
-				this.metrics.set(uploadId, {});
-			}
-			this.metrics.get(uploadId)[`${phase}_start`] = performance.now();
-		}
-
-		endTiming(uploadId, phase) {
-			const metrics = this.metrics.get(uploadId);
-			if (metrics && metrics[`${phase}_start`]) {
-				metrics[`${phase}_duration`] = performance.now() - metrics[`${phase}_start`];
-			}
-		}
-
-		recordUploadMetrics(uploadId, fileSize, success) {
-			const metrics = this.metrics.get(uploadId);
-			if (!metrics) return;
-
-			// Calculate upload speed (bytes per second)
-			const totalTime = (metrics.upload_duration || 0) / 1000; // Convert to seconds
-			const uploadSpeed = totalTime > 0 ? fileSize / totalTime : 0;
-
-			// Update running averages
-			this.updateAverages(metrics, uploadSpeed, success);
-
-			// Clean up old metrics (keep last 100)
-			if (this.metrics.size > 100) {
-				const oldestKey = this.metrics.keys().next().value;
-				this.metrics.delete(oldestKey);
-			}
-		}
-
-		updateAverages(metrics, uploadSpeed, success) {
-			// Exponential moving average for responsiveness
-			const alpha = 0.1;
-
-			if (metrics.processing_duration) {
-				this.averages.processingTime = (1 - alpha) * this.averages.processingTime +
-					alpha * metrics.processing_duration;
-			}
-
-			if (uploadSpeed > 0) {
-				this.averages.uploadSpeed = (1 - alpha) * this.averages.uploadSpeed +
-					alpha * uploadSpeed;
-			}
-
-			this.averages.successRate = (1 - alpha) * this.averages.successRate +
-				alpha * (success ? 100 : 0);
-		}
-
-		getOptimalBatchSize() {
-			// Adjust batch size based on performance
-			if (this.averages.uploadSpeed > 1000000) { // > 1MB/s
-				return 5;
-			} else if (this.averages.uploadSpeed > 500000) { // > 500KB/s
-				return 3;
-			} else {
-				return 1; // Slow connection, upload one at a time
-			}
-		}
-
-		/**
-		 * Clean up resources
-		 */
-		destroy() {
-			// Clean up blob URLs
-			for (const [uploadId, upload] of this.uploads) {
-				if (upload.preview && upload.preview.startsWith('blob:')) {
-					URL.revokeObjectURL(upload.preview);
-				}
-			}
-
-			// Clear maps
-			this.uploads.clear();
-			this.fields.clear();
-			this.subscribers.clear();
-
-			// Remove event listeners
-			document.removeEventListener('click', this.clickHandler);
-			document.removeEventListener('change', this.handleChange);
-			document.removeEventListener('paste', this.paste);
-			document.removeEventListener('dragstart', this.handleDragStart);
-			document.removeEventListener('dragend', this.handleDragEnd);
-			document.removeEventListener('dragenter', this.handleDragEnter);
-			document.removeEventListener('dragover', this.handleDragOver);
-			document.removeEventListener('dragleave', this.handleDragLeave);
-			document.removeEventListener('drop', this.handleDrop);
-		}
-	}
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index b62c55f..3fcaee1 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,httpHeaders:new Map,subscribers:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),clearHttpHeaders:t=>this.clearHttpHeaders(e,t),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}t.useHttpCaching&&!e.objectStoreNames.contains("headers")&&e.createObjectStore("headers",{keyPath:"key"})}loadStoreDataInBackground(e){const t=this.stores.get(e);if(!t?.db)return;const s=[this.loadStoreData(e),this.loadStoreCache(e),this.loadStoreHeaders(e)];Promise.all(s).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async loadStoreData(e){const t=this.stores.get(e);if(t?.db)return new Promise((s=>{const r=t.db.transaction([t.config.storeName],"readonly").objectStore(t.config.storeName).getAll();r.onsuccess=r=>{const i=r.target.result||[];i.forEach((e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.notify(e,"data-loaded",{count:i.length}),s(i)},r.onerror=()=>s([])}))}async loadStoreCache(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("cache"))return new Promise((e=>{const s=t.db.transaction(["cache"],"readonly").objectStore("cache").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)})),e()},s.onerror=()=>e()}))}async loadStoreHeaders(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("headers"))return new Promise((e=>{const s=t.db.transaction(["headers"],"readonly").objectStore("headers").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{t.httpHeaders.set(e.key,e)})),e()},s.onerror=()=>e()}))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers},o=t.httpHeaders.get(s);t.config.useHttpCaching&&o&&(o.etag&&(a["If-None-Match"]=o.etag),o.lastModified&&(a["If-Modified-Since"]=o.lastModified));const n=new AbortController;t.currentRequest=n;const c=await fetch(i,{method:"GET",headers:a,signal:n.signal});if(304===c.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!c.ok)throw new Error(`HTTP ${c.status}: ${c.statusText}`);const d=await c.json();return t.config.useHttpCaching&&this.storeResponseHeaders(e,s,c),await this.processFetchedData(e,d,s),this.notify(e,"data-loaded",{cached:!1,items:d.items||[]}),d}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s){const r=this.stores.get(e),i=t.items||[];if(r.db&&i.length>0){const e=r.db.transaction([r.config.storeName],"readwrite"),t=e.objectStore(r.config.storeName);for(const e of i){const s=this.processForStorage(e,r.config.validateData);if(s.valid){const i=this.getItemKey(s.data,r.config.keyPath);r.data.set(i,e),await t.put(s.data)}}await new Promise(((t,s)=>{e.oncomplete=()=>t(),e.onerror=()=>s(e.error)}))}const a={key:s,items:i.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters}};r.cache.set(s,a),await this.saveToCache(e,s,a),r.lastResponse={...t,has_more:t.has_more||!1,total:t.total||i.length,pages:t.pages||1,queue_stats:t.queue_stats||{}}}async save(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath);if(s.data.set(a,t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.put(i)}return this.notify(e,"item-saved",{item:t,key:a}),a}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:null};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:null};if(e instanceof FormData)return t?{valid:!1,error:`FormData at ${s}`}:{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e))return{valid:!0,data:e};if(e instanceof Set){const r=Array.from(e);return this.processForStorage(r,t,s)}if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;null!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;null!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:null}}async delete(e,t){const s=this.stores.get(e);if(s.data.delete(t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.delete(t)}this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);return r&&r.items?r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);if(t.data.clear(),t.cache.clear(),t.db){const e=t.db.transaction([t.config.storeName],"readwrite").objectStore(t.config.storeName);await e.clear()}this.notify(e,"data-cleared")}setFilter(e,t,s){const r=this.stores.get(e),i=r.filters[t];null==s||""===s?delete r.filters[t]:r.filters[t]=s,this.notify(e,"filters-changed",{filters:r.filters,changed:{key:t,oldValue:i,newValue:s}}),r.config.endpoint&&this.fetch(e)}async setFilters(e,t){const s=this.stores.get(e);Object.keys(t).some((e=>s.filters[e]!==t[e]))&&(s.filters={...s.filters,...t},this.notify(e,"filters-changed",{filters:s.filters,changed:t}),s.config.endpoint&&await this.fetch(e))}removeFilter(e,t){const s=this.stores.get(e),r=s.filters[t];void 0!==r&&(delete s.filters[t],this.notify(e,"filters-changed",{filters:s.filters,removed:{key:t,oldValue:r}}),s.config.endpoint&&this.fetch(e))}clearFilters(e){const t=this.stores.get(e),s={...t.filters};t.filters={...t.config.filters},this.notify(e,"filters-cleared",{oldFilters:s,filters:t.filters}),t.config.endpoint&&this.fetch(e)}clearCache(e){const t=this.stores.get(e);if(t.cache.clear(),t.db&&t.db.objectStoreNames.contains("cache")){t.db.transaction(["cache"],"readwrite").objectStore("cache").clear()}this.notify(e,"cache-cleared")}clearHttpHeaders(e,t=null){const s=this.stores.get(e);if(t){if(s.httpHeaders.delete(t),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").delete(t)}}else if(s.httpHeaders.clear(),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").clear()}}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}storeResponseHeaders(e,t,s){const r=this.stores.get(e),i={key:t,etag:s.headers.get("ETag"),lastModified:s.headers.get("Last-Modified"),timestamp:Date.now()};if(r.httpHeaders.set(t,i),r.db&&r.db.objectStoreNames.contains("headers")){r.db.transaction(["headers"],"readwrite").objectStore("headers").put(i)}}async saveToCache(e,t,s){const r=this.stores.get(e);if(!r.db||!r.db.objectStoreNames.contains("cache"))return;const i=r.db.transaction(["cache"],"readwrite").objectStore("cache");await i.put(s)}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let i=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,r={config:{dbName:i.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};r.config.headers={"X-WP-Nonce":window.auth.getNonce(),...r.config.headers},i.stores[t.storeName]=s,this.stores.set(s,r),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const r={};for(const[e,t]of Object.entries(i.stores))r[e]=this.getStoreAPI(t);return r}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,i]of e.entries())i instanceof File||i instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(i)):t.entries[s]=i);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,i]of Object.entries(e.entries))Array.isArray(i)?i.forEach((e=>t.append(s,e))):t.append(s,i);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:i}=t,r=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,i,(e=>{r.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}r.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((i,r)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>i(e.target.result),a.onerror=e=>r(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const i=this.stores.get(e);return i?.db&&i.db.objectStoreNames.contains(t)?new Promise((e=>{const r=i.db.transaction([t],"readonly").objectStore(t).getAll();r.onsuccess=t=>{const i=t.target.result||[];i.forEach(s),e(i)},r.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,i){const r=this.stores.get(e);return r?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=r.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>a(o.error);try{h=i(c,o)}catch(e){a(e)}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),i=t.cache.get(s);if(i&&this.isCacheValid(i,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:i.items||[]}),i;t.config.showLoading&&this.setLoading(!0);const r=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&i&&(i.etag&&(a["If-None-Match"]=i.etag),i.lastModified&&(a["If-Modified-Since"]=i.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(r,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return i?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:i.items||[]}),i):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const i=t.config.apiBase+t.config.endpoint;return s.toString()?`${i}?${s}`:i}async processFetchedData(e,t,s,i){const r=this.stores.get(e),a=t.items||[],o=[];r.db&&a.length>0&&await this.withTransaction(e,r.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const i=this._saveItem(e,s);o.push(i),t.put(i.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters},etag:i.headers.get("ETag"),lastModified:i.headers.get("Last-Modified")};r.cache.set(s,n),r.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),r.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}},o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}_saveItem(e,t){const s=this.stores.get(e),i=this.processForStorage(t,s.config.validateData);if(!i.valid)throw new Error(`Non-serializable data: ${i.error}`);const r=i.data,a=this.getItemKey(r,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:r,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),i=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(i.processed)})),this.notify(e,"item-saved",{item:i.item,key:i.key,previousItem:i.previousItem}),i.key}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const i=typeof e;if(["string","number","boolean"].includes(i))return{valid:!0,data:e};if("function"===i)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:null};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:null};if(e instanceof FormData)return t?{valid:!1,error:`FormData at ${s}`}:{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e))return{valid:!0,data:e};if(e instanceof Set){const i=Array.from(e);return this.processForStorage(i,t,s)}if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const i=[];for(let r=0;r<e.length;r++){const a=this.processForStorage(e[r],t,`${s}[${r}]`);if(!a.valid)return a;null!==a.data&&i.push(a.data)}return{valid:!0,data:i}}if("object"===i){const i={};for(const[r,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${r}`);if(!e.valid)return e;null!==e.data&&(i[r]=e.data)}return{valid:!0,data:i}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:null}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),i=t.cache.get(s);return i&&i.items?i.items.reduce(((e,s)=>{const i=t.data.get(s);return i&&e.push(i),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const i=this.stores.get(e),r={...i.filters};s?i.filters={...i.config.filters}:Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete i.filters[e]:i.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:r,filters:i.filters,updates:t}),i.config.endpoint&&await this.fetch(e)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e])))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const i=this.subscribers.get(e);i&&i.forEach((i=>{try{i(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let i=e;for(const e of s)i=i?.[e];return i}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/error.min.js b/assets/js/min/error.min.js
index e7d3937..26f65e8 100644
--- a/assets/js/min/error.min.js
+++ b/assets/js/min/error.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.options={apiUrl:"",logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3,...e},this.retryCount=0}async log(e,t={},o=null){console.error("API Error:",e,t);const r=this.getErrorType(e),n=this.getErrorMessage(e,r);switch(this.options.logToServer&&await this.logErrorToServer(r,n,t),r){case"network":case"server":if(this.options.retryEnabled&&this.retryCount<this.options.maxRetries&&o)return this.retryCount++,this.retryWithBackoff(o);break;case"auth":this.handleAuthError();break;case"rate_limit":return this.handleRateLimitError(o)}return this.options.displayNotifications&&this.displayErrorNotification(n,r,o),o&&this.options.retryEnabled||(this.retryCount=0),{success:!1,error:r,message:n,context:t}}getErrorType(e){if("AbortError"===e.name)return"timeout";if(!navigator.onLine)return"offline";if(e.response){const t=e.response.status;if(t>=400&&t<500)return 401===t||403===t?"auth":429===t?"rate_limit":"client";if(t>=500)return"server"}return"network"}getErrorMessage(e,t){const o={network:"We couldn't connect to the server. Please check your connection and try again.",timeout:"The request took too long to complete. Please try again.",offline:"You appear to be offline. Please check your internet connection.",auth:"Your session may have expired. Please log in again.",rate_limit:"You've made too many requests. Please wait a moment and try again.",server:"We're experiencing technical difficulties. Please try again later.",client:"Something went wrong with your request. Please try again.",unknown:"An unexpected error occurred. Please try again."};return e.response&&e.response.data&&e.response.data.message?e.response.data.message:e.message?e.message:o[t]||o.unknown}async logErrorToServer(e,t,o){try{if(!this.options.apiUrl)return;const r={...o,url:window.location.href,pathname:window.location.pathname,userAgent:navigator.userAgent,timestamp:(new Date).toISOString(),viewport:`${window.innerWidth}x${window.innerHeight}`,component:o.component||this.extractComponentFromStack(o.stack),method:o.method||this.extractMethodFromStack(o.stack),stack:o.stack||o.error?.stack,isLoggedIn:window.auth.isAuthenticated(),source:"frontend"},n=new FormData;n.append("error_type",e),n.append("message",t),n.append("context",JSON.stringify(r)),await fetch(`${this.options.apiUrl}errors/log`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:n})}catch(e){console.warn("Failed to log error to server",e)}}extractComponentFromStack(e){if(!e)return"Unknown";const t=e.match(/at\s+(\w+)\./);return t?t[1]:"Unknown"}extractMethodFromStack(e){if(!e)return null;const t=e.match(/at\s+\w+\.(\w+)\s+/);return t?t[1]:null}displayErrorNotification(e,t,o){if(window.jvbNotifications){const t=[];return o&&t.push({label:"Try Again",icon:"refresh",action:o}),void window.jvbNotifications.queuePopupNotification({type:"error",message:e,icon:"alert",priority:"high",displayDuration:this.options.notificationDuration,actions:t})}alert(e)}handleAuthError(){window.jvbSettings&&window.jvbSettings.loginUrl?window.location.href=window.jvbSettings.loginUrl:window.location.reload()}async handleRateLimitError(e){const t=2e3*(this.retryCount+1);if(await new Promise((e=>setTimeout(e,t))),e)return this.retryCount++,e()}async retryWithBackoff(e){const t=Math.min(1e3*Math.pow(2,this.retryCount),1e4);return this.options.displayNotifications&&this.displayRetryNotification(t),await new Promise((e=>setTimeout(e,t))),e()}displayRetryNotification(e){window.jvbNotifications&&window.jvbNotifications.queuePopupNotification({type:"info",message:`Retrying in ${e/1e3} seconds...`,icon:"refresh",priority:"medium",displayDuration:e})}resetRetryCount(){this.retryCount=0}collectUserFeedback(e){const t=document.createElement("dialog");return t.className="error-feedback-modal",t.innerHTML='\n            <h2>Help Us Improve</h2>\n            <p>We encountered an error. Would you like to tell us what happened?</p>\n            <form method="dialog" data-save="error">\n                <textarea placeholder="What were you trying to do when this error occurred?"></textarea>\n                <div class="actions">\n                    <button value="cancel">Skip</button>\n                    <button value="submit" class="primary">Send Feedback</button>\n                </div>\n            </form>\n        ',document.body.appendChild(t),new Promise((e=>{t.addEventListener("close",(()=>{const o="submit"===t.returnValue?t.querySelector("textarea").value:null;document.body.removeChild(t),e(o)})),t.showModal()}))}setupGlobalErrorHandling(){window.addEventListener("error",(e=>{this.log(e.error||new Error(e.message),{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno,type:"global_error"})})),window.addEventListener("unhandledrejection",(e=>{this.log(e.reason,{type:"unhandled_promise",message:e.reason?.message||"Unhandled promise rejection"})}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbError=new e({api:jvbSettings.api,logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3}))}))}))})();
\ No newline at end of file
+(()=>{class t{constructor(t={}){this.options={apiUrl:"",logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3,...t},this.retryCount=0}async log(t,e={},r=null){console.error("API Error:",t,e);const o=this.getErrorType(t),n=this.getErrorMessage(t,o);switch(this.options.logToServer&&await this.logErrorToServer(o,n,e),o){case"network":case"server":if(this.options.retryEnabled&&this.retryCount<this.options.maxRetries&&r)return this.retryCount++,this.retryWithBackoff(r);break;case"auth":this.handleAuthError();break;case"rate_limit":return this.handleRateLimitError(r)}return this.options.displayNotifications&&this.displayErrorNotification(n,o,r),r&&this.options.retryEnabled||(this.retryCount=0),{success:!1,error:o,message:n,context:e}}getErrorType(t){if("AbortError"===t.name)return"timeout";if(!navigator.onLine)return"offline";if(t.response){const e=t.response.status;if(e>=400&&e<500)return 401===e||403===e?"auth":429===e?"rate_limit":"client";if(e>=500)return"server"}return"network"}getErrorMessage(t,e){const r={network:"We couldn't connect to the server. Please check your connection and try again.",timeout:"The request took too long to complete. Please try again.",offline:"You appear to be offline. Please check your internet connection.",auth:"Your session may have expired. Please log in again.",rate_limit:"You've made too many requests. Please wait a moment and try again.",server:"We're experiencing technical difficulties. Please try again later.",client:"Something went wrong with your request. Please try again.",unknown:"An unexpected error occurred. Please try again."};return t.response&&t.response.data&&t.response.data.message?t.response.data.message:t.message?t.message:r[e]||r.unknown}async logErrorToServer(t,e,r){try{if(!this.options.apiUrl)return;const o={...r,url:window.location.href,pathname:window.location.pathname,userAgent:navigator.userAgent,timestamp:(new Date).toISOString(),viewport:`${window.innerWidth}x${window.innerHeight}`,component:r.component||this.extractComponentFromStack(r.stack),method:r.method||this.extractMethodFromStack(r.stack),stack:r.stack||r.error?.stack,isLoggedIn:window.auth.isAuthenticated(),source:"frontend"},n=new FormData;n.append("error_type",t),n.append("message",e),n.append("context",JSON.stringify(o)),await fetch(`${this.options.apiUrl}errors/log`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:n})}catch(t){console.warn("Failed to log error to server",t)}}extractComponentFromStack(t){if(!t)return"Unknown";const e=t.match(/at\s+(\w+)\./);return e?e[1]:"Unknown"}extractMethodFromStack(t){if(!t)return null;const e=t.match(/at\s+\w+\.(\w+)\s+/);return e?e[1]:null}displayErrorNotification(t,e,r){if(window.jvbNotifications){const e=[];return r&&e.push({label:"Try Again",icon:"refresh",action:r}),void window.jvbNotifications.queuePopupNotification({type:"error",message:t,icon:"alert",priority:"high",displayDuration:this.options.notificationDuration,actions:e})}alert(t)}handleAuthError(){window.jvbSettings&&window.jvbSettings.loginUrl?window.location.href=window.jvbSettings.loginUrl:window.location.reload()}async handleRateLimitError(t){const e=2e3*(this.retryCount+1);if(await new Promise((t=>setTimeout(t,e))),t)return this.retryCount++,t()}async retryWithBackoff(t){const e=Math.min(1e3*Math.pow(2,this.retryCount),1e4);return await new Promise((t=>setTimeout(t,e))),t()}resetRetryCount(){this.retryCount=0}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbError=new t({api:jvbSettings.api,logToServer:!0,displayNotifications:!0,notificationDuration:5e3,retryEnabled:!0,maxRetries:3}))}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index 95629ca..6fee3ca 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){this.config={collectFormData:!1,...e},this.isRestoring=!1;const t=window.jvbStore.register("forms",{storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4,validateData:!0,delayFetch:!0});this.store=t.forms,this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.isTimeline=window.crudManager&&window.crudManager.isTimeline,this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.blurHandler=this.handleBlur.bind(this),this.processRepeaterField=this.processRepeaterField.bind(this),this.processGroupField=this.processGroupField.bind(this),this.processLocationField=this.processLocationField.bind(this),this.processRegularField=this.processRegularField.bind(this),this.init()}async init(){this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners(),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.type&&this.handleOperationComplete(t)}))}async handleOperationComplete(e){if(e.formId)try{await this.store.delete(e.formId)}catch(e){console.warn("Failed to clear form cache:",e)}const t=this.forms.get(e.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){const e=await this.store.getAll(),t=window.location.pathname;e.filter((e=>{if("draft"!==e.status)return!1;const r=e.data?._wp_http_referer;return r===t})).forEach((e=>{const t=this.findFormElement(e);if(!t)return;let r=this.forms.get(e.formId);t.dataset.formId||(r=this.registerForm(t)),this.isRestoring=!0,new this.populateForm(t,e.data),setTimeout((()=>{this.isRestoring=!1}),0),this.showFormStatus(e.formId,"restored"),window.jvbA11y&&window.jvbA11y.announce("Your previous entry has been restored")}))}findFormElement(e){if(e.data?.form_id){const t=document.querySelector(`[name="form_id"][value="${e.data.form_id}"]`)?.closest("form");if(t)return t}if(e.data?.form_type){const t=document.querySelector(`[name="form_type"][value="${e.data.form_type}"]`)?.closest("form");if(t)return t}return document.querySelector(`[data-form-id="${e.formId}"]`)}showPendingNotification(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);if(!r)return;const s=document.createElement("div");s.className="pending-changes-notification",s.innerHTML=`\n        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore-changes" data-form-id="${e}">Restore</button>\n        <button class="discard-changes" data-form-id="${e}">Discard</button>\n    `,r.insertBefore(s,r.firstChild),s.querySelector(".restore-changes").addEventListener("click",(async()=>{await this.restorePendingForm(e,t),s.remove()})),s.querySelector(".discard-changes").addEventListener("click",(async()=>{await this.discardPendingForm(e),s.remove()}))}async restorePendingForm(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);r&&(new this.populateForm(r,t),await this.store.save({formId:e,data:t,status:"restored",timestamp:Date.now()}),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){try{await this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}catch(e){console.error("Failed to discard pending form:",e)}}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,status:"",options:{autosave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e,!0)};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.data&&this.showPendingNotification(r,e.data)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),this.initTagListFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=s.switchTab.bind(s);s.switchTab=(e,t)=>{c(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initTagListFields(e,t){e.querySelectorAll(".field.tag-list").forEach((e=>{const r=e.querySelector(".tag-input-row"),s=e.querySelector(".add-tag-item"),a=e.querySelector(".tag-items"),i=e.querySelector(".tag-template"),n=e.dataset.field,o=e.dataset.tagFormat||"first_field";if(!(r&&s&&a&&i))return;const l=()=>Array.from(r.querySelectorAll("input, select, textarea")).filter((e=>!e.closest("button"))),c=()=>{const r=l(),s={};let c=!1;if(r.forEach((e=>{const t=e.name.replace("new_",""),r=this.getFieldValue(e);r&&(c=!0),s[t]=r})),!c)return window.jvbA11y&&window.jvbA11y.announce("Please fill in at least one field","error"),void r[0].focus();const d=r.find((e=>{const t="required"in e.dataset&&"1"===e.dataset.required,r=this.getFieldValue(e);return t&&!r}));if(d){const e=d.closest(".field"),t=e?.querySelector("label")?.textContent||"This field";return this.showError(e,`${t} is required.`),void d.focus()}for(let t of r){let r=e.closest(".field");if(!this.validateField(t,r))return void t.focus()}const u=a.children.length,h=i.content.cloneNode(!0).firstElementChild;h.dataset.index=u;const m=h.querySelector(".tag-label");m&&(m.textContent=this.getTagDisplayText(s,o)),h.querySelectorAll('input[type="hidden"]').forEach((e=>{const t=e.dataset.field;e.name=`${n}:${u}:${t}`,e.value=s[t]||""})),a.appendChild(h),r.forEach((e=>{"checkbox"===e.type||"radio"===e.type?e.checked=!1:e.value="";let t=e.closest(".field");this.clearValidation(t)})),r.length>0&&r[0].focus(),t&&this.scheduleSave(t,{type:"tag_list",action:"add",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce("Item added")};s.addEventListener("click",c);const d=l();d.length>0&&(d[d.length-1].addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),c())})),d.slice(0,-1).forEach(((e,t)=>{e.addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),d[t+1].focus())}))}))),a.addEventListener("click",(e=>{if(e.target.closest(".remove-tag")){const r=e.target.closest(".tag-item"),s=r.querySelector(".tag-label")?.textContent||"Item";r.remove(),this.reindexTagList(a,n),t&&this.scheduleSave(t,{type:"tag_list",action:"remove",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce(`${s} removed`)}}))}))}reindexTagList(e,t){Array.from(e.children).forEach(((e,r)=>{e.dataset.index=r,e.querySelectorAll('input[type="hidden"]').forEach((e=>{const s=e.dataset.field;e.name=`${t}:${r}:${s}`}))}))}getTagDisplayText(e,t){const r=Object.values(e).filter((e=>e));if(0===r.length)return"New Item";switch(t){case"first_field":return r[0];case"all_fields":return r.join(", ");default:if(t.includes("{")){let r=t;for(const[t,s]of Object.entries(e))r=r.replace(`{${t}}`,s);return r}return e[t]||r[0]}}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s===a;case"!=":return s!==a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const r=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const s=this.collectFormData(t);this.notify("form-submit",{formId:t.dataset.formId,fullData:s,config:r})}else;}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const r=document.createElement("div");r.className="form-success-message success-message",r.textContent=t.message,e.insertBefore(r,e.firstChild);const s=window.getIcon?.("check-circle");s&&(s.classList.add("success-icon"),r.prepend(s))}if(t.title||t.description){const r=document.createElement("div");if(r.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,r.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,r.appendChild(t)}))}e.insertBefore(r,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const r=e.querySelector(`[data-field="${t.field}"]`);if(r){this.showError(r,t.message),this.touchedFields.add(t.field),r.scrollIntoView({behavior:"smooth",block:"center"});const e=r.querySelector("input, textarea, select");e&&e.focus()}}else{const r=document.createElement("div");r.className="form-error error-message",r.textContent=t.message;const s=window.getIcon?.("close-circle");s&&(s.classList.add("error-icon"),r.prepend(s)),e.insertBefore(r,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]"),r=t.dataset.action,s=t.closest("form");switch(r){case"clear-form":s?.dataset.formId&&(this.store.delete(s.dataset.formId),s.reset(),s.querySelector(".fstatus").hidden=!0),window.jvbA11y&&window.jvbA11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":s.querySelector(".fstatus").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(s&&(s.options.autosave||this.subscribers.size>0)){const e=s.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(s,a)}}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form")||this.isRestoring)return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,(()=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}showSuccess(e,t=""){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),r&&(r.hidden=!1),s&&(s.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let r=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const s=e.dataset.field;s&&this.touchedFields.add(s);this.validateField(t,e)||(r=!1,!1===r&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),r}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const r=t.target.closest("input, select");if(!r)return;const s=r.name;if(!s)return;e.querySelectorAll(`[data-show-if*="${s}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const r=e.dataset.field,s=e.querySelector(".validation-message");r&&s&&(t[r]=s.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")})).catch((t=>{console.error("Autosave failed:",t),this.showFormStatus(e.id,"error","Failed to save changes")}));const r=this.getChangedFields(e.data,t);if(0!==Object.keys(r).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.data,r);return Object.keys(s).length>0}showFormStatus(e,t,r=""){let s=this.forms.get(e);if(!s?.options.formStatus)return;if(s.status===t)return;s.status=t;const a=s.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove(),a.querySelector(".actions")?.remove();const n={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);if(o&&a.prepend(o),""===r&&(r=n[t]||t),i.textContent=r,a.classList.toggle("loading",["uploading","saving"].includes(t)),"restored"===t){const e=document.createElement("div");e.className="actions",e.innerHTML='\n            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>\n            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>\n        ',a.appendChild(e),setTimeout((()=>a.hidden=!0),1e4)}"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e,t=!1){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);if(e.classList.contains("table")&&"FORM"===e.tagName)return{};const r=new FormData(e);let s={};const a={},i={};for(let[t,n]of r.entries()){if(this.ignore.includes(t)||t.endsWith("_temp"))continue;this.getFieldProcessor(t)(t,n,s,a,i,e)}return 0!==Object.keys(i).length?(s=this.mergeRepeaterData(s,a),this.mergePostData(s,i)):this.mergeRepeaterData(s,a)}collectTimeline(e){let t={},r={},s=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)\](.+)$/);if(a){const[,t,o]=a;if(r[t]||(r[t]={id:parseInt(t)},s.push(t)),"post_thumbnail"===o)r[t].post_thumbnail=parseInt(e.querySelector(`[name="${i}"]`).closest(".item")?.dataset.id);else{this.getFieldProcessor(o)(o,n,r[t],{},{},e)}}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=s.map((e=>r[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],t}getFieldProcessor(e){return e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[r,s]of Object.entries(t))e[r]=s;return e}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),c||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary");if(!a)return;const i=a.querySelector(".result"),n=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(n.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;let o=i.cloneNode(!0),l=o.querySelector("h3"),c=o.querySelector("p");l.textContent=r.label;let d=this.formatFieldValue(t,r.type);this.isHtmlContent(d)?c.innerHTML=d:c.textContent=d,a.append(o)}i.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=e.querySelector(`[name=${t}]`),a=s?.closest(".field");if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){this.config={collectFormData:!1,...e},this.isRestoring=!1;const t=window.jvbStore.register("forms",{storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4,validateData:!0,delayFetch:!0});this.store=t.forms,this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.isTimeline=window.crudManager&&window.crudManager.isTimeline,this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.blurHandler=this.handleBlur.bind(this),this.processRepeaterField=this.processRepeaterField.bind(this),this.processGroupField=this.processGroupField.bind(this),this.processLocationField=this.processLocationField.bind(this),this.processRegularField=this.processRegularField.bind(this),this.init()}async init(){this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners(),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.type&&this.handleOperationComplete(t)}))}async handleOperationComplete(e){if(e.formId)try{await this.store.delete(e.formId)}catch(e){console.warn("Failed to clear form cache:",e)}const t=this.forms.get(e.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){const e=await this.store.getAll(),t=window.location.pathname;e.filter((e=>{if("draft"!==e.status)return!1;const r=e.data?._wp_http_referer;return r===t})).forEach((e=>{const t=this.findFormElement(e);if(!t)return;let r=this.forms.get(e.formId);t.dataset.formId||(r=this.registerForm(t)),this.isRestoring=!0,new this.populateForm(t,e.data),setTimeout((()=>{this.isRestoring=!1}),0),this.showFormStatus(e.formId,"restored"),window.jvbA11y&&window.jvbA11y.announce("Your previous entry has been restored")}))}findFormElement(e){if(e.data?.form_id){const t=document.querySelector(`[name="form_id"][value="${e.data.form_id}"]`)?.closest("form");if(t)return t}if(e.data?.form_type){const t=document.querySelector(`[name="form_type"][value="${e.data.form_type}"]`)?.closest("form");if(t)return t}return document.querySelector(`[data-form-id="${e.formId}"]`)}showPendingNotification(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);if(!r)return;const s=document.createElement("div");s.className="pending-changes-notification",s.innerHTML=`\n        <p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n        <button class="restore-changes" data-form-id="${e}">Restore</button>\n        <button class="discard-changes" data-form-id="${e}">Discard</button>\n    `,r.insertBefore(s,r.firstChild),s.querySelector(".restore-changes").addEventListener("click",(async()=>{await this.restorePendingForm(e,t),s.remove()})),s.querySelector(".discard-changes").addEventListener("click",(async()=>{await this.discardPendingForm(e),s.remove()}))}async restorePendingForm(e,t){const r=document.querySelector(`[data-form-id="${e}"]`);r&&(new this.populateForm(r,t),await this.store.save({formId:e,data:t,status:"restored",timestamp:Date.now()}),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){try{await this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}catch(e){console.error("Failed to discard pending form:",e)}}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,status:"",options:{autosave:"autosave"in e.dataset,autoUpload:!0,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e,!0)};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.data&&this.showPendingNotification(r,e.data)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),this.initTagListFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e,t),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const d=s.switchTab.bind(s);s.switchTab=(e,t)=>{d(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initTagListFields(e,t){e.querySelectorAll(".field.tag-list").forEach((e=>{const r=e.querySelector(".tag-input-row"),s=e.querySelector(".add-tag-item"),a=e.querySelector(".tag-items"),i=e.querySelector(".tag-template"),n=e.dataset.field,o=e.dataset.tagFormat||"first_field";if(!(r&&s&&a&&i))return;const l=()=>Array.from(r.querySelectorAll("input, select, textarea")).filter((e=>!e.closest("button"))),d=()=>{const r=l(),s={};let d=!1;if(r.forEach((e=>{const t=e.name.replace("new_",""),r=this.getFieldValue(e);r&&(d=!0),s[t]=r})),!d)return window.jvbA11y&&window.jvbA11y.announce("Please fill in at least one field","error"),void r[0].focus();const c=r.find((e=>{const t="required"in e.dataset&&"1"===e.dataset.required,r=this.getFieldValue(e);return t&&!r}));if(c){const e=c.closest(".field"),t=e?.querySelector("label")?.textContent||"This field";return this.showError(e,`${t} is required.`),void c.focus()}for(let t of r){let r=e.closest(".field");if(!this.validateField(t,r))return void t.focus()}const u=a.children.length,h=i.content.cloneNode(!0).firstElementChild;h.dataset.index=u;const m=h.querySelector(".tag-label");m&&(m.textContent=this.getTagDisplayText(s,o)),h.querySelectorAll('input[type="hidden"]').forEach((e=>{const t=e.dataset.field;e.name=`${n}:${u}:${t}`,e.value=s[t]||""})),a.appendChild(h),r.forEach((e=>{"checkbox"===e.type||"radio"===e.type?e.checked=!1:e.value="";let t=e.closest(".field");this.clearValidation(t)})),r.length>0&&r[0].focus(),t&&this.scheduleSave(t,{type:"tag_list",action:"add",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce("Item added")};s.addEventListener("click",d);const c=l();c.length>0&&(c[c.length-1].addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),d())})),c.slice(0,-1).forEach(((e,t)=>{e.addEventListener("keypress",(e=>{"Enter"===e.key&&(e.preventDefault(),c[t+1].focus())}))}))),a.addEventListener("click",(e=>{if(e.target.closest(".remove-tag")){const r=e.target.closest(".tag-item"),s=r.querySelector(".tag-label")?.textContent||"Item";r.remove(),this.reindexTagList(a,n),t&&this.scheduleSave(t,{type:"tag_list",action:"remove",fieldName:n,delay:this.autoSaveDefaults.delay}),window.jvbA11y&&window.jvbA11y.announce(`${s} removed`)}}))}))}reindexTagList(e,t){Array.from(e.children).forEach(((e,r)=>{e.dataset.index=r,e.querySelectorAll('input[type="hidden"]').forEach((e=>{const s=e.dataset.field;e.name=`${t}:${r}:${s}`}))}))}getTagDisplayText(e,t){const r=Object.values(e).filter((e=>e));if(0===r.length)return"New Item";switch(t){case"first_field":return r[0];case"all_fields":return r.join(", ");default:if(t.includes("{")){let r=t;for(const[t,s]of Object.entries(e))r=r.replace(`{${t}}`,s);return r}return e[t]||r[0]}}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s===a;case"!=":return s!==a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const r=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const s=this.collectFormData(t);this.notify("form-submit",{formId:t.dataset.formId,fullData:s,config:r})}}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const r=document.createElement("div");r.className="form-success-message success-message",r.textContent=t.message,e.insertBefore(r,e.firstChild);const s=window.getIcon?.("check-circle");s&&(s.classList.add("success-icon"),r.prepend(s))}if(t.title||t.description){const r=document.createElement("div");if(r.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,r.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,r.appendChild(t)}))}e.insertBefore(r,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const r=e.querySelector(`[data-field="${t.field}"]`);if(r){this.showError(r,t.message),this.touchedFields.add(t.field),r.scrollIntoView({behavior:"smooth",block:"center"});const e=r.querySelector("input, textarea, select");e&&e.focus()}}else{const r=document.createElement("div");r.className="form-error error-message",r.textContent=t.message;const s=window.getIcon?.("close-circle");s&&(s.classList.add("error-icon"),r.prepend(s)),e.insertBefore(r,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]"),r=t.dataset.action,s=t.closest("form");switch(r){case"clear-form":s?.dataset.formId&&(this.store.delete(s.dataset.formId),s.reset(),s.querySelector(".fstatus").hidden=!0),window.jvbA11y&&window.jvbA11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":s.querySelector(".fstatus").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(s&&(s.options.autosave||this.subscribers.size>0)){const e=s.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(s,a)}}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form")||this.isRestoring)return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,(()=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}showSuccess(e,t=""){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),r&&(r.hidden=!1),s&&(s.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")})).catch((t=>{console.error("Autosave failed:",t),this.showFormStatus(e.id,"error","Failed to save changes")}));const r=this.getChangedFields(e.data,t);if(0!==Object.keys(r).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.data,r);return Object.keys(s).length>0}showFormStatus(e,t,r=""){let s=this.forms.get(e);if(!s?.options.formStatus)return;if(s.status===t)return;s.status=t;const a=s.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove(),a.querySelector(".actions")?.remove();const n={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);if(o&&a.prepend(o),""===r&&(r=n[t]||t),i.textContent=r,a.classList.toggle("loading",["uploading","saving"].includes(t)),"restored"===t){const e=document.createElement("div");e.className="actions",e.innerHTML='\n            <button type="button" class="button button-small" data-action="dismiss-restore">Got it</button>\n            <button type="button" class="button button-small button-link" data-action="clear-form">Start over</button>\n        ',a.appendChild(e),setTimeout((()=>a.hidden=!0),1e4)}"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);if(e.classList.contains("table")&&"FORM"===e.tagName)return{};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 0!==Object.keys(a).length?(r=this.mergeRepeaterData(r,s),this.mergePostData(r,a)):this.mergeRepeaterData(r,s)}collectTimeline(e){let t={},r={},s=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)](.+)$/);if(a){const[,t,o]=a;if(r[t]||(r[t]={id:parseInt(t)},s.push(t)),"post_thumbnail"===o)r[t].post_thumbnail=parseInt(e.querySelector(`[name="${i}"]`).closest(".item")?.dataset.id);else{this.getFieldProcessor(o)(o,n,r[t],{},{},e)}}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=s.map((e=>r[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],t}getFieldProcessor(e){return e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[r,s]of Object.entries(t))e[r]=s;return e}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const d=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),d||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 d=n[n.length-1];void 0!==l[d]?(Array.isArray(l[d])||(l[d]=[l[d]]),l[d].push(t)):l[d]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary");if(!a)return;const i=a.querySelector(".result"),n=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(n.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;let o=i.cloneNode(!0),l=o.querySelector("h3"),d=o.querySelector("p");l.textContent=r.label;let c=this.formatFieldValue(t,r.type,s);this.isHtmlContent(c)?d.innerHTML=c:d.textContent=c,a.append(o)}let o=s.querySelectorAll("[data-upload-field]");o&&o.forEach((e=>{let t=e.querySelector("h2").textContent,r=e.querySelectorAll(".item-grid.preview img");if(r){let e=i.cloneNode(!0),s=e.querySelector("h3");e.querySelector("p").remove(),s.textContent=t,r.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),a.append(e)}})),i.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=e.querySelector(`[name=${t}]`);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, h2"))}let a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`),i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":e;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"upload":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>")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/gallery.min.js b/assets/js/min/gallery.min.js
index e30cc52..7c26669 100644
--- a/assets/js/min/gallery.min.js
+++ b/assets/js/min/gallery.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.index=0,this.images=[],this.zoom={scale:1,min:1,max:4,threshold:50,x:0,y:0,startX:0,startY:0,ease:.2,panning:!1},this.swipe=this.resetSwipe(),this.activePointers=new Map,this.lastTap=0,this.initElements(),this.initModal(),this.initListeners(),this.initSubscribers()}initElements(){this.elements={imageSelector:"img[data-gallery]",gallery:{modal:"dialog.gallery",wrap:".wrap",nextButton:".next",prevButton:".prev",image:".image",leftImage:".image-left",rightImage:".image-right",counter:".counter"}},this.ui=window.uiFromSelectors(this.elements)}initModal(){this.modal=new window.jvbModal(this.ui.gallery.modal,{openMessage:"Opened Gallery",closeMessage:"Closed Gallery"}),this.modal.subscribe(((e,t)=>{"modal-close"===e&&this.toggleGallery(!1)}))}buildGalleryItems(e=null){let t=e?`[data-gallery="${e}"]`:this.elements.imageSelector;this.items=Array.from(document.querySelectorAll(t)).map(((e,t)=>({id:e.dataset.id||t,srcset:e.srcset||e.src,sizes:e.sizes||"100vw",src:e.currentSrc||e.src,full:e.dataset.full||e.src,alt:e.alt||"",element:e})))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.pointerDownHandler=this.onPointerDown.bind(this),this.pointerMoveHandler=this.onPointerMove.bind(this),this.pointerUpHandler=this.onPointerUp.bind(this),this.wheelHandler=this.onWheel.bind(this),this.keyHandler=this.handleKeys.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){let t=window.targetCheck(e,this.elements.imageSelector);t&&!this.modal.isOpen?(e.preventDefault(),this.buildGalleryItems(t.dataset.gallery||null),this.index=this.items.findIndex((e=>e.element===t)),this.toggleGallery(!0)):this.modal.isOpen&&(window.targetCheck(e,this.elements.gallery.nextButton)?(console.log("Next"),this.nextElement()):window.targetCheck(e,this.elements.gallery.prevButton)&&(console.log("Previous"),this.prevElement()))}handleKeys(e){if(this.modal.isOpen){switch(e.key){case"ArrowLeft":e.preventDefault(),this.prevElement();break;case"ArrowRight":e.preventDefault(),this.nextElement()}e.ctrlKey&&("+"!==e.key&&"="!==e.key||(e.preventDefault(),this.handleZoom(.2)),"-"===e.key&&(e.preventDefault(),this.handleZoom(-.2)),"0"===e.key&&(e.preventDefault(),this.resetZoom()))}}onPointerDown(e){e.preventDefault(),this.swipe.startX=e.clientX,this.swipe.startY=e.clientY,this.ui.gallery.image.setPointerCapture(e.pointerId),this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=performance.now();if(t-this.lastTap<300&&1===this.activePointers.size)return this.zoom.scale>1?this.resetZoom():this.handleZoom(1,e.clientX,e.clientY),void(this.lastTap=0);if(this.lastTap=t,2===this.activePointers.size){const e=[...this.activePointers.values()];return this.pinchStartDist=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),void(this.pinchStartScale=this.zoom.scale)}this.zoom.scale>1&&(this.zoom.panning=!0,this.zoom.startX=e.clientX-this.zoom.x,this.zoom.startY=e.clientY-this.zoom.y,this.ui.gallery.image.style.cursor="grabbing")}onPointerMove(e){if(this.activePointers.has(e.pointerId))if(this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),2!==this.activePointers.size)this.zoom.panning&&(this.zoom.x=e.clientX-this.zoom.startX,this.zoom.y=e.clientY-this.zoom.startY,this.applyTransform());else{const e=[...this.activePointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),i=this.pinchStartScale*(t/this.pinchStartDist)-this.zoom.scale;this.handleZoom(i)}}onPointerUp(e){if(this.activePointers.delete(e.pointerId),this.activePointers.size<2&&(this.pinchStartDist=0),!this.zoom.panning&&0===this.activePointers.size){this.swipe.endX=e.clientX,this.swipe.endY=e.clientY;const t=this.swipe.endX-this.swipe.startX;this.swipe.endY,this.swipe.startY;Math.abs(t)>this.zoom.threshold&&(t>0?(console.log("Swipe right"),this.prevElement()):(console.log("Swipe left"),this.nextElement()))}0===this.activePointers.size&&(this.zoom.panning=!1,this.ui.gallery.image.style.cursor=this.zoom.scale>1?"grab":"default")}onWheel(e){if(!e.ctrlKey)return;e.preventDefault();const t=e.deltaY<0?.2:-.2;this.handleZoom(t,e.clientX,e.clientY)}clampPan(){const e=this.ui.gallery.wrap;if(!e)return;const t=e.getBoundingClientRect(),i=Math.min(t.width/1920,t.height/1920),s=1920*i,n=1920*i*this.zoom.scale,o=s*this.zoom.scale,l=t.width-n-32,r=t.height-o-32;this.zoom.x=Math.min(32,Math.max(l,this.zoom.x)),this.zoom.y=Math.min(32,Math.max(r,this.zoom.y))}handleZoom(e,t=null,i=null){const s=this.zoom.scale;let n=s+e;if(n=Math.min(this.zoom.max,Math.max(this.zoom.min,n)),n===s)return;const o=n/s;let l=this.ui.gallery.image.getBoundingClientRect();null!==t&&null!==i||(t=l.left+l.width/2,i=l.top+l.height/2);const r=t-l.left,a=i-l.top;this.zoom.x=(this.zoom.x-r)*o+r,this.zoom.y=(this.zoom.y-a)*o+a,this.zoom.scale=n,this.applyTransform(),this.notify("zoom",{scale:this.zoom.scale})}applyTransform(){const e=this.ui.gallery.image;e.style.transform=`translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`,e.style.cursor=this.zoom.scale>1?"grab":"default"}resetZoom(){this.zoom.scale=1,this.zoom.x=0,this.zoom.y=0,this.zoom.startX=0,this.zoom.startY=0,this.zoom.panning=!1,this.applyTransform()}resetSwipe(){return{startX:null,startY:null,endX:null,endY:null}}toggleGallery(e,t=null){e?(this.ui.gallery.image.draggable=!1,this.ui.gallery.image.style.userSelect="none",this.ui.gallery.image.addEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.addEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.addEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.addEventListener("pointercancel",this.pointerUpHandler),window.addEventListener("wheel",this.wheelHandler,{passive:!1}),window.addEventListener("keydown",this.keyHandler),this.moveIntoView()):(this.ui.gallery.image.removeEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.removeEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.removeEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.removeEventListener("pointercancel",this.pointerUpHandler),window.removeEventListener("wheel",this.wheelHandler),window.removeEventListener("keydown",this.keyHandler),this.resetZoom(),this.resetSwipe(),this.activePointers.clear(),this.lastTap=0),e&&!this.modal.isOpen&&this.modal.handleOpen()}moveIntoView(e=0){let t=this.index+e;t<0?t=this.items.length-1:t>=this.items.length?t=0:t===this.items.length-3&&this.notify("load-more"),this.index=t,this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}`)}nextElement(){this.resetZoom(),this.moveIntoView(1)}prevElement(){this.resetZoom(),this.moveIntoView(-1)}updateDisplay(){const e=this.items[this.index];if(!e)return;const t=this.ui.gallery.image;if(e.srcset&&(t.srcset=e.srcset,t.sizes=e.sizes),t.src=e.src,t.alt=e.alt,e.full&&e.full!==e.src){const i=new Image;i.onload=()=>{this.items[this.index]===e&&(t.src=e.full,t.removeAttribute("srcset"),t.removeAttribute("sizes"))},i.src=e.full}this.ui.gallery.counter.textContent=`${this.index+1} / ${this.items.length}`,this.ui.gallery.prevButton.disabled=this.items.length<=1,this.ui.gallery.nextButton.disabled=this.items.length<=1}preloadAdjacent(){[-1,1].forEach((e=>{const t=this.index+e;if(t>0&&t<this.items.length){const i=this.items[t];(e<0?this.ui.gallery.leftImage:this.ui.gallery.rightImage).src=i.full}}))}initSubscribers(){this.subscribers=new Set}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.toggleGallery(!1),document.removeEventListener("click",this.clickHandler)}}document.addEventListener("DOMContentLoaded",(function(){document.querySelector("dialog.gallery")&&(window.jvbGallery=new e)}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.index=0,this.images=[],this.zoom={scale:1,min:1,max:4,threshold:50,x:0,y:0,startX:0,startY:0,ease:.2,panning:!1},this.swipe=this.resetSwipe(),this.activePointers=new Map,this.lastTap=0,this.initElements(),this.initModal(),this.initListeners(),this.initSubscribers()}initElements(){this.elements={imageSelector:"img[data-gallery]",gallery:{modal:"dialog.gallery",wrap:".wrap",nextButton:".next",prevButton:".prev",image:".image",leftImage:".image-left",rightImage:".image-right",counter:".counter"}},this.ui=window.uiFromSelectors(this.elements)}initModal(){this.modal=new window.jvbModal(this.ui.gallery.modal,{openMessage:"Opened Gallery",closeMessage:"Closed Gallery"}),this.modal.subscribe((e=>{"modal-close"===e&&this.toggleGallery(!1)}))}buildGalleryItems(e=null){let t=e?`[data-gallery="${e}"]`:this.elements.imageSelector;this.items=Array.from(document.querySelectorAll(t)).map(((e,t)=>({id:e.dataset.id||t,srcset:e.srcset||e.src,sizes:e.sizes||"100vw",src:e.currentSrc||e.src,full:e.dataset.full||e.src,alt:e.alt||"",element:e})))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.pointerDownHandler=this.onPointerDown.bind(this),this.pointerMoveHandler=this.onPointerMove.bind(this),this.pointerUpHandler=this.onPointerUp.bind(this),this.wheelHandler=this.onWheel.bind(this),this.keyHandler=this.handleKeys.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){let t=window.targetCheck(e,this.elements.imageSelector);t&&!this.modal.isOpen?(e.preventDefault(),this.buildGalleryItems(t.dataset.gallery||null),this.index=this.items.findIndex((e=>e.element===t)),this.toggleGallery(!0)):this.modal.isOpen&&(window.targetCheck(e,this.elements.gallery.nextButton)?(console.log("Next"),this.nextElement()):window.targetCheck(e,this.elements.gallery.prevButton)&&(console.log("Previous"),this.prevElement()))}handleKeys(e){if(this.modal.isOpen){switch(e.key){case"ArrowLeft":e.preventDefault(),this.prevElement();break;case"ArrowRight":e.preventDefault(),this.nextElement()}e.ctrlKey&&("+"!==e.key&&"="!==e.key||(e.preventDefault(),this.handleZoom(.2)),"-"===e.key&&(e.preventDefault(),this.handleZoom(-.2)),"0"===e.key&&(e.preventDefault(),this.resetZoom()))}}onPointerDown(e){e.preventDefault(),this.swipe.startX=e.clientX,this.swipe.startY=e.clientY,this.ui.gallery.image.setPointerCapture(e.pointerId),this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY});const t=performance.now();if(t-this.lastTap<300&&1===this.activePointers.size)return this.zoom.scale>1?this.resetZoom():this.handleZoom(1,e.clientX,e.clientY),void(this.lastTap=0);if(this.lastTap=t,2===this.activePointers.size){const e=[...this.activePointers.values()];return this.pinchStartDist=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),void(this.pinchStartScale=this.zoom.scale)}this.zoom.scale>1&&(this.zoom.panning=!0,this.zoom.startX=e.clientX-this.zoom.x,this.zoom.startY=e.clientY-this.zoom.y,this.ui.gallery.image.style.cursor="grabbing")}onPointerMove(e){if(this.activePointers.has(e.pointerId))if(this.activePointers.set(e.pointerId,{x:e.clientX,y:e.clientY}),2!==this.activePointers.size)this.zoom.panning&&(this.zoom.x=e.clientX-this.zoom.startX,this.zoom.y=e.clientY-this.zoom.startY,this.applyTransform());else{const e=[...this.activePointers.values()],t=Math.hypot(e[0].x-e[1].x,e[0].y-e[1].y),i=this.pinchStartScale*(t/this.pinchStartDist)-this.zoom.scale;this.handleZoom(i)}}onPointerUp(e){if(this.activePointers.delete(e.pointerId),this.activePointers.size<2&&(this.pinchStartDist=0),!this.zoom.panning&&0===this.activePointers.size){this.swipe.endX=e.clientX,this.swipe.endY=e.clientY;const t=this.swipe.endX-this.swipe.startX;this.swipe.endY,this.swipe.startY;Math.abs(t)>this.zoom.threshold&&(t>0?(console.log("Swipe right"),this.prevElement()):(console.log("Swipe left"),this.nextElement()))}0===this.activePointers.size&&(this.zoom.panning=!1,this.ui.gallery.image.style.cursor=this.zoom.scale>1?"grab":"default")}onWheel(e){if(!e.ctrlKey)return;e.preventDefault();const t=e.deltaY<0?.2:-.2;this.handleZoom(t,e.clientX,e.clientY)}handleZoom(e,t=null,i=null){const s=this.zoom.scale;let n=s+e;if(n=Math.min(this.zoom.max,Math.max(this.zoom.min,n)),n===s)return;const l=n/s;let o=this.ui.gallery.image.getBoundingClientRect();null!==t&&null!==i||(t=o.left+o.width/2,i=o.top+o.height/2);const r=t-o.left,a=i-o.top;this.zoom.x=(this.zoom.x-r)*l+r,this.zoom.y=(this.zoom.y-a)*l+a,this.zoom.scale=n,this.applyTransform(),this.notify("zoom",{scale:this.zoom.scale})}applyTransform(){const e=this.ui.gallery.image;e.style.transform=`translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`,e.style.cursor=this.zoom.scale>1?"grab":"default"}resetZoom(){this.zoom.scale=1,this.zoom.x=0,this.zoom.y=0,this.zoom.startX=0,this.zoom.startY=0,this.zoom.panning=!1,this.applyTransform()}resetSwipe(){return{startX:null,startY:null,endX:null,endY:null}}toggleGallery(e){e?(this.ui.gallery.image.draggable=!1,this.ui.gallery.image.style.userSelect="none",this.ui.gallery.image.addEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.addEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.addEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.addEventListener("pointercancel",this.pointerUpHandler),window.addEventListener("wheel",this.wheelHandler,{passive:!1}),window.addEventListener("keydown",this.keyHandler),this.moveIntoView()):(this.ui.gallery.image.removeEventListener("pointerdown",this.pointerDownHandler),this.ui.gallery.image.removeEventListener("pointermove",this.pointerMoveHandler),this.ui.gallery.image.removeEventListener("pointerup",this.pointerUpHandler),this.ui.gallery.image.removeEventListener("pointercancel",this.pointerUpHandler),window.removeEventListener("wheel",this.wheelHandler),window.removeEventListener("keydown",this.keyHandler),this.resetZoom(),this.resetSwipe(),this.activePointers.clear(),this.lastTap=0),e&&!this.modal.isOpen&&this.modal.handleOpen()}moveIntoView(e=0){let t=this.index+e;t<0?t=this.items.length-1:t>=this.items.length?t=0:t===this.items.length-3&&this.notify("load-more"),this.index=t,this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}`)}nextElement(){this.resetZoom(),this.moveIntoView(1)}prevElement(){this.resetZoom(),this.moveIntoView(-1)}updateDisplay(){const e=this.items[this.index];if(!e)return;const t=this.ui.gallery.image;if(e.srcset&&(t.srcset=e.srcset,t.sizes=e.sizes),t.src=e.src,t.alt=e.alt,e.full&&e.full!==e.src){const i=new Image;i.onload=()=>{this.items[this.index]===e&&(t.src=e.full,t.removeAttribute("srcset"),t.removeAttribute("sizes"))},i.src=e.full}this.ui.gallery.counter.textContent=`${this.index+1} / ${this.items.length}`,this.ui.gallery.prevButton.disabled=this.items.length<=1,this.ui.gallery.nextButton.disabled=this.items.length<=1}preloadAdjacent(){[-1,1].forEach((e=>{const t=this.index+e;if(t>0&&t<this.items.length){const i=this.items[t];(e<0?this.ui.gallery.leftImage:this.ui.gallery.rightImage).src=i.full}}))}initSubscribers(){this.subscribers=new Set}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((i=>{try{i(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.toggleGallery(!1),document.removeEventListener("click",this.clickHandler)}}document.addEventListener("DOMContentLoaded",(function(){document.querySelector("dialog.gallery")&&(window.jvbGallery=new e)}))})();
\ No newline at end of file
diff --git a/assets/js/min/handleSelection.min.js b/assets/js/min/handleSelection.min.js
index f57924b..eb95d96 100644
--- a/assets/js/min/handleSelection.min.js
+++ b/assets/js/min/handleSelection.min.js
@@ -1 +1 @@
-window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.ui=e.ui||{},this.itemSelector=e.itemSelector||".item",this.checkboxSelector=e.checkboxSelector||'[name*="select-item"]',this.selectedItems=new Set,this.lastSelected=null,this.subscribers=new Set,this.init()}init(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("keydown",this.keyHandler)}handleKeys(e){(e.ctrlKey||e.metaKey)&&"a"===e.key&&(e.preventDefault(),this.ui.selectAll&&(this.ui.selectAll.checked=!0,this.selectAll(!0),window.jvbA11y&&window.jvbA11y.announce("All items selected"))),"Escape"===e.key&&this.selectedItems.size>0&&(this.selectAll(!1),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")),("Delete"===e.key||"Backspace"===e.key)&&!e.target.matches("input, textarea")&&this.selectedItems>0&&(e.preventDefault(),confirm(`Remove ${this.selectedItems.size} selected item${1!==this.selectedItems.size?"s":""}?`)&&this.deselect(this.selectedItems))}handleClick(e){const t=e.target.closest(`${this.checkboxSelector}, label[for]`);if(!t)return;const s="LABEL"===t.tagName?document.getElementById(t.getAttribute("for")):t;if(s)if(e.shiftKey&&this.lastSelected)e.preventDefault(),this.handleRangeSelection(s);else{const e=s.closest(this.itemSelector);e&&(this.lastSelected=e)}}handleChange(e){if(this.ui.selectAll&&e.target===this.ui.selectAll)this.selectAll(e.target.checked);else{const t=e.target.closest(this.checkboxSelector);if(!t)return;this.toggleSelection(this.getItemId(t))}}toggleSelection(e){if(!e)return;let t=!0;this.selectedItems.has(e)?(t=!1,this.selectedItems.delete(e)):this.selectedItems.add(e),t?this.notify("item-selected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}):this.notify("item-deselected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(this.itemSelector);e||(this.selectedItems.clear(),this.ui.selectAll&&(this.ui.selectAll.checked=!1)),t.forEach((t=>{const s=this.getItemId(t),i=t.querySelector(this.checkboxSelector);i&&(i.checked=e),e&&s&&this.selectedItems.add(s)})),this.notify("select-all",{container:this.container,selected:e,items:t}),this.updateSelectionUI()}clearSelection(){this.selectAll(!1)}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(this.itemSelector));const t=e.closest(this.itemSelector);if(!t)return;const s=Array.from(this.container.querySelectorAll(this.itemSelector)),i=s.indexOf(this.lastSelected),c=s.indexOf(t);if(-1===i||-1===c)return;const l=Math.min(i,c),n=Math.max(i,c);let h=!e.checked;for(let e=l;e<=n;e++){const t=s[e],i=t.querySelector(this.checkboxSelector),c=this.getItemId(t);i&&c&&(i.checked=h,this.selectedItems.add(c))}this.lastSelected=t,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:this.selectedItems,container:this.container});const r=n-l+1;window.jvbA11y&&window.jvbA11y.announce(`Selected ${r} items in range`)}updateSelectionUI(){const e=this.selectedItems.size,t=this.container.querySelectorAll(this.itemSelector).length;if(this.ui.bulkControls&&(this.ui.bulkControls.hidden=0===e),this.ui.count){const t=1===e?"item":"items";this.ui.count.textContent=0===e?"":`{ ${e} ${t} selected }`,this.ui.count.hidden=0===e}if(this.ui.selectAll){this.ui.selectAll.checked=t>0&&e===t,this.ui.selectAll.indeterminate=e>0&&e<t;const s=this.ui.selectAll.nextElementSibling||this.ui.selectAll.previousElementSibling;s&&"LABEL"===s.tagName&&(s.textContent=t>0&&e===t?"Clear Selection":"Select All")}}getItemId(e){const t=e.closest(this.itemSelector);return t?t.dataset.id||t.dataset.itemId||t.dataset.uploadId||t.id:null}getSelected(){return Array.from(this.selectedItems)}isSelected(e){return this.selectedItems.has(e)}select(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.add(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!0)}})),this.updateSelectionUI(),this.notify("item-selected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}deselect(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.delete(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!1)}})),this.updateSelectionUI(),this.notify("item-deselected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.container&&(this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("keydown",this.keyHandler)),this.clearSelection(),this.subscribers.clear(),this.container=null,this.ui=null,this.lastSelected=null}};
\ No newline at end of file
+window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.ui=e.ui||{},this.itemSelector=e.itemSelector||".item",this.checkboxSelector=e.checkboxSelector||'[name*="select-item"]',this.selectedItems=new Set,this.lastSelected=null,this.subscribers=new Set,this.init()}init(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("keydown",this.keyHandler)}handleKeys(e){(e.ctrlKey||e.metaKey)&&"a"===e.key&&(e.preventDefault(),this.ui.selectAll&&(this.ui.selectAll.checked=!0,this.selectAll(!0),window.jvbA11y&&window.jvbA11y.announce("All items selected"))),"Escape"===e.key&&this.selectedItems.size>0&&(this.selectAll(!1),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")),("Delete"===e.key||"Backspace"===e.key)&&!e.target.matches("input, textarea")&&this.selectedItems>0&&(e.preventDefault(),confirm(`Remove ${this.selectedItems.size} selected item${1!==this.selectedItems.size?"s":""}?`)&&this.deselect(this.selectedItems))}handleClick(e){const t=e.target.closest(`${this.checkboxSelector}, label[for]`);if(!t)return;const s="LABEL"===t.tagName?document.getElementById(t.getAttribute("for")):t;if(s)if(e.shiftKey&&this.lastSelected)e.preventDefault(),this.handleRangeSelection(s);else{const e=s.closest(this.itemSelector);e&&(this.lastSelected=e)}}handleChange(e){if(this.ui.selectAll&&e.target===this.ui.selectAll)this.selectAll(e.target.checked);else{const t=e.target.closest(this.checkboxSelector);if(!t)return;this.toggleSelection(this.getItemId(t))}}toggleSelection(e){if(!e)return;let t=!0;this.selectedItems.has(e)?(t=!1,this.selectedItems.delete(e)):this.selectedItems.add(e),t?this.notify("item-selected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}):this.notify("item-deselected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(this.itemSelector);e||(this.selectedItems.clear(),this.ui.selectAll&&(this.ui.selectAll.checked=!1)),t.forEach((t=>{const s=this.getItemId(t),i=t.querySelector(this.checkboxSelector);i&&(i.checked=e),e&&s&&this.selectedItems.add(s)})),this.notify("select-all",{container:this.container,selected:e,items:t}),this.updateSelectionUI()}clearSelection(){this.selectAll(!1)}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(this.itemSelector));const t=e.closest(this.itemSelector);if(!t)return;const s=Array.from(this.container.querySelectorAll(this.itemSelector)),i=s.indexOf(this.lastSelected),c=s.indexOf(t);if(-1===i||-1===c)return;const l=Math.min(i,c),n=Math.max(i,c);let h=!e.checked;for(let e=l;e<=n;e++){const t=s[e],i=t.querySelector(this.checkboxSelector),c=this.getItemId(t);i&&c&&(i.checked=h,this.selectedItems.add(c))}this.lastSelected=t,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:this.selectedItems,container:this.container});const r=n-l+1;window.jvbA11y&&window.jvbA11y.announce(`Selected ${r} items in range`)}updateSelectionUI(){const e=this.selectedItems.size,t=this.container.querySelectorAll(this.itemSelector).length;if(this.ui.bulkControls&&(this.ui.bulkControls.hidden=0===e),this.ui.count){const t=1===e?"item":"items";this.ui.count.textContent=0===e?"":`{ ${e} ${t} selected }`,this.ui.count.hidden=0===e}if(this.ui.selectAll){this.ui.selectAll.checked=t>0&&e===t,this.ui.selectAll.indeterminate=e>0&&e<t;const s=this.ui.selectAll.nextElementSibling||this.ui.selectAll.previousElementSibling;s&&"LABEL"===s.tagName&&(s.textContent=t>0&&e===t?"Clear Selection":"Select All")}}getItemId(e){const t=e.closest(this.itemSelector);return t?t.dataset.id||t.dataset.itemId||t.dataset.uploadId||t.id:null}isSelected(e){return this.selectedItems.has(e)}select(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.add(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!0)}})),this.updateSelectionUI(),this.notify("item-selected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}deselect(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.delete(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!1)}})),this.updateSelectionUI(),this.notify("item-deselected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.container&&(this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("keydown",this.keyHandler)),this.clearSelection(),this.subscribers.clear(),this.container=null,this.ui=null,this.lastSelected=null}};
\ No newline at end of file
diff --git a/assets/js/min/integrations.min.js b/assets/js/min/integrations.min.js
index 29c5549..6c5014a 100644
--- a/assets/js/min/integrations.min.js
+++ b/assets/js/min/integrations.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.initElements(),this.initListeners(),this.init()}initElements(){this.selectors={form:"form.integration",action:"data-action"};let e=document.querySelectorAll(this.selectors.form);this.forms=new Map,e.forEach((e=>{this.forms.set(e.dataset.service,e)}))}initListeners(){this.handleClick=this.clickHandler.bind(this),this.handleChange=this.changeHandler.bind(this),this.handleSubmit=this.submitHandler.bind(this),document.addEventListener("click",this.handleClick),document.addEventListener("change",this.handleChange),document.addEventListener("submit",this.handleSubmit)}init(){document.addEventListener("DOMContentLoaded",(()=>{this.checkForOAuthMessages()}))}checkForOAuthMessages(){const e=new URLSearchParams(window.location.search),t=e.get("success"),s=e.get("error");if(t){this.showNotification(t,"success",5e3),this.cleanURL();document.querySelectorAll("form.integration").forEach((e=>{this.updateUI(e,"connected")}))}else s&&(this.showNotification(s,"error",8e3),this.cleanURL())}cleanURL(){const e=new URL(window.location);e.searchParams.delete("success"),e.searchParams.delete("error"),window.history.replaceState({},document.title,e.pathname+e.hash)}showNotification(e,t="info",s=5e3){let o=document.querySelector(".integration-status-message");if(!o){o=document.createElement("div"),o.className="integration-status-message";const e=document.querySelector(".integration-settings")||document.querySelector("main")||document.body;e.insertBefore(o,e.firstChild)}o.textContent=e,o.className=`integration-status-message ${t}`,this.notificationTimeout&&clearTimeout(this.notificationTimeout),s>0&&(this.notificationTimeout=setTimeout((()=>{o.className="integration-status-message",o.textContent=""}),s)),this.popup&&this.addPopup(e,s)}addPopup(e,t=2e3){this.popup||(this.popup=document.querySelector(".integration-popup")||this.createPopupElement()),this.popup.textContent=e,this.popup.classList.add("showing"),setTimeout((()=>{this.popup.classList.remove("showing")}),t)}createPopupElement(){const e=document.createElement("div");return e.className="integration-popup",document.body.appendChild(e),e}clickHandler(e){if(e.target.closest(this.selectors.form)&&("BUTTON"===e.target.tagName||e.target.closest("button"))){e.preventDefault();let t="BUTTON"===e.target.tagName?e.target:e.target.closest("button");this.handleAction(t)}}changeHandler(e){if(e.target.closest(this.selectors.form))if("action"in e.target.dataset)this.handleAction(e.target);else{let t=this.getFormFromTarget(e.target);if(!t)return;t.classList.add("hasChanges"),t.querySelector(".setup .text").textContent="Unsaved Changes"}}submitHandler(e){e.target.closest(this.selectors.form)&&e.preventDefault()}getFormFromTarget(e){let t=e.closest("form")?.dataset.service;return this.forms.get(t)??!1}handleOAuthClick(e){const t=e.dataset.service,s=e.href,o=(screen.width-600)/2,i=(screen.height-700)/2;this.showNotification("Opening authorization window...","info"),e.classList.add("loading"),e.setAttribute("aria-busy","true");const n=window.open(s,"oauth_"+t,`width=600,height=700,left=${o},top=${i},toolbar=no,menubar=no,location=yes,status=yes,resizable=yes`);if(!n)return this.showNotification("Popup was blocked. Please allow popups and try again.","error"),e.classList.remove("loading"),e.removeAttribute("aria-busy"),!0;n.focus(),this.showNotification("Waiting for authorization...","info");const a=setInterval((()=>{try{n.closed&&(clearInterval(a),e.classList.remove("loading"),e.removeAttribute("aria-busy"),this.showNotification("Checking authorization status...","info"),setTimeout((()=>{this.checkForOAuthMessages(),setTimeout((()=>{const e=new URLSearchParams(window.location.search);e.has("success")||e.has("error")||window.location.reload()}),500)}),500))}catch(e){}}),500);return setTimeout((()=>{clearInterval(a),e.classList.remove("loading"),e.removeAttribute("aria-busy")}),3e5),!1}async handleAction(e){const t=e.closest("form"),s=t.dataset.service,o=e.dataset.action,i="BUTTON"===e.tagName,n=i&&"save_credentials"===o;if(!("confirm"in e.dataset)||confirm(e.dataset.confirm)){this.updateUI(t,"syncing");try{this.updateUI(t,"syncing");const a={service:s,action:o,user_id:window.auth.getUser(),data:{}};if(i||(a.data[e.name.replace(s+"_","")]=e.value),n){const e=new FormData(t);for(let[t,o]of e.entries())["service"].includes(t)||t.includes("nonce")||(a.data[t.replace(s+"_","")]=o)}const r=await fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(a)}),c=await r.json();if(r.ok&&c.success){let e="connected";switch(o){case"clear_credentials":e="disconnected";break;case"save_credentials":this.showNotification("Settings saved successfully","success")}this.updateUI(t,e),c.reload&&setTimeout((()=>{window.location.reload()}),50)}else this.updateUI(t,"error",c.message??""),this.showNotification(c.message||"Operation failed","error")}catch(e){this.updateUI(t,"error"),this.showNotification("Network error: "+e.message,"error"),console.error("API Error:",e)}}}updateUI(e,t,s=""){let o=["connected","disconnected","hasChanges","syncing","error"];if(!o.includes(t))return;s=""===s?{connected:"Set Up",disconnected:"Not Set Up",hasChanges:"Unsaved Changes",syncing:"Testing changes",error:"Something went wrong"}[t]:s,"syncing"===t?e.querySelectorAll("button").forEach((e=>{e.disabled=!0})):e.querySelectorAll("button[disabled]").forEach((e=>{e.disabled=!1})),e.classList.remove(...o),e.classList.add(t,"flash"),e.querySelector(".setup .text").textContent=s,"syncing"===t?e.querySelectorAll("button").forEach((e=>e.disabled=!0)):e.querySelectorAll("button:disabled").forEach((e=>e.disabled=!1)),setTimeout((()=>e.classList.remove("flash")),600)}}window.jvbOAuthPopup=function(e,t){const s=(window.screen.width-600)/2,o=(window.screen.height-700)/2;e+=(e.indexOf("?")>-1?"&":"?")+"popup=1";const i=window.open(e,t+"-oauth",`width=600,height=700,left=${s},top=${o},scrollbars=yes,resizable=yes,toolbar=no,menubar=no`);if(!i)return alert("Please allow popups for this site to complete the authorization process."),!1;window.jvbOAuthComplete=function(e,s,o){if(e===t)if(s){const e=document.querySelector(`.integration-card[data-service="${t}"] .setup .text`);e&&(e.textContent="Connection successful! Refreshing..."),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3)}else alert("OAuth authorization failed: "+(o||"Unknown error")),jvbRefreshIntegration(t)};const n=setInterval((()=>{try{i.closed&&(clearInterval(n),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3))}catch(e){}}),1e3);return!1},window.jvbRefreshIntegration=function(e){fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({service:e,action:"check_oauth_status"})}).then((e=>e.json())).then((t=>{if(console.log("OAuth status check result:",t),t.success&&t.authorized){const t=document.querySelector(`.integration-card[data-service="${e}"]`);if(t){t.classList.remove("disconnected"),t.classList.add("connected");const e=t.querySelector(".setup .text");e&&(e.textContent="Connected"),setTimeout((()=>{location.reload()}),1500)}}else location.reload()})).catch((e=>{console.error("Error checking OAuth status:",e),location.reload()}))},document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.integrations=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.initElements(),this.initListeners(),this.init()}initElements(){this.selectors={form:"form.integration",action:"data-action"};let e=document.querySelectorAll(this.selectors.form);this.forms=new Map,e.forEach((e=>{this.forms.set(e.dataset.service,e)}))}initListeners(){this.handleClick=this.clickHandler.bind(this),this.handleChange=this.changeHandler.bind(this),this.handleSubmit=this.submitHandler.bind(this),document.addEventListener("click",this.handleClick),document.addEventListener("change",this.handleChange),document.addEventListener("submit",this.handleSubmit)}init(){document.addEventListener("DOMContentLoaded",(()=>{this.checkForOAuthMessages()}))}checkForOAuthMessages(){const e=new URLSearchParams(window.location.search),t=e.get("success"),s=e.get("error");if(t){this.showNotification(t,"success",5e3),this.cleanURL();document.querySelectorAll("form.integration").forEach((e=>{this.updateUI(e,"connected")}))}else s&&(this.showNotification(s,"error",8e3),this.cleanURL())}cleanURL(){const e=new URL(window.location);e.searchParams.delete("success"),e.searchParams.delete("error"),window.history.replaceState({},document.title,e.pathname+e.hash)}showNotification(e,t="info",s=5e3){let n=document.querySelector(".integration-status-message");if(!n){n=document.createElement("div"),n.className="integration-status-message";const e=document.querySelector(".integration-settings")||document.querySelector("main")||document.body;e.insertBefore(n,e.firstChild)}n.textContent=e,n.className=`integration-status-message ${t}`,this.notificationTimeout&&clearTimeout(this.notificationTimeout),s>0&&(this.notificationTimeout=setTimeout((()=>{n.className="integration-status-message",n.textContent=""}),s)),this.popup&&this.addPopup(e,s)}addPopup(e,t=2e3){this.popup||(this.popup=document.querySelector(".integration-popup")||this.createPopupElement()),this.popup.textContent=e,this.popup.classList.add("showing"),setTimeout((()=>{this.popup.classList.remove("showing")}),t)}createPopupElement(){const e=document.createElement("div");return e.className="integration-popup",document.body.appendChild(e),e}clickHandler(e){if(e.target.closest(this.selectors.form)&&("BUTTON"===e.target.tagName||e.target.closest("button"))){e.preventDefault();let t="BUTTON"===e.target.tagName?e.target:e.target.closest("button");this.handleAction(t)}}changeHandler(e){if(e.target.closest(this.selectors.form))if("action"in e.target.dataset)this.handleAction(e.target);else{let t=this.getFormFromTarget(e.target);if(!t)return;t.classList.add("hasChanges"),t.querySelector(".setup .text").textContent="Unsaved Changes"}}submitHandler(e){e.target.closest(this.selectors.form)&&e.preventDefault()}getFormFromTarget(e){let t=e.closest("form")?.dataset.service;return this.forms.get(t)??!1}async handleAction(e){const t=e.closest("form"),s=t.dataset.service,n=e.dataset.action,o="BUTTON"===e.tagName,i=o&&"save_credentials"===n;if(!("confirm"in e.dataset)||confirm(e.dataset.confirm)){this.updateUI(t,"syncing");try{this.updateUI(t,"syncing");const a={service:s,action:n,user_id:window.auth.getUser(),data:{}};if(o||(a.data[e.name.replace(s+"_","")]=e.value),i){const e=new FormData(t);for(let[t,n]of e.entries())["service"].includes(t)||t.includes("nonce")||(a.data[t.replace(s+"_","")]=n)}const r=await fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(a)}),c=await r.json();if(r.ok&&c.success){let e="connected";switch(n){case"clear_credentials":e="disconnected";break;case"save_credentials":this.showNotification("Settings saved successfully","success")}this.updateUI(t,e),c.reload&&setTimeout((()=>{window.location.reload()}),50)}else this.updateUI(t,"error",c.message??""),this.showNotification(c.message||"Operation failed","error")}catch(e){this.updateUI(t,"error"),this.showNotification("Network error: "+e.message,"error"),console.error("API Error:",e)}}}updateUI(e,t,s=""){let n=["connected","disconnected","hasChanges","syncing","error"];if(!n.includes(t))return;s=""===s?{connected:"Set Up",disconnected:"Not Set Up",hasChanges:"Unsaved Changes",syncing:"Testing changes",error:"Something went wrong"}[t]:s,"syncing"===t?e.querySelectorAll("button").forEach((e=>{e.disabled=!0})):e.querySelectorAll("button[disabled]").forEach((e=>{e.disabled=!1})),e.classList.remove(...n),e.classList.add(t,"flash"),e.querySelector(".setup .text").textContent=s,"syncing"===t?e.querySelectorAll("button").forEach((e=>e.disabled=!0)):e.querySelectorAll("button:disabled").forEach((e=>e.disabled=!1)),setTimeout((()=>e.classList.remove("flash")),600)}}window.jvbOAuthPopup=function(e,t){const s=(window.screen.width-600)/2,n=(window.screen.height-700)/2;e+=(e.indexOf("?")>-1?"&":"?")+"popup=1";const o=window.open(e,t+"-oauth",`width=600,height=700,left=${s},top=${n},scrollbars=yes,resizable=yes,toolbar=no,menubar=no`);if(!o)return alert("Please allow popups for this site to complete the authorization process."),!1;window.jvbOAuthComplete=function(e,s,n){if(e===t)if(s){const e=document.querySelector(`.integration-card[data-service="${t}"] .setup .text`);e&&(e.textContent="Connection successful! Refreshing..."),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3)}else alert("OAuth authorization failed: "+(n||"Unknown error")),jvbRefreshIntegration(t)};const i=setInterval((()=>{try{o.closed&&(clearInterval(i),setTimeout((()=>{jvbRefreshIntegration(t)}),1e3))}catch(e){}}),1e3);return!1},window.jvbRefreshIntegration=function(e){fetch(jvbSettings.api+"integrations",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({service:e,action:"check_oauth_status"})}).then((e=>e.json())).then((t=>{if(console.log("OAuth status check result:",t),t.success&&t.authorized){const t=document.querySelector(`.integration-card[data-service="${e}"]`);if(t){t.classList.remove("disconnected"),t.classList.add("connected");const e=t.querySelector(".setup .text");e&&(e.textContent="Connected"),setTimeout((()=>{location.reload()}),1500)}}else location.reload()})).catch((e=>{console.error("Error checking OAuth status:",e),location.reload()}))},document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.integrations=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/navigation.min.js b/assets/js/min/navigation.min.js
index d5ff0b8..e34308d 100644
--- a/assets/js/min/navigation.min.js
+++ b/assets/js/min/navigation.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}navIDs(){return Array.from(this.navs.keys()).map((e=>`#${e}`))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"]');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"]');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/populate.min.js b/assets/js/min/populate.min.js
index 4ffdb88..d57507b 100644
--- a/assets/js/min/populate.min.js
+++ b/assets/js/min/populate.min.js
@@ -1 +1 @@
-window.jvbPopulate=class{constructor(e,t={},a={},r={}){for(let[l,o]of Object.entries(t)){let t=e.querySelector(`[data-field="${l}"]`);t&&this.populateField(t,l,o,a,r)}}populateField(e,t,a,r={},l={}){if(e&&null!=a)switch(this.getFieldType(e)){case"upload":case"gallery":case"image":this.populateUploadField(e,t,a,r);break;case"repeater":this.populateRepeaterField(e,t,a,l);break;case"taxonomy":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}getFieldType(e){if(e.dataset.fieldType)return e.dataset.fieldType;if(e.dataset.type)return e.dataset.type;const t=["upload","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateTextField(e,t,a){const r=e.querySelector(`[name="${t}"], input, textarea`);if((!r||"file"!==r.type)&&r&&(r.value=String(a||""),r.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=r.value.length)}}populateTextareaField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(r){if(r.value,r.value=String(a||""),r.dispatchEvent(new Event("change",{bubbles:!0})),r.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=r.value.length;const a=parseInt(r.dataset.limit,10);r.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="number"]`);r&&(r.value=Number(a)||0)}populateBooleanField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="checkbox"]`);r&&(r.checked=Boolean(a))}populateSelectField(e,t,a){const r=String(a||""),l=e.querySelector(`select[name="${t}"]`);if(l)return void(l.value=r);const o=e.querySelector(`input[type="radio"][name="${t}"][value="${r}"]`);o&&(o.checked=!0)}populateSetField(e,t,a){let r=a;if("string"==typeof a)try{r=JSON.parse(a)}catch(e){r=a.split(",").map((e=>e.trim()))}Array.isArray(r)||(r=[String(r)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=r.includes(e.value)}))}populateDateField(e,t,a){const r=e.querySelector(`[name="${t}"], input`);if(r&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(r.type){case"date":r.value=t.toISOString().split("T")[0];break;case"time":r.value=t.toTimeString().slice(0,5);break;case"datetime-local":r.value=t.toISOString().slice(0,16);break;default:r.value=e}}catch(t){r.value=e}}}populateEditorField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!r)return void console.warn(`Editor field ${t}: textarea not found`);const l=String(a||"");r.value=l;const o=e.querySelector(".editor");if(o){let e=null;if(o.__quill)e=o.__quill;else if(o.quill)e=o.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(o);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===o){e=t;break}e?(e.root.innerHTML=l,o.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),o.innerHTML=l)}else console.warn(`Editor container not found for ${t}`);r.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((r=>{if(void 0!==a[r]){const l=e.querySelector(`[name="${t}_${r}"], [name="${r}"]`);l&&(l.value=String(a[r]||""))}}))}populateTaxonomyField(e,t,a){let r=[];if(Array.isArray(a))r=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);r=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){r=a.split(",").map((e=>e.trim()))}else a&&(r=[String(a)]);if(0===r.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=r.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateUploadField(e,t,a,r={}){if("timeline"===e.dataset.subtype||"timeline"===t)return void this.populateTimelineGallery(e,t,a,r);if(!a)return;const l=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===l.length)return;const o=e.querySelector(`input[type="hidden"][name="${t}"]`);o&&(o.value=l.join(","));const i=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),i&&(window.removeChildren(i),l.forEach((e=>{const t=window.getTemplate("uploadItem");if(!t)return void console.warn("uploadItem template not found");let a=t.querySelector('input[name="select-item"]'),l=t.querySelector('label[for="select-item"]');t.dataset.id=e,a.name=a.name+`-${e}`,a.id=a.name,l.htmlFor=a.name;const o=t.querySelector("img");t.querySelector("video").remove();const n=t.querySelector("details");if(r[e]){const a=r[e];o&&(o.src=a.medium||a.small||a.large||"",o.alt=a["image-alt-text"]||a.alt||"");const l=t.querySelector('[name="image-title"]'),i=t.querySelector('[name="image-alt-text"]'),n=t.querySelector('[name="image-caption"]');l&&(l.value=a["image-title"]||a.title||""),i&&(i.value=a["image-alt-text"]||a.alt||""),n&&(n.value=a["image-caption"]||a.caption||"")}else console.warn("No image data found for ID:",e);n?.querySelector(".upload-meta > .hint")?.remove(),i.append(t)})),l.length>0&&n&&(n.hidden=!0))}populateTimelineGallery(e,t,a,r){if(!a||"object"!=typeof a)return;const l=Object.values(a);if(0===l.length)return;const o=e.querySelector(".item-grid"),i=e.querySelector(".file-upload-container");if(e.querySelector(".progress")?.remove(),o){window.removeChildren(o),console.log(l);for(let[e,t]of Object.entries(a)){let e=t.post_thumbnail;const a=window.getTemplate("timelineItem");if(!a)return;const l=a.querySelector("img");a.querySelector("video")?.remove(),a.querySelector(".select-item span")?.remove();let i=a.querySelector('input[name="select-item"]'),n=a.querySelector('label[for="select-item"]');a.dataset.id=e,a.dataset.postId=t.id,i.name=i.name+`-${e}`,i.id=i.name,n.htmlFor=i.name;const c=r[e];l&&c&&(l.src=c.medium||c.small||c.large||"",l.title=c["image-title"]);const s={...t,...c};a.querySelectorAll(".field").forEach((e=>{if(e.classList.contains("group"))return;const a=e.querySelector('input:not([type="file"]), textarea');if(!a)return;const l=e.querySelector("label"),o=a.name.replace("upload_data::",""),i=s[o];this.populateField(e,o,i,r);const n=`[${t.id}]${o}`;a.name=n,a.id=n,l&&(l.htmlFor=n)})),o.append(a)}l.length>0&&i&&(i.hidden=!0)}}populateRepeaterField(e,t,a,r={}){if(!a||!Array.isArray(a))return;const l=e.querySelector(".repeater-items"),o=e.querySelector("template");l&&o?(window.removeChildren(l),a.forEach(((a,r)=>{if(!a||"object"!=typeof a)return;const i=window.getTemplate(o.className);if(!i)return void console.warn(`Repeater field ${t}: template not found`);i.id=`${e.closest("form").id}-${t}-row-${r}`,i.dataset.index=r;const n=i.querySelector(".row-number");n&&(n.textContent=`#${r+1}`),i.querySelectorAll("input, select, textarea").forEach((e=>{const l=e.name,o=`${t}:${r}:${l}`,i=`${t}-${r}-${l}-${e.value}`;e.name=o,e.id=i;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=i),void 0!==a[l]&&this.populateRepeaterFieldValue(e,l,a[l])})),l.appendChild(i)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}};
\ No newline at end of file
+window.jvbPopulate=class{constructor(e,t={},a={},l={}){for(let[r,o]of Object.entries(t)){let t=e.querySelector(`[data-field="${r}"]`);t&&this.populateField(t,r,o,a,l)}}populateField(e,t,a,l={},r={}){if(e&&null!=a)switch(this.getFieldType(e)){case"upload":case"gallery":case"image":this.populateUploadField(e,t,a,l);break;case"repeater":this.populateRepeaterField(e,t,a,r);break;case"taxonomy":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}getFieldType(e){if(e.dataset.fieldType)return e.dataset.fieldType;if(e.dataset.type)return e.dataset.type;const t=["upload","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateTextField(e,t,a){const l=e.querySelector(`[name="${t}"], input, textarea`);if((!l||"file"!==l.type)&&l&&(l.value=String(a||""),l.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=l.value.length)}}populateTextareaField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(l){if(l.value=String(a||""),l.dispatchEvent(new Event("change",{bubbles:!0})),l.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=l.value.length;const a=parseInt(l.dataset.limit,10);l.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="number"]`);l&&(l.value=Number(a)||0)}populateBooleanField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="checkbox"]`);l&&(l.checked=Boolean(a))}populateSelectField(e,t,a){const l=String(a||""),r=e.querySelector(`select[name="${t}"]`);if(r)return void(r.value=l);const o=e.querySelector(`input[type="radio"][name="${t}"][value="${l}"]`);o&&(o.checked=!0)}populateSetField(e,t,a){let l=a;if("string"==typeof a)try{l=JSON.parse(a)}catch(e){l=a.split(",").map((e=>e.trim()))}Array.isArray(l)||(l=[String(l)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=l.includes(e.value)}))}populateDateField(e,t,a){const l=e.querySelector(`[name="${t}"], input`);if(l&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(l.type){case"date":l.value=t.toISOString().split("T")[0];break;case"time":l.value=t.toTimeString().slice(0,5);break;case"datetime-local":l.value=t.toISOString().slice(0,16);break;default:l.value=e}}catch(t){l.value=e}}}populateEditorField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!l)return void console.warn(`Editor field ${t}: textarea not found`);const r=String(a||"");l.value=r;const o=e.querySelector(".editor");if(o){let e=null;if(o.__quill)e=o.__quill;else if(o.quill)e=o.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(o);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===o){e=t;break}e?(e.root.innerHTML=r,o.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),o.innerHTML=r)}else console.warn(`Editor container not found for ${t}`);l.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((l=>{if(void 0!==a[l]){const r=e.querySelector(`[name="${t}_${l}"], [name="${l}"]`);r&&(r.value=String(a[l]||""))}}))}populateTaxonomyField(e,t,a){let l=[];if(Array.isArray(a))l=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);l=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){l=a.split(",").map((e=>e.trim()))}else a&&(l=[String(a)]);if(0===l.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=l.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateUploadField(e,t,a,l={}){if("timeline"===e.dataset.subtype||"timeline"===t)return void this.populateTimelineGallery(e,t,a,l);if(!a)return;const r=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===r.length)return;const o=e.querySelector(`input[type="hidden"][name="${t}"]`);o&&(o.value=r.join(","));const i=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),i&&(window.removeChildren(i),r.forEach((e=>{const t=window.getTemplate("uploadItem");if(!t)return void console.warn("uploadItem template not found");let a=t.querySelector('input[name="select-item"]'),r=t.querySelector('label[for="select-item"]');t.dataset.id=e,a.name=a.name+`-${e}`,a.id=a.name,r.htmlFor=a.name;const o=t.querySelector("img");t.querySelector("video").remove();const n=t.querySelector("details");if(l[e]){const a=l[e];o&&(o.src=a.medium||a.small||a.large||"",o.alt=a["image-alt-text"]||a.alt||"");const r=t.querySelector('[name="image-title"]'),i=t.querySelector('[name="image-alt-text"]'),n=t.querySelector('[name="image-caption"]');r&&(r.value=a["image-title"]||a.title||""),i&&(i.value=a["image-alt-text"]||a.alt||""),n&&(n.value=a["image-caption"]||a.caption||"")}else console.warn("No image data found for ID:",e);n?.querySelector(".upload-meta > .hint")?.remove(),i.append(t)})),r.length>0&&n&&(n.hidden=!0))}populateTimelineGallery(e,t,a,l){if(console.log("populating Timeline Gallery"),!a||"object"!=typeof a)return;const r=Object.values(a);if(0===r.length)return;const o=e.querySelector(".item-grid"),i=e.querySelector(".file-upload-container");if(e.querySelector(".progress")?.remove(),o){window.removeChildren(o),console.log(r);for(let e of Object.entries(a)){let t=e.post_thumbnail;const a=window.getTemplate("timelineItem");if(!a)return;const r=a.querySelector("img");a.querySelector("video")?.remove(),a.querySelector(".select-item span")?.remove();let i=a.querySelector('input[name="select-item"]'),n=a.querySelector('label[for="select-item"]');a.dataset.id=t,a.dataset.postId=e.id,i.name=i.name+`-${t}`,i.id=i.name,n.htmlFor=i.name;const c=l[t];r&&c&&(r.src=c.medium||c.small||c.large||"",r.title=c["image-title"]);const s={...e,...c};a.querySelectorAll(".field").forEach((t=>{if(t.classList.contains("group"))return;const a=t.querySelector('input:not([type="file"]), textarea');if(!a)return;const r=t.querySelector("label"),o=a.name.replace("upload_data::",""),i=s[o];this.populateField(t,o,i,l);const n=`[${e.id}]${o}`;a.name=n,a.id=n,r&&(r.htmlFor=n)})),o.append(a)}r.length>0&&i&&(i.hidden=!0)}}populateRepeaterField(e,t,a){if(!a||!Array.isArray(a))return;const l=e.querySelector(".repeater-items"),r=e.querySelector("template");l&&r?(window.removeChildren(l),a.forEach(((a,o)=>{if(!a||"object"!=typeof a)return;const i=window.getTemplate(r.className);if(!i)return void console.warn(`Repeater field ${t}: template not found`);i.id=`${e.closest("form").id}-${t}-row-${o}`,i.dataset.index=o;const n=i.querySelector(".row-number");n&&(n.textContent=`#${o+1}`),i.querySelectorAll("input, select, textarea").forEach((e=>{const l=e.name,r=`${t}:${o}:${l}`,i=`${t}-${o}-${l}-${e.value}`;e.name=r,e.id=i;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=i),void 0!==a[l]&&this.populateRepeaterFieldValue(e,l,a[l])})),l.appendChild(i)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}};
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index c49e5dd..719e968 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){if(this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.user=window.auth.getUser(),!this.user)return console.log("Queue: User not logged in, queue disabled"),this.store=null,void(this.canUpdateUI=!1);this.headers={"X-WP-Nonce":window.auth.getNonce(),...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const t=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1});this.store=t.queue,this.classes=["offline","synced","pending"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.updateUI=()=>window.debouncer.schedule("queue-ui-update",this._updateUI.bind(this),100),this.initQueue()}async initQueue(){const e=this.getOperationsByStatus(["completed","failed_permanent"],!1);e.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":if(t.item){const e=this.store.data.get(t.item.id);e&&e.status!==t.item.status&&this.handleOperationStatusChange(t.item,e.status)}this.hasQueuedOperations()&&this.startPolling();break;default:this.updateUI()}})),this.notify("queue-initialized",{operations:e})}handleOperationStatusChange(e,t){if(e&&t!==e.status)switch(e.status){case"completed":this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.get(e);this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}hideQueue(){this.ui.panel.hidden=!0,this.ui.toggle.hidden=!0}showQueue(){this.ui.panel.hidden=!1,this.ui.toggle.hidden=!1}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0===e.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0?(this.startPolling(),this.showQueue()):this.hideQueue()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading"),e.data?._isFormData&&(e.data=await this.store.objectToFormData(e.data));const t=`${this.config.apiBase}${e.endpoint}`;let s;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),s=e.data):(s=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const i=await fetch(t,{method:e.method,headers:e.headers,body:s}),a=await i.json();if(!i.ok||!1===a.success)throw new Error(a.message||`HTTP ${i.status}`);if(a.id&&e.id!==a.id){const t=this.getQueue(a.id);t?(t.data=window.deepMerge(t.data,e.data),t.status=a.status||"pending",t.serverData=a,this.updateOperationStatus(t.id,t.status),this.setQueue(t),this.removeOperationFromUI(e.id),e=t):(this.clearQueue(e.id),e.id=a.id,e.status=a.status||"pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e))}else e.status=a.status||"pending",e.serverData=a,this.updateOperationStatus(e.id,e.status),this.setQueue(e);this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(e,t){if(0===(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length)return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationFromUI(e)));try{const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:window.auth.getUser()})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);this.notify(`${t}-operation`,i),s?this.clearQueue(e):(i.status="queued",i.retries=0,this.setQueue(i),this.updateOperationStatus(i.id,i.status))})),"retry"===t&&this.startActivityTracking(),this.updateUI(),a}catch(s){return await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t))),{success:!1,error:s.message}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return e.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationsByStatus("completed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"dismiss")}}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationsByStatus("failed");if(e.length>0){const t=e.map((e=>e.id));this.updateServerOperations(t,"retry")}}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}initUI(){this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}_updateUI(){if(!this.canUpdateUI)return;const e=Array.from(this.store.data.values()),t=this.store.lastResponse?.queue_stats||{queued:0,localProcessing:0,uploading:0,pending:0,processing:0,completed:0,failed:0,failed_permanent:0};if(this.ui.count){const s=e.length-t.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}this.ui.clearButton.disabled=0===this.getOperationsByStatus("completed").length,this.ui.retryButton.disabled=0===this.getOperationsByStatus("failed").length&&0===this.getOperationsByStatus("failed_permanent").length,Object.entries(this.ui.filters).forEach((([s,i])=>{const a="all"===s?e.length:t[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===e.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else e.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.append(t)}))}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),d=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),d&&(d.style.width=`${i}%`);const l=t.querySelector(".actions");l&&this.updateActionButtons(e,l)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}getOperationsByStatus(e,t=!0){return Array.isArray(e)||"string"!=typeof e||(e=[e]),t?Array.from(this.store.data.values()).filter((t=>e.includes(t.status))):Array.from(this.store.data.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){if(this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.user=window.auth.getUser(),!this.user)return console.log("Queue: User not logged in, queue disabled"),this.store=null,void(this.canUpdateUI=!1);this.headers={"X-WP-Nonce":window.auth.getNonce(),...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const t=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1});this.store=t.queue,this.classes=["offline","synced","pending"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.updateUI=()=>window.debouncer.schedule("queue-ui-update",this._updateUI.bind(this),100),this.initQueue()}async initQueue(){this.maybeStartPolling()||this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.maybeStartPolling(),this.updateUI();break;case"item-saved":console.log(t,"Item saved data"),t.previousItem&&t.previousItem.status!==t.item.status&&this.handleOperationStatusChange(t.item,t.previousItem.status),this.maybeStartPolling();break;default:this.updateUI()}}))}maybeStartPolling(){return this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&(this.startPolling(),!0)}handleOperationStatusChange(e){switch(e.status){case"completed":console.log(e),this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}hideQueue(){this.ui.panel.hidden=!0,this.ui.toggle.hidden=!0}showQueue(){this.ui.panel.hidden=!1,this.ui.toggle.hidden=!1}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0!==e.length){this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.maybeStartPolling()?this.showQueue():this.hideQueue()}else this.stopActivityTracking()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading"),e.data?._isFormData&&(e.data=await this.store.objectToFormData(e.data));const t=`${this.config.apiBase}${e.endpoint}`;let s;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),s=e.data):(s=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const i=await fetch(t,{method:e.method,headers:e.headers,body:s}),a=await i.json();if(!i.ok||!1===a.success)throw new Error(a.message||`HTTP ${i.status}`);a.id&&e.id!==a.id?e=await this.handleServerMerge(e,a):(e.status=a.status||"pending",e.serverData=a,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(s.data=window.deepMerge(s.data,e.data),s.status=t.status||"pending",s.serverData=t,this.updateOperationStatus(s.id,s.status),this.removeOperationFromUI(e.id),this.clearQueue(e.id),s):(this.clearQueue(e.id),e.id=t.id,e.status=t.status||"pending",e.serverData=t,this.updateOperationStatus(e.id,e.status),e)}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}getOperationIds(e){return e.map((e=>e.id))}async updateServerOperations(e,t){if(0===(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length)return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationFromUI(e)));try{const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:window.auth.getUser()})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);this.notify(`${t}-operation`,i),s?this.clearQueue(e):(i.status="queued",i.retries=0,this.setQueue(i),this.updateOperationStatus(i.id,i.status))})),"retry"===t&&this.startActivityTracking(),this.updateUI(),a}catch(s){return await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t))),{success:!1,error:s.message}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.isPolling||this.isProcessing)return e.preventDefault(),"You have unsaved changes in the queue. Proceed?"},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationIds(this.getOperationsByStatus("completed"));e.length>0&&this.updateServerOperations(e,"dismiss")}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationIds(this.getOperationsByStatus("failed"));e.length>0&&this.updateServerOperations(e,"retry")}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}initUI(){this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}_updateUI(){if(!this.canUpdateUI)return;const e=Array.from(this.store.data.values()),t=this.store.lastResponse?.queue_stats||{queued:0,localProcessing:0,uploading:0,pending:0,processing:0,completed:0,failed:0,failed_permanent:0};if(this.ui.count){const s=e.length-t.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}this.ui.clearButton.disabled=0===this.getOperationsByStatus("completed").length,this.ui.retryButton.disabled=0===this.getOperationsByStatus("failed").length&&0===this.getOperationsByStatus("failed_permanent").length,Object.entries(this.ui.filters).forEach((([s,i])=>{const a="all"===s?e.length:t[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===e.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else e.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.append(t)}))}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),l=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const d=t.querySelector(".actions");d&&this.updateActionButtons(e,d)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}getOperationsByStatus(e,t=!0){return Array.isArray(e)||"string"!=typeof e||(e=[e]),t?Array.from(this.store.data.values()).filter((t=>e.includes(t.status))):Array.from(this.store.data.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/quill.min.js b/assets/js/min/quill.min.js
index d36eccd..2c1bcb9 100644
--- a/assets/js/min/quill.min.js
+++ b/assets/js/min/quill.min.js
@@ -1 +1 @@
-window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const o=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        <i class="icon icon-paragraph"></i>\n                    </button>\n                    <button type="button" class="ql-h1">\n                        <i class="icon icon-text-h-one"></i>\n                    </button>\n                    <button type="button" class="ql-h2">\n                        <i class="icon icon-text-h-two"></i>\n                    </button>\n                    <button type="button" class="ql-h3">\n                        <i class="icon icon-text-h-three"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        <i class="icon icon-text-b-fi"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        <i class="icon icon-text-italic"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        <i class="icon icon-text-underline"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        <i class="icon icon-text-strikethrough"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        <i class="icon icon-list-dashes"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        <i class="icon icon-list-numbers"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        <i class="icon icon-text-align-left"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        <i class="icon icon-text-align-center"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        <i class="icon icon-text-align-right"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        <i class="icon icon-link"></i>\n                    </button>\n                    ${o}\n                </span>\n            `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const o=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;this.quill.getText(t.index,t.length);const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const o=e.querySelector(".remove");o&&o.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),o=new FormData;o.append("image",e),objectID&&o.append("post_id",objectID),window.jvbLoading&&window.jvbLoading.showLoading("Uploading image...","Processing Upload");try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:o});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{window.jvbLoading&&window.jvbLoading.hide(),t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});o.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),o.on("text-change",(()=>{t.value=o.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))};
\ No newline at end of file
+window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const l=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${window.getIcon("image")}\n                </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        <i class="icon icon-paragraph"></i>\n                    </button>\n                    <button type="button" class="ql-h1">\n                        <i class="icon icon-text-h-one"></i>\n                    </button>\n                    <button type="button" class="ql-h2">\n                        <i class="icon icon-text-h-two"></i>\n                    </button>\n                    <button type="button" class="ql-h3">\n                        <i class="icon icon-text-h-three"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        <i class="icon icon-text-b-fi"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        <i class="icon icon-text-italic"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        <i class="icon icon-text-underline"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        <i class="icon icon-text-strikethrough"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        <i class="icon icon-list-dashes"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        <i class="icon icon-list-numbers"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        <i class="icon icon-text-align-left"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        <i class="icon icon-text-align-center"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        <i class="icon icon-text-align-right"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        <i class="icon icon-link"></i>\n                    </button>\n                    ${l}\n                </span>\n            `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const l=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const l=e.querySelector(".remove");l&&l.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),l=new FormData;l.append("image",e),objectID&&l.append("post_id",objectID);try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":window.auth.getNonce()},body:l});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});l.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),l.on("text-change",(()=>{t.value=l.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))};
\ No newline at end of file
diff --git a/assets/js/min/referral.min.js b/assets/js/min/referral.min.js
index ff24a90..b712ec0 100644
--- a/assets/js/min/referral.min.js
+++ b/assets/js/min/referral.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("aside.referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.hasCopy=navigator.clipboard&&navigator.clipboard.writeText,this.initElements(),this.storesInited=!1,this.initStore(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copyBtn:".copy-btn",checkCode:".check-code-btn",submit:"[type=submit]",recentList:".recent-referrals-list",invite:"form.invite",adminList:".items-list.referral",dash:".replace .referral-dashboard",stats:{codeUsed:'[data-stat="code_used"]',consultations:'[data-stat="consultations"]',treatments:'[data-stat="treatments"]',rewards:'[data-stat="total_rewards"]'},list:".referrals-list"},this.forms=this.container.querySelectorAll("form"),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.bindEventListeners(!0)},onClose:()=>{this.bindEventListeners(!1)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors),this.dashTabs=null,this.ui.dash&&(this.dashTabs=new window.jvbTabs(this.ui.dash)),this.hasCopy||document.querySelectorAll(this.selectors.copyBtn).forEach((e=>{e.remove()})),this.formController=null,this.ui.invite&&(this.formController=new window.jvbForm,this.formController.registerForm(this.ui.invite,{autosave:!0,endpoint:"referrals",formStatus:!1}),this.formController.subscribe(((e,t)=>{"form-submit"===e&&((t=t.fullData).action="invite",window.jvbQueue.addToQueue({endpoint:"referrals",data:t,title:"Submitting invitations"}))})))}initStore(){if(!this.isLoggedIn())return;const e=window.jvbStore.register("referrals",[{storeName:"stats",keyPath:"user_id",endpoint:"referrals/stats",TTL:3e5,showLoading:!1,delayFetch:!1,filters:{type:"dashboard",user:window.auth.getUser()}},{storeName:"list",keyPath:"id",endpoint:"referrals",TTL:6e5,showLoading:!1,delayFetch:!1,filters:{user:window.auth.getUser(),status:"all",limit:50,offset:0}}]);this.statsStore=e.stats,this.listStore=e.list,this.statsStore&&this.statsStore.subscribe(this.handleStatsEvent.bind(this)),this.listStore&&this.listStore.subscribe(this.handleListEvent.bind(this)),this.ui.dash&&this.initViewController()}initViewController(){this.listStore&&this.ui.adminList&&(this.view=new window.jvbViews(this.ui.adminList,this.listStore),this.view.subscribe(((e,t)=>{switch(e){case"item-action":this.handleItemAction(t);break;case"bulk-action":this.handleBulkAction(t)}})))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this)}bindEventListeners(e){const t=e?"addEventListener":"removeEventListener";this.forms.forEach((e=>{e[t]("submit",this.submitHandler)})),this.container[t]("click",this.clickHandler),this.container[t]("input",this.inputHandler)}isLoggedIn(){return Boolean(window.auth.getUser())}handleStatsEvent(e,t){switch(e){case"data-loaded":t.items&&t.items.length>0&&this.updateStatsDisplay();break;case"fetch-error":console.error("Error loading stats:",t.error)}}handleListEvent(e,t){switch(e){case"data-loaded":this.ui.recentList&&this.renderRecentReferrals();break;case"fetch-error":console.error("Error loading referrals:",t.error)}}updateStatsDisplay(){if(0===!this.statsStore.data.size)return;let e=this.statsStore.data.get(parseInt(window.auth.getUser()));const t={total:e.code_used||0,treated:e.treatments||0,pending:e.pending||0,rewards:"$"+parseFloat(e.total_rewards||0).toFixed(2)};Object.entries(t).forEach((([e,t])=>{const s=this.container.querySelector(`[data-stat="${e}"]`);s&&(s.textContent=t)}));const s=this.container.querySelectorAll(".stats .card");s.length>=4&&(s[0].querySelector(".stat-number").textContent=t.code_used,s[1].querySelector(".stat-number").textContent=t.consultations,s[2].querySelector(".stat-number").textContent=t.treatments,s[3].querySelector(".stat-number").textContent=t.total_rewards)}handleItemAction(e){const{action:t,itemId:s}=e;switch(t){case"remove":this.removeReferral(s);break;case"resend":this.resendInvite(s)}}async removeReferral(e){if(confirm("Remove this referral from your list?"))try{const t=await fetch(`${jvbSettings.api}referrals`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({action:"remove",referral_id:e})});(await t.json()).success&&(this.listStore&&this.listStore.fetch(),this.statsStore&&this.statsStore.fetch(),this.a11y?.announce("Referral removed"))}catch(e){console.error("Error removing referral:",e)}}async resendInvite(e){try{const t=await fetch(`${jvbSettings.api}referrals`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({action:"resend",referral_id:e})}),s=await t.json();s.success?this.a11y?.announce("Invitation resent"):alert(s.message||"Cannot resend yet. Wait 7 days between invites.")}catch(e){console.error("Error resending invite:",e)}}handleClick(e){const t=e.target.closest(".copy-btn, .check-code-btn, .attn");t&&(t.classList.contains("copy-btn")?this.handleCopyClick(t):t.classList.contains("check-code-btn")?this.handleCheckCode(e):t.classList.contains("attn")&&t.classList.remove("attn"))}handleCopyClick(e){const t=e.dataset.target,s=this.container.querySelector(`#${t}`);if(!s)return;const r=s.textContent.trim();this.hasCopy&&navigator.clipboard.writeText(r).then((()=>{this.showCopySuccess(e)})).catch((()=>{this.selectText(s),this.showCopyFallback(e)}))}selectText(e){if(window.getSelection&&document.createRange){const t=window.getSelection(),s=document.createRange();s.selectNodeContents(e),t.removeAllRanges(),t.addRange(s)}else if(document.body.createTextRange){const t=document.body.createTextRange();t.moveToElementText(e),t.select()}}showCopySuccess(e){const t=e.innerHTML;e.innerHTML=window.jvbIcon("check",{size:16})+" Copied!",e.classList.add("success"),setTimeout((()=>{e.innerHTML=t,e.classList.remove("success")}),2e3)}showCopyFallback(e){const t=e.innerHTML;e.innerHTML="✓ Selected - Press Ctrl+C",e.classList.add("selected"),setTimeout((()=>{e.innerHTML=t,e.classList.remove("selected")}),3e3)}handleInput(e){"referral_code"!==e.target.id&&"referral_code"!==e.target.name||(e.target.value=e.target.value.toUpperCase())}async handleCheckCode(e){e.preventDefault();const t=e.target.closest("form"),s=t.querySelector('[name="referral_code"]'),r=t.querySelector(".code-status");if(!s||!r)return;const a=s.value.trim();if(a){r.hidden=!1,r.className="code-status loading",r.innerHTML='<span class="spinner"></span> Checking...';try{const e=await this.validateCodeOnly(a);e.success?this.showCodeStatus(r,`✓ Valid! Referred by ${e.referrer_name}`,"success"):this.showCodeStatus(r,e.message||"Invalid code","error")}catch(e){console.error("Error checking code:",e),this.showCodeStatus(r,"Error checking code","error")}}else this.showCodeStatus(r,"Please enter a code","error")}showCodeStatus(e,t,s){e.hidden=!1,e.className=`code-status ${s}`,e.textContent=t,"error"===s&&setTimeout((()=>{e.hidden=!0}),5e3)}async checkForReferral(){const e=this.getUrlParameter("ref"),t=this.getUrlParameter("rname"),s=this.getUrlParameter("remail"),r=this.getUrlParameter("seeReferral");if(!e&&!r)return;if(r&&!e)return this.popup.openPopup(),void this.removeUrlParameter("seeReferral");const a=this.container.querySelector('[name="referral_code"]');if(!a)return;const n=e.toUpperCase();if(a.value=n,a.readOnly=!0,t||s){const e=this.container.querySelector('[name="referral_name"]');e&&(e.value=t);const r=this.container.querySelector('[name="referral_email"]');r&&(r.value=s)}this.popup.openPopup();try{const e=await this.validateCodeOnly(n);if(e.success){const t=a.closest("form").querySelector(".code-status");t&&this.showCodeStatus(t,`✓ ${e.referrer_name} invited you!`,"success");const s=this.container.querySelector('[name="referral_name"]');s&&!s.value&&s.focus()}else a.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),a.readOnly=!1}this.removeUrlParameter("ref"),this.removeUrlParameter("rname"),this.removeUrlParameter("remail")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}referrals/code`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({code:e})});return await t.json()}async loadStats(){if(this.container.querySelector(".stats-summary"))try{const e=await fetch(`${jvbSettings.api}referrals/my-stats?user=${window.auth.getUser()}`,{headers:{"X-WP-Nonce":window.auth.getNonce()}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}async loadSidebarStats(){try{const e=await fetch(`${jvbSettings.api}referrals/stats?user=${window.auth.getUser()}&type=quick`,{headers:{"X-WP-Nonce":window.auth.getNonce()}}),t=await e.json();t.success&&t.stats&&this.updateSidebarStats(t.stats)}catch(e){console.error("Error loading sidebar stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}renderRecentReferrals(){let e=this.ui.recentList,t=Array.from(this.listStore.data.values());t&&0!==t.length?e.innerHTML=t.map((e=>`\n\t\t\t<div class="referral-item">\n\t\t\t\t<div class="referral-info">\n\t\t\t\t\t<strong>${window.escapeHtml(e.referee_name)}</strong>\n\t\t\t\t\t<span class="status-badge">${e.referral_status}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class="referral-date">${window.formatTimeAgo(e.referred_at)}</div>\n\t\t\t</div>\n\t\t`)).join(""):e.innerHTML='<p class="no-referrals">Share your code to get started!</p>'}formatDate(e){const t=new Date(e),s=new Date,r=Math.abs(s-t),a=Math.floor(r/864e5);return 0===a?"Today":1===a?"Yesterday":a<7?`${a} days ago`:t.toLocaleDateString("en-US",{month:"short",day:"numeric"})}async handleFormSubmit(e){e.preventDefault();const t=e.target,s=new FormData(t);this.setFormLoading(!0,t);try{let e={success:!1,message:""};if("referral-code-form"===t.id){const t={name:s.get("referral_name"),email:s.get("referral_email"),referral_code:s.get("referral_code")};t.name&&t.email&&t.referral_code?e=await this.makeRequest("auth/register",t):e.message="Please fill in all fields"}else if("login-form"===t.id){const t={type:"login",email:s.get("login_email"),context:{redirect_to:window.location.href+"?seeReferral=1"}};e=await this.makeRequest("magic",t)}e.success?this.handleSuccess(t,e):this.showFormMessage(t,e.message||"Something went wrong. Please try again.","error")}catch(e){console.error("Error submitting form:",e),this.showFormMessage(t,"Something went wrong. Please try again.","error")}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic","auth/register"].includes(e))return{success:!1,message:"Invalid endpoint"};const s=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(t)});if(!s.ok){const e=await s.text();console.error("Error response:",s.status,e);try{return JSON.parse(e)}catch{return{success:!1,message:"Server error"}}}return await s.json()}handleSuccess(e,t){e.style.display="none";const s=e.nextElementSibling;s&&s.classList.contains("success-content")&&(s.hidden=!1,s.scrollIntoView({behavior:"smooth",block:"center"})),this.dispatchEvent("emailSent",{email:t.email})}showFormMessage(e,t,s="error"){const r=e.querySelector(".status");if(!r)return;const a=r.querySelector(".message");a&&(a.textContent=t),r.hidden=!1,r.className=`status ${s}`,"error"===s&&setTimeout((()=>{r.hidden=!0}),5e3)}setFormLoading(e,t){t.querySelectorAll("input, button").forEach((t=>t.disabled=e));const s=t.querySelector(".status");if(s&&(s.classList.toggle("loading",e),e)){s.hidden=!1;const e=s.querySelector(".message");e&&(e.textContent="Sending...")}}dispatchEvent(e,t){const s=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(s)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbReferral=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("aside.referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.hasCopy=navigator.clipboard&&navigator.clipboard.writeText,this.initElements(),this.initStore(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copyBtn:".copy-btn",checkCode:".check-code-btn",submit:"[type=submit]",recentList:".recent-referrals-list",stats:{codeUsed:'[data-stat="code_used"]',consultations:'[data-stat="consultations"]',treatments:'[data-stat="treatments"]',rewards:'[data-stat="total_rewards"]'}},this.forms=this.container.querySelectorAll("form"),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.bindEventListeners(!0)},onClose:()=>{this.bindEventListeners(!1)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors),this.hasCopy||document.querySelectorAll(this.selectors.copyBtn).forEach((e=>{e.remove()})),this.formController=null}initStore(){if(!this.isLoggedIn())return;const e=window.jvbStore.register("referrals",[{storeName:"stats",keyPath:"user_id",endpoint:"referrals/stats",TTL:3e5,showLoading:!1,delayFetch:!1,filters:{type:"dashboard",user:window.auth.getUser()}},{storeName:"list",keyPath:"id",endpoint:"referrals",TTL:6e5,showLoading:!1,delayFetch:!1,filters:{user:window.auth.getUser(),status:"all",limit:50,offset:0}}]);this.statsStore=e.stats,this.listStore=e.list,this.statsStore&&this.statsStore.subscribe(this.handleStatsEvent.bind(this)),this.listStore&&this.listStore.subscribe(this.handleListEvent.bind(this))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this)}bindEventListeners(e){const t=e?"addEventListener":"removeEventListener";this.forms.forEach((e=>{e[t]("submit",this.submitHandler)})),this.container[t]("click",this.clickHandler),this.container[t]("input",this.inputHandler)}isLoggedIn(){return Boolean(window.auth.getUser())}handleStatsEvent(e,t){switch(e){case"data-loaded":t.items&&t.items.length>0&&this.updateStatsDisplay();break;case"fetch-error":console.error("Error loading stats:",t.error)}}handleListEvent(e,t){switch(e){case"data-loaded":this.ui.recentList&&this.renderRecentReferrals();break;case"fetch-error":console.error("Error loading referrals:",t.error)}}updateStatsDisplay(){if(0===!this.statsStore.data.size)return;let e=this.statsStore.data.get(parseInt(window.auth.getUser()));const t={total:e.code_used||0,treated:e.treatments||0,pending:e.pending||0,rewards:"$"+parseFloat(e.total_rewards||0).toFixed(2)};Object.entries(t).forEach((([e,t])=>{const r=this.container.querySelector(`[data-stat="${e}"]`);r&&(r.textContent=t)}));const r=this.container.querySelectorAll(".stats .card");r.length>=4&&(r[0].querySelector(".stat-number").textContent=t.code_used,r[1].querySelector(".stat-number").textContent=t.consultations,r[2].querySelector(".stat-number").textContent=t.treatments,r[3].querySelector(".stat-number").textContent=t.total_rewards)}handleClick(e){const t=e.target.closest(".copy-btn, .check-code-btn, .attn");t&&(t.classList.contains("copy-btn")?this.handleCopyClick(t):t.classList.contains("check-code-btn")?this.handleCheckCode(e):t.classList.contains("attn")&&t.classList.remove("attn"))}handleCopyClick(e){const t=e.dataset.target,r=this.container.querySelector(`#${t}`);if(!r)return;const s=r.textContent.trim();this.hasCopy&&navigator.clipboard.writeText(s).then((()=>{e.classList.toggle("success"),setTimeout((()=>{e.classList.remove("success")}),1500)}))}handleError(e,t){const{message:r,code:s,field:a}=t;switch(a?this.showFieldError(e,a,r):this.showFormStatus(e,"error",r||"Something went wrong. Please try again."),s){case"duplicate_email":break;case"invalid_code":const t=e.querySelector('[name="referral_code"]');t&&(t.readOnly=!1,t.focus());break;case"turnstile_failed":window.turnstile&&e.querySelector(".cf-turnstile")&&window.turnstile.reset()}}showFieldError(e,t,r){let s=e.querySelector(`.field[data-field="${t}"]`);if(s||(s=e.querySelector(`.field[data-field="referral_${t}"]`)),!s)return void this.showFormStatus(e,"error",r);const a=s.querySelector("input, textarea, select"),n=s.querySelector(".validation-message"),i=s.querySelector(".validation-icon.error"),o=s.querySelector(".validation-icon.success");a?(s.classList.remove("has-success"),s.classList.add("has-error"),a.classList.add("error"),a.setAttribute("aria-invalid","true"),i&&(i.hidden=!1),o&&(o.hidden=!0),n&&(n.textContent=r,n.hidden=!1),a.focus(),this.a11y?.announce(`Error in ${t}: ${r}`)):this.showFormStatus(e,"error",r)}showFormStatus(e,t,r=""){const s=e.querySelector(".fstatus");if(!s)return void console.warn("No .fstatus element found in form");s.hidden=!1;const a=s.querySelector(".message");s.querySelector(".icon")?.remove(),s.querySelector(".actions")?.remove();const n={saving:"Sending...",submitted:"Sent successfully!",error:"Something went wrong",checking:"Checking code..."},i={submitted:"check-circle",error:"close-circle",checking:"loading"};if(i[t]&&window.getIcon){const e=window.getIcon(i[t]);e&&s.prepend(e)}a&&(a.textContent=r||n[t]||t),s.classList.toggle("loading",["saving","checking"].includes(t)),"submitted"===t&&setTimeout((()=>s.hidden=!0),3e3),this.a11y&&this.a11y.announce(r||n[t]||t)}clearFormErrors(e){e.querySelectorAll(".field.has-error, .field.has-success").forEach((e=>{this.clearFieldValidation(e)}));const t=e.querySelector(".fstatus");t&&(t.hidden=!0)}clearFieldValidation(e){if(!e)return;const t=e.querySelector("input, textarea, select"),r=e.querySelector(".validation-message"),s=e.querySelectorAll(".validation-icon");e.classList.remove("has-error","has-success"),t&&(t.classList.remove("error"),t.removeAttribute("aria-invalid")),s.forEach((e=>e.hidden=!0)),r&&(r.hidden=!0,r.textContent="")}handleInput(e){"referral_code"!==e.target.id&&"referral_code"!==e.target.name||(e.target.value=e.target.value.toUpperCase());const t=e.target.closest(".field");t&&t.classList.contains("has-error")&&this.clearFieldValidation(t)}async handleCheckCode(e){e.preventDefault();const t=e.target.closest("form"),r=t.querySelector('[name="referral_code"]'),s=t.querySelector(".code-status");if(!r||!s)return;const a=r.value.trim();if(a){s.hidden=!1,s.className="code-status loading",s.innerHTML='<span class="spinner"></span> Checking...';try{const e=await this.validateCodeOnly(a);e.success?this.showCodeStatus(s,`✓ Valid! Referred by ${e.referrer_name}`,"success"):this.showCodeStatus(s,e.message||"Invalid code","error")}catch(e){console.error("Error checking code:",e),this.showCodeStatus(s,"Error checking code","error")}}else this.showCodeStatus(s,"Please enter a code","error")}showCodeStatus(e,t,r){e.hidden=!1,e.className=`code-status ${r}`,e.textContent=t,"error"===r&&setTimeout((()=>{e.hidden=!0}),5e3)}async checkForReferral(){const e=this.getUrlParameter("ref"),t=this.getUrlParameter("rname"),r=this.getUrlParameter("remail"),s=this.getUrlParameter("seeReferral");if(!e&&!s)return;if(s&&!e)return this.popup.openPopup(),void this.removeUrlParameter("seeReferral");const a=this.container.querySelector('[name="referral_code"]');if(!a)return;const n=e.toUpperCase();if(a.value=n,a.readOnly=!0,t||r){const e=this.container.querySelector('[name="referral_name"]');e&&(e.value=t);const s=this.container.querySelector('[name="referral_email"]');s&&(s.value=r)}this.popup.openPopup();try{const e=await this.validateCodeOnly(n);if(e.success){const t=a.closest("form").querySelector(".code-status");t&&this.showCodeStatus(t,`✓ ${e.referrer_name} invited you!`,"success");const r=this.container.querySelector('[name="referral_name"]');r&&!r.value&&r.focus()}else a.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),a.readOnly=!1}this.removeUrlParameter("ref"),this.removeUrlParameter("rname"),this.removeUrlParameter("remail")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}referrals/code`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({code:e})});return await t.json()}renderRecentReferrals(){let e=this.ui.recentList,t=Array.from(this.listStore.data.values());t&&0!==t.length?e.innerHTML=t.map((e=>`\n\t\t\t<div class="referral-item">\n\t\t\t\t<div class="referral-info">\n\t\t\t\t\t<strong>${window.escapeHtml(e.referee_name)}</strong>\n\t\t\t\t\t<span class="status-badge">${e.referral_status}</span>\n\t\t\t\t</div>\n\t\t\t\t<div class="referral-date">${window.formatTimeAgo(e.referred_at)}</div>\n\t\t\t</div>\n\t\t`)).join(""):e.innerHTML='<p class="no-referrals">Share your code to get started!</p>'}async handleFormSubmit(e){e.preventDefault();const t=e.target,r=new FormData(t);this.clearFormErrors(t),this.setFormLoading(!0,t);try{let e={success:!1,message:""};if("referral-code-form"===t.id){let t={name:r.get("referral_name"),email:r.get("referral_email"),referral_code:r.get("referral_code")};r.get("cf-turnstile-response")&&(t["cf-turnstile-response"]=r.get("cf-turnstile-response")),t.name&&t.email&&t.referral_code?e=await this.makeRequest("auth/register",t):e.message="Please fill in all fields"}else if("login-form"===t.id){let t={type:"login",email:r.get("login_email"),context:{redirect_to:window.location.href+"?seeReferral=1"}};r.get("cf-turnstile-response")&&(t["cf-turnstile-response"]=r.get("cf-turnstile-response")),t.email?e=await this.makeRequest("magic",t):e.message="Please fill in your email"}e.success?this.handleSuccess(t,e):this.handleError(t,e)}catch(e){console.error("Error submitting form:",e),this.showFormMessage(t,"Something went wrong. Please try again.","error")}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic","auth/register"].includes(e))return{success:!1,message:"Invalid endpoint"};const r=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify(t)});if(!r.ok){const e=await r.text();console.error("Error response:",r.status,e);try{return JSON.parse(e)}catch{return{success:!1,message:"Server error"}}}return await r.json()}handleSuccess(e,t){e.style.display="none";const r=e.nextElementSibling;r&&r.classList.contains("success-content")&&(r.hidden=!1,r.scrollIntoView({behavior:"smooth",block:"center"})),this.dispatchEvent("emailSent",{email:t.email})}showFormMessage(e,t,r="error"){const s=e.querySelector(".status");if(!s)return;const a=s.querySelector(".message");a&&(a.textContent=t),s.hidden=!1,s.className=`status ${r}`,"error"===r&&setTimeout((()=>{s.hidden=!0}),5e3)}setFormLoading(e,t){t.querySelectorAll("input, button, textarea, select").forEach((t=>t.disabled=e)),e&&this.showFormStatus(t,"saving")}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbReferral=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/referralAdmin.min.js b/assets/js/min/referralAdmin.min.js
new file mode 100644
index 0000000..40abd83
--- /dev/null
+++ b/assets/js/min/referralAdmin.min.js
@@ -0,0 +1 @@
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.referral=window.jvbReferral,this.hasCopy=navigator.clipboard&&navigator.clipboard.writeText,this.initElements(),this.initListeners()}initElements(){this.selectors={copyBtn:".copy-btn",invite:"form.invite",adminList:".items-list.referral",dash:".replace .referral-dashboard",list:".referrals-list"},this.ui=window.uiFromSelectors(this.selectors),this.tabs=null,this.ui.dash&&(this.tabs=new window.jvbTabs(this.ui.dash),this.initViewController()),this.ui.invite&&(this.formController=new window.jvbForm,this.formController.registerForm(this.ui.invite,{autosave:!0,endpoint:"referrals",formStatus:!1}),this.formController.subscribe(((e,t)=>{"form-submit"===e&&((t=t.fullData).action="invite",window.jvbQueue.addToQueue({endpoint:"referrals",data:t,title:"Submitting invitations"}))})))}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),window.jvbQueue&&window.jvbQueue.subscribe(this.handleQueueEvent.bind(this))}handleClick(e){const t=e.target.closest(".copy-btn");t&&this.handleCopyClick(t)}handleCopyClick(e){const t=e.dataset.target,i=e.closest(".row").querySelector(`#${t}`);if(!i)return;const s=i.textContent.trim();this.hasCopy&&navigator.clipboard.writeText(s).then((()=>{e.classList.toggle("success"),setTimeout((()=>{e.classList.remove("success")}),1500)}))}initViewController(){this.referral.listStore&&this.ui.adminList&&(this.view=new window.jvbViews(this.ui.adminList,this.referral.listStore),this.view.subscribe(((e,t)=>{switch(e){case"item-action":this.handleItemAction(t);break;case"bulk-action":this.handleBulkAction(t)}})))}handleItemAction(e){const{action:t,itemId:i}=e;switch(t){case"remove":this.removeReferral(i);break;case"resend":this.resendInvite(i)}}async removeReferral(e){if(confirm("Remove this referral from your list?"))try{const t=await fetch(`${jvbSettings.api}referrals`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({action:"remove",referral_id:e})});(await t.json()).success&&(this.referral.listStore&&this.referral.listStore.fetch(),this.referral.statsStore&&this.referral.statsStore.fetch(),this.a11y?.announce("Referral removed"))}catch(e){console.error("Error removing referral:",e)}}async resendInvite(e){try{const t=await fetch(`${jvbSettings.api}referrals`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":window.auth.getNonce()},body:JSON.stringify({action:"resend",referral_id:e})}),i=await t.json();i.success?this.a11y?.announce("Invitation resent"):alert(i.message||"Cannot resend yet. Wait 7 days between invites.")}catch(e){console.error("Error resending invite:",e)}}handleQueueEvent(e,t){"operation-complete"===e&&t.details&&(t.details.successful||t.details.failed)&&this.showInviteResults(t.details)}showInviteResults(e){if(!this.ui.invite)return;const t=this.ui.invite.querySelector('[data-field="invite"]');if(!t)return;const i=t.querySelector(".tag-list");if(!i)return;const s=new Map;e.successful?.forEach((e=>{s.set(e.email,{status:"success",name:e.name})})),e.failed?.forEach((e=>{s.set(e.email,{status:"error",name:e.name,reason:e.reason})}));i.querySelectorAll(".tag").forEach((e=>{const t=JSON.parse(e.dataset.value||"{}"),i=s.get(t.email);i&&this.updateTagStatus(e,i)})),this.showInviteSummary(e)}updateTagStatus(e,t){e.classList.remove("success","error");const i=e.querySelector(".status-icon");i&&i.remove(),e.classList.add(t.status);const s=document.createElement("span");s.className="status-icon",s.innerHTML="success"===t.status?window.jvbIcon("check-circle",{size:14}):window.jvbIcon("warning-circle",{size:14}),t.reason&&(s.title=t.reason);const n=e.querySelector(".remove-tag");n?e.insertBefore(s,n):e.appendChild(s)}showInviteSummary(e){const t=e.successful?.length||0,i=e.failed?.length||0;let s=`Invites sent! ${t} successful`;i>0&&(s+=`, ${i} failed`),this.formController&&this.formController.showStatus(this.ui.invite,"submitted",s),i>0&&this.showFailureDetails(e.failed)}showFailureDetails(e){const t=`\n        <details class="invite-failures" open>\n            <summary>${e.length} invitation(s) failed - click for details</summary>\n            <ul>\n                ${e.map((e=>`\n                    <li>\n                        <strong>${window.escapeHtml(e.name)}</strong>\n                        (${window.escapeHtml(e.email)}):\n                        <em>${window.escapeHtml(e.reason)}</em>\n                    </li>\n                `)).join("")}\n            </ul>\n        </details>\n    `,i=this.ui.invite.querySelector(".fstatus");if(i){const e=i.querySelector(".invite-failures");e&&e.remove(),i.insertAdjacentHTML("beforeend",t)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbAdminReferral=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 7c52d8f..3966c88 100644
--- a/assets/js/min/uploader.min.js
+++ b/assets/js/min/uploader.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStoreReady=!1,this.uploadStoreReady=!1,this.hasCheckedForUploads=!1;const{fields:e,uploads:t}=window.jvbStore.register("uploads",[{storeName:"fields",keyPath:"id",indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],TTL:6048e5,delayFetch:!0},{storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}],delayFetch:!0}]);this.fieldStore=e,this.uploadStore=t,window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.uploadElements=new Map,this.fieldElements=new Map,this.groupElements=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.sortableInstances=new Map,this.initWorker(),this.subscribers=new Set,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.init()}async init(){this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":s&&this.handleOperationCancelled(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":this.handleOperationComplete(t,s);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e)))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),o=this.buildFieldUI(e),a={id:t,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(a),this.fieldElements.set(t,{element:e,ui:o,config:s}),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"single"!==s.type&&this.initSortable(t),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initSortable(e){if(!window.Sortable)return;!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0);const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((t=>{const s=t.classList.contains("group")?t.closest(".upload-group")?.dataset.groupId:null;this.createSortableForGrid(t,e,s)}));const s=t.element.querySelector(".empty-group");s&&!s.sortableInstance&&(s.sortableInstance=new Sortable(s,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:e,pull:!1,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:t=>this.handleDrop(t,e)}))}syncSortableSelection(e,t){this.sortableInstances.forEach(((s,o)=>{if(o.startsWith(e)){s.el.querySelectorAll(".item").forEach((e=>{const s=e.dataset.uploadId;t.has(s)?Sortable.utils.select(e):Sortable.utils.deselect(e)}))}}))}handleDrop(e,t){const s=e.to,o=e.from,a=e.items?.length>0?e.items:[e.item],r=a.map((e=>e.dataset.uploadId));switch(this.getDropTargetType(s)){case"empty-group":this.handleDropToEmptyGroup(a,r,t);break;case"preview":default:this.handleDropToPreview(a,r,t);break;case"group":this.handleDropToGroup(a,r,s,o,t)}this.updateSortableState(s),o!==s&&this.updateSortableState(o)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}handleDropToGroup(e,t,s,o,a){try{if(s===o)return void this.handleReorder({to:s,items:e});t.forEach((e=>{this.addToGroup(e,s,!1)})),this.schedulePersistance(a);const r=e.length>1?`Moved ${e.length} items to group`:"Moved item to group";this.a11y.announce(r);const i=this.selectionHandlers.get(a);i?.clearSelection()}catch(t){this.handleDropError(e,a,t)}}handleDropToPreview(e,t,s){try{t.forEach((e=>{this.removeFromGroup(e)})),this.schedulePersistance(s);const o=e.length>1?`Moved ${e.length} items to preview`:"Moved item to preview";this.a11y.announce(o);const a=this.selectionHandlers.get(s);a?.clearSelection()}catch(t){this.handleDropError(e,s,t)}}handleDropError(e,t,s,o="An error occurred"){console.error("Drop error:",s);const a=this.fieldElements.get(t);a?.ui?.preview&&e.forEach((e=>a.ui.preview.appendChild(e))),this.a11y.announce(`${o}. Items returned to preview.`)}handleDropToEmptyGroup(e,t,s){try{const o=this.createGroup(s);if(!o)return void this.handleDropError(e,s,new Error("Group creation failed"),"Failed to create group");e.forEach(((e,s)=>{o.grid.appendChild(e),this.addToGroup(t[s],o.grid,!1)})),this.schedulePersistance(s);const a=e.length>1?`Created group with ${e.length} items`:"Created group with item";this.a11y.announce(a);const r=this.selectionHandlers.get(s);r?.clearSelection()}catch(t){this.handleDropError(e,s,t)}}updateSortableState(e){const t=e?.sortableInstance;t&&t.option("disabled",!1)}refreshSortable(e){const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((e=>this.updateSortableState(e)))}handleReorder(e){const t=e.to,s=t.closest(".field, .upload");if(!s)return;e.items&&e.items.length>0?e.items:e.item;let o=Array.from(t.querySelectorAll(".item:not(.sortable-ghost):not(.sortable-clone)")).map((e=>e.dataset.uploadId)).filter((e=>e)),a=s.querySelector('input[type="hidden"]');a&&o.length>0&&(a.value=o.join(","));const r=this.getFieldIdFromElement(t);if(r){const e=this.getFieldData(r);if(t.classList.contains("group")){const s=t.dataset.groupId,a=e?.groups?.find((e=>e.id===s));a&&(a.uploads=o)}this.schedulePersistance(r)}this.a11y.announce("Item reordered"),s.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex,items:o},bubbles:!0}))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const o=this.getFieldIdFromElement(t);o&&(this.processFiles(o,s),this.a11y.announce(`${s.length} file(s) dropped for upload`))}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}if(t){const s=this.getFieldData(t);"post_group"===s?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return;o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const a=t.length;let r=0;this.updateUploadProgress(e,0,a,"Processing files...");const i=Array.from(t).map((async t=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,l={id:i,attachmentId:null,fieldId:e,status:"local_processing",groupId:null,meta:{originalName:t.name,size:t.size,type:t.type}};await this.uploadStore.save(l);const n=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,s.config.subtype):t;this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),await this.saveBlobData(i,d||t);const c=this.getSubtypeFromMime(t.type),u=this.createUploadElement({id:i,preview:n,meta:l.meta,subtype:c},"post_group"===s.config.destination);o.ui.preview&&(o.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:n,location:o.ui.preview}));const p=this.uploadStore.get(i);return p&&(p.status="processed",await this.uploadStore.save(p)),s.uploads.add(i),await this.saveFieldData(s),r++,this.updateUploadProgress(e,r,a,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),r++,this.updateUploadProgress(e,r,a,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((o,a)=>{let r,i=!1;r=setTimeout((()=>{i||(i=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),a(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:r}),this.handleProcess(e,t).then((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),o(e))})).catch((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),a(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((o,a)=>{const r=new Image,i=document.createElement("canvas"),l=i.getContext("2d");let n=null;const d=()=>{r.onload=null,r.onerror=null,n&&(URL.revokeObjectURL(n),n=null),i.width=1,i.height=1,l.clearRect(0,0,1,1)};r.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(r,t);i.width=n,i.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(r,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);i.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});o(s)}else a(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),a(new Error(`Canvas processing failed: ${e.message}`))}},r.onerror=()=>{d(),a(new Error(`Failed to load image: ${e.name}`))};try{n=this.createPreviewUrl(e),r.src=n}catch(e){d(),a(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,o){return new Promise(((a,r)=>{if(!this.worker.worker)return void r(new Error("Worker not available"));const i=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===i)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});a(s)}else r(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),r(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:i,file:e,maxDimension:s,quality:o,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n\t\t\t\tself.onmessage = async function(e) {\n\t\t\t\t\tconst { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst bitmap = await createImageBitmap(file);\n\t\t\t\t\t\tconst scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n\t\t\t\t\t\tconst width = Math.round(bitmap.width * scale);\n\t\t\t\t\t\tconst height = Math.round(bitmap.height * scale);\n\t\t\t\t\t\tconst canvas = new OffscreenCanvas(width, height);\n\t\t\t\t\t\tconst ctx = canvas.getContext('2d');\n\t\t\t\t\t\tctx.imageSmoothingEnabled = true;\n\t\t\t\t\t\tctx.imageSmoothingQuality = 'high';\n\t\t\t\t\t\tctx.drawImage(bitmap, 0, 0, width, height);\n\t\t\t\t\t\tbitmap.close();\n\t\t\t\t\t\tconst blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });\n\t\t\t\t\t\tself.postMessage({ messageId, success: true, blob: blob, format: outputFormat });\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tself.postMessage({ messageId, success: false, error: error.message });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t"],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:o}=e;if(s<=t&&o<=t)return{width:s,height:o};const a=Math.min(t/s,t/o);return{width:Math.round(s*a),height:Math.round(o*a)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}async submitUploads(e){const t=this.getFieldData(e);this.fieldElements.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=this.getFieldGroups(e);if(0===o.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const a=[],r=new FormData;let i=[];for(const e of o){const t={images:[],fields:{}};for(let[s,o]of Object.entries(e.changes))t.fields[s]=o;const o=s.filter((t=>{const s=this.uploadStore.get(t);return s?.groupId===e.id}));for(const e of o){const s=await this.getBlobData(e);if(s){r.append("files[]",s);const o={upload_id:e,index:i.length},a=this.uploadElements.get(e),l=a?.element?.querySelector('[name="featured"]');l?.checked&&(t.fields.featured=e),t.images.push(o),i.push(e)}}a.push(t)}const l=s.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of l){const t={images:[],fields:{}},s=await this.getBlobData(e);if(s){r.append("files[]",s);const o={upload_id:e,index:i.length};t.images.push(o),i.push(e)}a.push(t)}r.append("content",t.config.content),r.append("user",t.config.itemID),r.append("posts",JSON.stringify(a)),r.append("upload_ids",JSON.stringify(i));const n={endpoint:"uploads/groups",method:"POST",data:r,title:`Creating ${a.length} ${t.config.content}${a.length>1?"s":""} from uploads...`,popup:`Creating ${a.length} post${a.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(n);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),this.a11y.announce(`Creating ${a.length} post${a.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}}async queueUpload(e){const t=this.getFieldData(e);if(!t?.uploads||0===t.uploads.size)return;const s=Array.from(t.uploads),o=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");const a={endpoint:"uploads",method:"POST",data:o,title:`Uploading ${s.length} file${s.length>1?"s":""} to server...`,popup:`Uploading ${s.length} file${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(a);return s.forEach((t=>{const s=this.uploadStore.get(t);s&&(s.operationId=e,s.status="queued",this.uploadStore.save(s),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let o=[];const a=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const a=await this.getBlobData(e);a&&(s.append("files[]",a),o.push(t.id))}));return await Promise.all(a),s.append("upload_ids",JSON.stringify(o)),s}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),s=this.uploadStore.get(t);if(!s)return;if(!this.getFieldData(s.fieldId))return;let o={};o[e.target.name]=e.target.value,s.meta={...s.meta,...o},await this.uploadStore.save(s);let a={};a[s.attachmentId??s.id]=s.meta;const r={endpoint:"uploads/meta",method:"POST",data:a,title:"Updating meta",canMerge:!0,headers:{action_nonce:window.auth.getNonce("dash")}};try{await this.queue.addToQueue(r)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:s.id})}}async handleOperationComplete(e,t){if((e.result?.data||e.serverData?.data||[]).forEach((e=>{const t=this.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploadStore.save(t),this.updateUploadStatus(e.upload_id,"completed"))})),!t)return;const s=this.getFieldData(t);if(!s)return;const o=Array.from(s.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of o)await this.clearUpload(e,!1),s.uploads.delete(e);0===s.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(s),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const s=this.uploadStore.get(t);s&&(s.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(s),this.updateUploadStatus(t,s.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s)await this.clearUpload(e,!1);await this.clearFieldFromStores(e),this.updateFieldState(e),this.a11y.announce("Upload cancelled")}getFieldGroups(e){const t=this.getFieldData(e);return t?.groups?t.groups.map((e=>({id:e.id,uploads:e.uploads||[],changes:e.changes||{}}))):[]}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}async restoreSelectedUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,s]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=s,await this.restoreField(t))}}async restoreField(e){const{config:t,context:s,uploads:o,groups:a,id:r}=e;s?.modalType&&await this.openModalForRestore(s);let i=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!i){const e=`${t.content}_${t.itemID}_${t.name}`;i=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!i)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=i.dataset.uploader;l&&this.fieldElements.has(l)||(l=this.registerUploader(i));const n=this.fieldElements.get(l),d=this.getFieldData(l);if(!n||!d)return void console.error("Failed to register field for restoration");d.state=e.state||"ready",n.ui||(n.ui=this.buildFieldUI(i)),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),n.ui.dropZone&&(n.ui.dropZone.hidden=!0),a&&a.length>0&&await this.restoreGroups(l,a);const c=o instanceof Set?Array.from(o):Array.isArray(o)?o:[];for(const e of c){const t=this.uploadStore.get(e);t&&await this.restoreUpload(l,t)}await this.saveFieldData(d),this.updateFieldState(l),this.maybeLockUploads(l),this.refreshSortable(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}async restoreUpload(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(!s||!o)return void console.error("Field not found for upload restoration:",e);const a=await this.getBlobData(t.id);if(!a)return void console.warn("Blob data not found for upload:",t.id);const r=this.createPreviewUrl(a),i=this.getSubtypeFromMime(a.type),l=this.createUploadElement({id:t.id,preview:r,meta:t.meta||{originalName:a.name,size:a.size,type:a.type},subtype:i},"post_group"===o.config.destination);let n;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){n=e.grid;const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads||(s.uploads=[]),s.uploads.includes(t.id)||s.uploads.push(t.id))}else n=s.ui.preview,t.groupId=null}else n=s.ui.preview;n?n.appendChild(l):s.ui.preview&&(s.ui.preview.appendChild(l),n=s.ui.preview),this.uploadElements.set(t.id,{element:l,preview:r,location:n}),o.uploads||(o.uploads=new Set),o.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),n&&this.updateSortableState(n)}async restoreGroups(e,t){const s=this.fieldElements.get(e),o=this.getFieldData(e);if(s&&o){for(const s of t){const t=this.createGroup(e,s.id);if(!t){console.warn("Failed to create group:",s.id);continue}const a=o.groups?.find((e=>e.id===s.id));if(a&&(s.changes&&(a.changes={...s.changes}),s.uploads&&(a.uploads=[...s.uploads]),s.changes)){const e=t.element.querySelector('[name*="post_title"]'),o=t.element.querySelector('[name*="post_excerpt"]');e&&s.changes.post_title&&(e.value=s.changes.post_title),o&&s.changes.post_excerpt&&(o.value=s.changes.post_excerpt)}}await this.saveFieldData(o)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:s}=e;let o=null;switch(t){case"create":o=document.querySelector('[data-action="create"]');break;case"edit":s&&(o=document.querySelector(`[data-action="edit"][data-id="${s}"]`));break;case"bulkEdit":o=document.querySelector('[data-action="bulk-edit"]')}o?(o.click(),await new Promise((e=>setTimeout(e,300)))):console.warn("Modal trigger not found for restoration:",e)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,o=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,o)).toFixed(s))+" "+["Bytes","KB","MB","GB"][o]}async clearUpload(e,t=!0){const s=this.uploadElements.get(e);if(s&&(this.revokePreviewUrl(s.preview),s.element)){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}if(this.uploadElements.delete(e),await this.uploadStore.delete(e),t){const t=this.uploadStore.get(e);t?.fieldId&&await this.schedulePersistance(t.fieldId)}}async clearFieldFromStores(e){const t=this.getFieldData(e);if(t?.uploads){const e=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const t of e)await this.uploadStore.delete(t)}await this.fieldStore.delete(e)}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}updateFieldState(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t||!s)return;const o=t.element,a=s.uploads?.size||0,r=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;o.dataset.hasUploads=a>0?"true":"false",o.dataset.uploadCount=a.toString(),o.dataset.hasGroups=r?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${a} item${1!==a?"s":""}`)}updateUploadProgress(e,t,s,o){const a=this.fieldElements.get(e);if(!a?.ui?.progress?.progress)return;const r=a.ui.progress,i=s>0?t/s*100:0;r.fill&&(r.fill.style.width=`${i}%`),r.text&&(r.text.textContent=o),r.count&&(r.count.textContent=`${t}/${s}`),r.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.getFieldData(e);s&&(s.state=t,this.saveFieldData(s))}updateUploadStatus(e,t){const s=this.uploadStore.get(e);s&&(s.status=t,this.uploadStore.save(s),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),s=this.uploadStore.get(e);if(!s||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${s.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(s.status),s.status)}showUploadProgress(e,t=!0){const s=this.uploadElements.get(e);if(!s?.element)return;const o=s.element.querySelector(".progress");o&&(t?(o.style.removeProperty("animation"),o.hidden=!1):(o.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{o.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const o=this.uploadElements.get(e);if(!o?.element)return;const a=o.element.querySelector(".progress");if(!a)return;const r=a.querySelector(".fill"),i=a.querySelector(".details"),l=a.querySelector(".icon");r&&(r.style.width=`${t}%`),s&&i&&(i.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),s=this.getFieldData(e);if(!t?.ui?.dropZone||!s)return;const o=s.uploads?.size||0,a="post_group"===s.config.destination?20:s.config?.maxFiles||999;t.ui.dropZone.hidden=o>=a,t.element.classList.toggle("at-max-uploads",o>=a),"post_group"===s.config.destination&&o>=a&&this.a11y.announce("Maximum of 20 uploads reached. Please submit current uploads before adding more.")}createSortableForGrid(e,t,s=null){if(!e||e.sortableInstance)return;const o=new Sortable(e,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:t,pull:!0,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleDrop(e,t),onSelect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&!t.checked&&(t.checked=!0,t.dispatchEvent(new Event("change",{bubbles:!0})))},onDeselect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&t.checked&&(t.checked=!1,t.dispatchEvent(new Event("change",{bubbles:!0})))},onAdd:e=>this.updateSortableState(e.to),onRemove:e=>this.updateSortableState(e.from)});e.sortableInstance=o;const a=s?`${t}-group-${s}`:`${t}-preview`;return this.sortableInstances.set(a,o),o}createGroup(e,t=null){const s=this.getFieldData(e),o=this.fieldElements.get(e);if(!s||!o)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const a=this.createGroupElement(t,e);if(!a)return null;o.ui.groups||(o.ui.groups={groups:new Map,container:null,empty:null,display:null}),o.ui.groups.groups.set(t,a),o.ui.groups.container&&o.ui.groups.empty?o.ui.groups.container.insertBefore(a,o.ui.groups.empty):o.ui.groups.container&&o.ui.groups.container.appendChild(a);const r=a.querySelector(".item-grid.group");this.groupElements.set(t,{element:a,grid:r,fieldId:e}),s.groups||(s.groups=[]);return s.groups.find((e=>e.id===t))||(s.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(s)),this.addGroupSelectionHandler(e,t),r&&this.createSortableForGrid(r,e,t),{id:t,element:a,grid:r}}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let o=window.getTemplate("groupMetadata");const a=s.querySelector(".fields");if(a&&o){a.append(o);const r=a.querySelector('[name="post_title"]'),i=a.querySelector('[name="post_excerpt"]');r&&(r.id=`${e}_title`,r.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const l=this.getFieldData(t);if(l&&""!==l.config.content){let e=s.querySelector("summary");e&&(e.textContent=l.config.content+" Fields")}}else s.querySelector("details")?.remove();const r=s.querySelector(".item-grid.group");return r&&(r.dataset.groupId=e),s}deleteGroup(e,t=!0){const s=this.groupElements.get(e);if(!s)return;const o=this.getFieldData(s.fieldId);if(!o)return;const a=o.groups?.find((t=>t.id===e));let r=!0;t&&a?.uploads?.length>0&&(r=!window.confirm("Delete uploads in group?")),t&&r&&a?.uploads&&a.uploads.forEach((e=>{this.removeFromGroup(e)})),o.groups&&(o.groups=o.groups.filter((t=>t.id!==e)),this.saveFieldData(o)),s.element&&(s.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${s.fieldId}-group-${e}`,l=this.sortableInstances.get(i);l?.destroy&&l.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){const o=this.uploadStore.get(e),a=this.uploadElements.get(e);if(!o||!a)return;const r=this.getFieldData(o.fieldId),i=this.fieldElements.get(o.fieldId);if(!r||!i)return;if(!t&&a.location===i.ui.preview||t===a.location)return;if(o.groupId){const t=r.groups?.find((e=>e.id===o.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(o.groupId))}const l=a.element.querySelector('[name*="select-item"]');l&&(l.checked=!1);let n=a.element.querySelector('[name="featured"]');if(n&&(n.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,o.groupId=null;else{const s=t.dataset.groupId;n&&(n.name=s+"_"+n.name);const a=r.groups?.find((e=>e.id===s));a&&(a.uploads||(a.uploads=[]),a.uploads.push(e),o.groupId=s)}a.location=t,t.append(a.element),this.uploadStore.save(o),s&&this.saveFieldData(r),this.updateSortableState(t),a.location&&a.location!==t&&this.updateSortableState(a.location)}removeFromGroup(e){const t=this.uploadStore.get(e),s=this.uploadElements.get(e);if(!t||!s)return;const o=this.getFieldData(t.fieldId),a=this.fieldElements.get(t.fieldId);if(!o||!a)return;if(t.groupId){const s=o.groups?.find((e=>e.id===t.groupId));s&&(s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}a.ui?.preview&&(a.ui.preview.appendChild(s.element),s.location=a.ui.preview);const r=s.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.uploadStore.save(t),this.updateSortableState(a.ui.preview)}removeUpload(e,t){const s=this.getFieldData(e),o=this.uploadStore.get(t),a=this.uploadElements.get(t);if(!s||!o)return;if(s.uploads?.delete(t),o.groupId){const e=s.groups?.find((e=>e.id===o.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(o.groupId))}a?.element?.remove(),this.clearUpload(t),this.saveFieldData(s),this.updateFieldState(e),this.maybeLockUploads(e);const r=this.selectionHandlers.get(e);r&&r.deselect(t),this.a11y.announce("Upload removed")}handleGroupMetaChange(e){const t=this.getGroupFromElement(e);if(!t)return;const s=this.getFieldData(t.fieldId),o=s?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!o)return;o.changes||(o.changes={});let a=e.name;a.includes("group")&&(a=a.replace(`${o.id}_`,"").replace(`${o.id}[`,"").replace("]","")),o.changes[a]=e.value,this.saveFieldData(s),this.schedulePersistance(t.fieldId)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":const t=this.fieldElements.get(s);t&&(t.element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s));break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),s=t?.dataset.uploader;if(!s)return;const o=this.selected.get(s);if(o&&0!==o.size){const e=this.createGroup(s);if(!e)return;o.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${o.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,o=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(o)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,o=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(o,s),this.a11y.announce("Item removed"),this.schedulePersistance(o))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fieldElements.get(e);if(!t?.element)return;const s=new window.jvbHandleSelection({container:t.element,ui:{selectAll:t.element.querySelector('[name="select-all-uploads"]'),bulkControls:t.element.querySelector(".selection-actions"),count:t.element.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return s.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.syncSortableSelection(e,s.selectedItems),this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,s),s}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const o=this.groupElements.get(t);if(!o?.element)return;const a=new window.jvbHandleSelection({container:o.element,ui:{selectAll:o.element.querySelector(this.selectors.groups.selectAll),bulkControls:o.element.querySelector(this.selectors.groups.actions),count:o.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return a.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,a),a}handleSelectAll(e,t){}getCurrentSelection(e){let t=[];for(let[s,o]of this.selectionHandlers)(e===s||s.includes(e))&&o.selectedItems.size>0&&(t=t.concat([...o.selectedItems]));return t}getFieldData(e){const t=this.fieldStore.get(e);return t?(Array.isArray(t.uploads)?t.uploads=new Set(t.uploads):t.uploads||(t.uploads=new Set),Array.isArray(t.groups)||(t.groups=[]),t):null}async saveFieldData(e){await this.fieldStore.save({...e,timestamp:Date.now()})}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",getRuntimeData:e=>this.fieldElements.get(e),getStoreData:e=>this.getFieldData(e)},upload:{selector:this.selectors.items.item,key:"uploadId",getRuntimeData:e=>this.uploadElements.get(e),getStoreData:e=>this.uploadStore.get(e)},group:{selector:this.selectors.groups.container,key:"groupId",getRuntimeData:e=>this.groupElements.get(e),getStoreData:e=>{const t=this.groupElements.get(e);if(!t)return null;const s=this.getFieldData(t.fieldId);return s?.groups?.find((t=>t.id===e))}}},o=s[t];if(!o)return null;const a=e.closest(o.selector);if(!a)return null;const r=a.dataset[o.key];return{...o.getRuntimeData(r),...o.getStoreData(r)}}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){const t=this.getFromElement(e,"field");return t?.id??null}getUploadIdFromElement(e){const t=this.getFromElement(e,"upload");return t?.id??null}getGroupIdFromElement(e){const t=this.getFromElement(e,"group");return t?.id??null}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]||0}getModalType(e){if(!e?.element)return null;if(void 0!==e._cachedModalType)return e._cachedModalType;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return;s.dataset.uploadId=e.id,s.dataset.subtype=e.subtype||"image";let[o,a,r,i,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch(o&&(o.value=e.id),e.subtype){case"image":a&&(a.src=e.preview,a.alt=e.meta?.originalName||""),r?.remove(),i?.remove();break;case"video":r&&(r.src=e.preview),a?.remove(),i?.remove();break;case"document":const t=e.meta?.originalName||"",s=t.split(".").pop()?.toLowerCase()||"",o={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(o[s]||"file");i&&(i.innerText=t,i.prepend(l)),a?.remove(),r?.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,a=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,a&&(a.htmlFor=o)}})),s}normalizeFieldData(e){return e?(Array.isArray(e.uploads)?e.uploads=new Set(e.uploads):e.uploads||(e.uploads=new Set),Array.isArray(e.groups)||(e.groups=[]),e.groups=e.groups.map((e=>({...e,uploads:Array.isArray(e.uploads)?e.uploads:[]}))),e):null}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),250)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const s=await t.arrayBuffer(),o=this.uploadStore.get(e)||{id:e};o.blobData={buffer:s,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(o)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const s=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([s],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.fieldStoreReady=!0,this.checkIfBothStoresReady()}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.uploadStoreReady=!0,this.checkIfBothStoresReady();break;case"item-saved":this.showSaveIndicator(t.key)}}checkIfBothStoresReady(){this.fieldStoreReady&&this.uploadStoreReady&&!this.hasCheckedForUploads&&(this.hasCheckedForUploads=!0,this.checkForStoredUploads())}async checkForStoredUploads(){const e=this.fieldStore.getAll().filter((e=>{if(!e.uploads)return!1;return(e.uploads instanceof Set?Array.from(e.uploads):Array.isArray(e.uploads)?e.uploads:[]).some((e=>{const t=this.uploadStore.get(e);return t&&!t.operationId&&["completed","processed","local_processing","processed-original"].includes(t.status)}))}));0!==e.length&&this.showRecoveryNotification(e)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let o,a=window.getTemplate("restoreNotification");if(!a)return void console.error("Restore notification template not found");if(s>0){o=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else o=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const r=a.querySelector(".restore-details");r&&(r.textContent=o);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const o=e.querySelector(".item-grid.restore");for(let e of t.uploads){const s=this.uploadStore.get(e);let a=window.getTemplate("uploadItem");if(!a)continue;const r=await this.getBlobData(s.id);if(r)try{const e=this.createPreviewUrl(r);let[o,i,l,n,d]=[a.querySelector('[name="featured"]'),a.querySelector("img"),a.querySelector("video"),a.querySelector("label > span"),a.querySelector("details")];a.dataset.uploadId=s.id,a.dataset.fieldId=t.id;let c=this.getSubtypeFromMime(r.type);switch(a.dataset.subtype=c,c){case"image":[i.src,i.alt]=[e,r.name??s.meta?.originalName??""],l.remove(),n.remove();break;case"video":l.src=e,i.remove(),n.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}n.innerText=s.originalFile.name,n.prepend(t),i.remove(),l.remove()}a.dataset.previewUrl=e}catch(e){console.warn("Failed to create preview for upload:",s.id,e)}const i=a.querySelector("summary span");i&&(i.textContent=s.meta?.originalName||"Unknown file");const l=a.querySelector("details");l&&s.meta&&(l.textContent=`${this.formatBytes(s.meta.size)} • ${s.meta.type}`),a.querySelectorAll("input").forEach((e=>{let t=e.id;if(t){let o=t+s.id,a=e.parentNode.querySelector(`label[for="${t}"]`);e.id=o,a&&(a.htmlFor=o)}})),o&&o.appendChild(a)}a.querySelector(".wrap").appendChild(o)}document.querySelector(".field.upload").appendChild(a),a=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(a),this.restoreSelection=new window.jvbHandleSelection({container:a,ui:{selectAll:a.querySelector("#select-all-restore"),count:a.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}async handleRestoreAll(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{let s=e.dataset.uploadId,o=e.dataset.fieldId;t.push({uploadId:s,fieldId:o})})),await this.restoreSelectedUploads(t),this.cleanupRestore()}showSaveIndicator(e){}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){await this.fieldStore.clear(),await this.uploadStore.clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.uploadElements.clear(),this.fieldElements.clear(),this.groupElements.clear(),this.selected.clear(),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStoreReady=!1,this.uploadStoreReady=!1,this.hasCheckedForUploads=!1;const{fields:e,uploads:t}=window.jvbStore.register("uploads",[{storeName:"fields",keyPath:"id",indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],TTL:6048e5,delayFetch:!0},{storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}],delayFetch:!0}]);this.fieldStore=e,this.uploadStore=t,window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.uploadElements=new Map,this.fieldElements=new Map,this.groupElements=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.sortableInstances=new Map,this.initWorker(),this.subscribers=new Set,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.init()}async init(){this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const o=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":o&&this.handleOperationCancelled(o);break;case"operation-status":o&&this.updateFieldStatus(o,t.status);break;case"operation-complete":this.handleOperationComplete(t,o);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,o)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}scanFields(e,t){console.log(t,"autoUpload");e.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e,t)))}registerUploader(e,t){const o=this.determineFieldId(e),s=this.extractFieldConfig(e,t),a=this.buildFieldUI(e);console.log(s,"registering with config");const r={id:o,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(r),this.fieldElements.set(o,{element:e,ui:a,config:s}),e.dataset.uploader=o,this.addFieldSelectionHandler(o),"single"!==s.type&&this.initSortable(o),o}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},o=e.querySelector(".group-display");return o&&(t.groups={display:o,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initSortable(e){if(!window.Sortable)return;!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0);const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((t=>{const o=t.classList.contains("group")?t.closest(".upload-group")?.dataset.groupId:null;this.createSortableForGrid(t,e,o)}));const o=t.element.querySelector(".empty-group");o&&!o.sortableInstance&&(o.sortableInstance=new Sortable(o,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:e,pull:!1,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:t=>this.handleDrop(t,e)}))}syncSortableSelection(e,t){this.sortableInstances.forEach(((o,s)=>{if(s.startsWith(e)){o.el.querySelectorAll(".item").forEach((e=>{const o=e.dataset.uploadId;t.has(o)?Sortable.utils.select(e):Sortable.utils.deselect(e)}))}}))}handleDrop(e,t){const o=e.to,s=e.from,a=e.items?.length>0?e.items:[e.item],r=a.map((e=>e.dataset.uploadId));switch(this.getDropTargetType(o)){case"empty-group":this.handleDropToEmptyGroup(a,r,t);break;case"preview":default:this.handleDropToPreview(a,r,t);break;case"group":this.handleDropToGroup(a,r,o,s,t)}this.updateSortableState(o),s!==o&&this.updateSortableState(s)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}handleDropToGroup(e,t,o,s,a){try{if(o===s)return void this.handleReorder({to:o,items:e});t.forEach((e=>{this.addToGroup(e,o,!1)})),this.schedulePersistance(a);const r=e.length>1?`Moved ${e.length} items to group`:"Moved item to group";this.a11y.announce(r);const i=this.selectionHandlers.get(a);i?.clearSelection()}catch(t){this.handleDropError(e,a,t)}}handleDropToPreview(e,t,o){try{t.forEach((e=>{this.removeFromGroup(e)})),this.schedulePersistance(o);const s=e.length>1?`Moved ${e.length} items to preview`:"Moved item to preview";this.a11y.announce(s);const a=this.selectionHandlers.get(o);a?.clearSelection()}catch(t){this.handleDropError(e,o,t)}}handleDropError(e,t,o,s="An error occurred"){console.error("Drop error:",o);const a=this.fieldElements.get(t);a?.ui?.preview&&e.forEach((e=>a.ui.preview.appendChild(e))),this.a11y.announce(`${s}. Items returned to preview.`)}handleDropToEmptyGroup(e,t,o){try{const s=this.createGroup(o);if(!s)return void this.handleDropError(e,o,new Error("Group creation failed"),"Failed to create group");e.forEach(((e,o)=>{s.grid.appendChild(e),this.addToGroup(t[o],s.grid,!1)})),this.schedulePersistance(o);const a=e.length>1?`Created group with ${e.length} items`:"Created group with item";this.a11y.announce(a);const r=this.selectionHandlers.get(o);r?.clearSelection()}catch(t){this.handleDropError(e,o,t)}}updateSortableState(e){const t=e?.sortableInstance;t&&t.option("disabled",!1)}refreshSortable(e){const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((e=>this.updateSortableState(e)))}handleReorder(e){const t=e.to,o=t.closest(".field, .upload");if(!o)return;let s=Array.from(t.querySelectorAll(".item:not(.sortable-ghost):not(.sortable-clone)")).map((e=>e.dataset.uploadId)).filter((e=>e)),a=o.querySelector('input[type="hidden"]');a&&s.length>0&&(a.value=s.join(","));const r=this.getFieldIdFromElement(t);if(r){const e=this.getFieldData(r);if(t.classList.contains("group")){const o=t.dataset.groupId,a=e?.groups?.find((e=>e.id===o));a&&(a.uploads=s)}this.schedulePersistance(r)}this.a11y.announce("Item reordered"),o.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex,items:s},bubbles:!0}))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const o=Array.from(e.dataTransfer.files);if(0===o.length)return;const s=this.getFieldIdFromElement(t);s&&(this.processFiles(s,o),this.a11y.announce(`${o.length} file(s) dropped for upload`))}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const o=Array.from(e.target.files);o.length>0&&t&&this.processFiles(t,o)}if(t){const o=this.getFieldData(t);if(!o.config.autoUpload)return;"post_group"===o?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const o=this.getFieldData(e),s=this.fieldElements.get(e);if(!o||!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups?.display&&(s.ui.groups.display.hidden=!1);const a=t.length;let r=0;this.updateUploadProgress(e,0,a,"Processing files...");const i=Array.from(t).map((async t=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,n={id:i,attachmentId:null,fieldId:e,status:"local_processing",groupId:null,meta:{originalName:t.name,size:t.size,type:t.type}};await this.uploadStore.save(n);const l=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,o.config.subtype):t;this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),await this.saveBlobData(i,d||t);const c=this.getSubtypeFromMime(t.type),u=this.createUploadElement({id:i,preview:l,meta:n.meta,subtype:c},"post_group"===o.config.destination);s.ui.preview&&(s.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:l,location:s.ui.preview}));const p=this.uploadStore.get(i);return p&&(p.status="processed",await this.uploadStore.save(p)),o.uploads.add(i),await this.saveFieldData(o),r++,this.updateUploadProgress(e,r,a,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(o){return console.error("Error processing file:",t.name,o),r++,this.updateUploadProgress(e,r,a,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),o.config.autoUpload&&"post_group"!==o.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const o=this.worker.settings.timeout;return new Promise(((s,a)=>{let r,i=!1;r=setTimeout((()=>{i||(i=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),a(new Error(`Processing timeout for ${e.name}`)))}),o),this.worker.tasks.set(t,{file:e,timeoutId:r}),this.handleProcess(e,t).then((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),s(e))})).catch((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),a(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const o=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,o,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,o,.85)}async processOnMainThread(e,t,o){return new Promise(((s,a)=>{const r=new Image,i=document.createElement("canvas"),n=i.getContext("2d");let l=null;const d=()=>{r.onload=null,r.onerror=null,l&&(URL.revokeObjectURL(l),l=null),i.width=1,i.height=1,n.clearRect(0,0,1,1)};r.onload=()=>{try{const{width:l,height:c}=this.calculateOptimalDimensions(r,t);i.width=l,i.height=c,n.imageSmoothingEnabled=!0,n.imageSmoothingQuality="high",n.drawImage(r,0,0,l,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,o);i.toBlob((t=>{if(d(),t){const o=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});s(o)}else a(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),a(new Error(`Canvas processing failed: ${e.message}`))}},r.onerror=()=>{d(),a(new Error(`Failed to load image: ${e.name}`))};try{l=this.createPreviewUrl(e),r.src=l}catch(e){d(),a(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,o,s){return new Promise(((a,r)=>{if(!this.worker.worker)return void r(new Error("Worker not available"));const i=`${t}_${Date.now()}`,n=t=>{if(t.data.messageId===i)if(this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),t.data.success){const o=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});a(o)}else r(new Error(t.data.error||"Worker processing failed"))},l=e=>{this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),r(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",n),this.worker.worker.addEventListener("error",l),this.worker.worker.postMessage({messageId:i,file:e,maxDimension:o,quality:s,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n\t\t\t\tself.onmessage = async function(e) {\n\t\t\t\t\tconst { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst bitmap = await createImageBitmap(file);\n\t\t\t\t\t\tconst scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n\t\t\t\t\t\tconst width = Math.round(bitmap.width * scale);\n\t\t\t\t\t\tconst height = Math.round(bitmap.height * scale);\n\t\t\t\t\t\tconst canvas = new OffscreenCanvas(width, height);\n\t\t\t\t\t\tconst ctx = canvas.getContext('2d');\n\t\t\t\t\t\tctx.imageSmoothingEnabled = true;\n\t\t\t\t\t\tctx.imageSmoothingQuality = 'high';\n\t\t\t\t\t\tctx.drawImage(bitmap, 0, 0, width, height);\n\t\t\t\t\t\tbitmap.close();\n\t\t\t\t\t\tconst blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });\n\t\t\t\t\t\tself.postMessage({ messageId, success: true, blob: blob, format: outputFormat });\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tself.postMessage({ messageId, success: false, error: error.message });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t"],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:o,height:s}=e;if(o<=t&&s<=t)return{width:o,height:s};const a=Math.min(t/o,t/s);return{width:Math.round(o*a),height:Math.round(s*a)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}async submitUploads(e){const t=this.getFieldData(e);this.fieldElements.get(e);if(!t?.uploads||0===t.uploads.size)return;let o=Array.from(t.uploads);if(0===o.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const s=this.getFieldGroups(e);if(0===s.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const a=[],r=new FormData;let i=[];for(const e of s){const t={images:[],fields:{}};for(let[o,s]of Object.entries(e.changes))t.fields[o]=s;const s=o.filter((t=>{const o=this.uploadStore.get(t);return o?.groupId===e.id}));for(const e of s){const o=await this.getBlobData(e);if(o){r.append("files[]",o);const s={upload_id:e,index:i.length},a=this.uploadElements.get(e),n=a?.element?.querySelector('[name="featured"]');n?.checked&&(t.fields.featured=e),t.images.push(s),i.push(e)}}a.push(t)}const n=o.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of n){const t={images:[],fields:{}},o=await this.getBlobData(e);if(o){r.append("files[]",o);const s={upload_id:e,index:i.length};t.images.push(s),i.push(e)}a.push(t)}r.append("content",t.config.content),r.append("user",t.config.itemID),r.append("posts",JSON.stringify(a)),r.append("upload_ids",JSON.stringify(i));const l={endpoint:"uploads/groups",method:"POST",data:r,title:`Creating ${a.length} ${t.config.content}${a.length>1?"s":""} from uploads...`,popup:`Creating ${a.length} post${a.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(l);return o.forEach((t=>{const o=this.uploadStore.get(t);o&&(o.operationId=e,o.status="queued",this.uploadStore.save(o),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),this.a11y.announce(`Creating ${a.length} post${a.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}}async queueUpload(e){const t=this.getFieldData(e);if(!t?.uploads||0===t.uploads.size)return;const o=Array.from(t.uploads),s=this.prepareUploadData(t,o);this.a11y.announce("Queuing for upload");const a={endpoint:"uploads",method:"POST",data:s,title:`Uploading ${o.length} file${o.length>1?"s":""} to server...`,popup:`Uploading ${o.length} file${o.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(a);return o.forEach((t=>{const o=this.uploadStore.get(t);o&&(o.operationId=e,o.status="queued",this.uploadStore.save(o),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async prepareUploadData(e,t){const o=new FormData;o.append("content",e.config.content),o.append("mode",e.config.mode),o.append("field_name",e.config.name),o.append("fieldId",e.id),o.append("field_type",e.config.type),o.append("subtype",e.config.subtype),o.append("item_id",e.config.itemID),o.append("destination",e.config.destination||"meta");let s=[];const a=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const a=await this.getBlobData(e);a&&(o.append("files[]",a),s.push(t.id))}));return await Promise.all(a),o.append("upload_ids",JSON.stringify(s)),o}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),o=this.uploadStore.get(t);if(!o)return;if(!this.getFieldData(o.fieldId))return;let s={};s[e.target.name]=e.target.value,o.meta={...o.meta,...s},await this.uploadStore.save(o);let a={};a[o.attachmentId??o.id]=o.meta;const r={endpoint:"uploads/meta",method:"POST",data:a,title:"Updating meta",canMerge:!0,headers:{action_nonce:window.auth.getNonce("dash")}};try{await this.queue.addToQueue(r)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:o.id})}}async handleOperationComplete(e,t){if((e.result?.data||e.serverData?.data||[]).forEach((e=>{const t=this.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploadStore.save(t),this.updateUploadStatus(e.upload_id,"completed"))})),!t)return;const o=this.getFieldData(t);if(!o)return;const s=Array.from(o.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of s)await this.clearUpload(e,!1),o.uploads.delete(e);0===o.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(o),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const o=this.uploadStore.get(t);o&&(o.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(o),this.updateUploadStatus(t,o.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const o=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of o)await this.clearUpload(e,!1);await this.clearFieldFromStores(e),this.updateFieldState(e),this.a11y.announce("Upload cancelled")}getFieldGroups(e){const t=this.getFieldData(e);return t?.groups?t.groups.map((e=>({id:e.id,uploads:e.uploads||[],changes:e.changes||{}}))):[]}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const o=e.closest(".item");o&&t.push({uploadId:o.dataset.uploadId,fieldId:o.dataset.fieldId})})),t}async restoreSelectedUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,o]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=o,await this.restoreField(t))}}async restoreField(e){const{config:t,context:o,uploads:s,groups:a,id:r}=e;o?.modalType&&await this.openModalForRestore(o);let i=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!i){const e=`${t.content}_${t.itemID}_${t.name}`;i=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!i)return void console.warn(`Field ${t.name} not found for restoration`,t);let n=i.dataset.uploader;n&&this.fieldElements.has(n)||(n=this.registerUploader(i));const l=this.fieldElements.get(n),d=this.getFieldData(n);if(!l||!d)return void console.error("Failed to register field for restoration");d.state=e.state||"ready",l.ui||(l.ui=this.buildFieldUI(i)),l.ui.groups?.display&&(l.ui.groups.display.hidden=!1),l.ui.dropZone&&(l.ui.dropZone.hidden=!0),a&&a.length>0&&await this.restoreGroups(n,a);const c=s instanceof Set?Array.from(s):Array.isArray(s)?s:[];for(const e of c){const t=this.uploadStore.get(e);t&&await this.restoreUpload(n,t)}await this.saveFieldData(d),this.updateFieldState(n),this.maybeLockUploads(n),this.refreshSortable(n),console.log(t),t.autoUpload&&"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(n)}async restoreUpload(e,t){const o=this.fieldElements.get(e),s=this.getFieldData(e);if(!o||!s)return void console.error("Field not found for upload restoration:",e);const a=await this.getBlobData(t.id);if(!a)return void console.warn("Blob data not found for upload:",t.id);const r=this.createPreviewUrl(a),i=this.getSubtypeFromMime(a.type),n=this.createUploadElement({id:t.id,preview:r,meta:t.meta||{originalName:a.name,size:a.size,type:a.type},subtype:i},"post_group"===s.config.destination);let l;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){l=e.grid;const o=s.groups?.find((e=>e.id===t.groupId));o&&(o.uploads||(o.uploads=[]),o.uploads.includes(t.id)||o.uploads.push(t.id))}else l=o.ui.preview,t.groupId=null}else l=o.ui.preview;l?l.appendChild(n):o.ui.preview&&(o.ui.preview.appendChild(n),l=o.ui.preview),this.uploadElements.set(t.id,{element:n,preview:r,location:l}),s.uploads||(s.uploads=new Set),s.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),l&&this.updateSortableState(l)}async restoreGroups(e,t){const o=this.fieldElements.get(e),s=this.getFieldData(e);if(o&&s){for(const o of t){const t=this.createGroup(e,o.id);if(!t){console.warn("Failed to create group:",o.id);continue}const a=s.groups?.find((e=>e.id===o.id));if(a&&(o.changes&&(a.changes={...o.changes}),o.uploads&&(a.uploads=[...o.uploads]),o.changes)){const e=t.element.querySelector('[name*="post_title"]'),s=t.element.querySelector('[name*="post_excerpt"]');e&&o.changes.post_title&&(e.value=o.changes.post_title),s&&o.changes.post_excerpt&&(s.value=o.changes.post_excerpt)}}await this.saveFieldData(s)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:o}=e;let s=null;switch(t){case"create":s=document.querySelector('[data-action="create"]');break;case"edit":o&&(s=document.querySelector(`[data-action="edit"][data-id="${o}"]`));break;case"bulkEdit":s=document.querySelector('[data-action="bulk-edit"]')}s?(s.click(),await new Promise((e=>setTimeout(e,300)))):console.warn("Modal trigger not found for restoration:",e)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const o=t<0?0:t,s=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,s)).toFixed(o))+" "+["Bytes","KB","MB","GB"][s]}async clearUpload(e,t=!0){const o=this.uploadElements.get(e);if(o&&(this.revokePreviewUrl(o.preview),o.element)){const e=o.element.dataset.previewUrl;this.revokePreviewUrl(e),delete o.element.dataset.previewUrl}if(this.uploadElements.delete(e),await this.uploadStore.delete(e),t){const t=this.uploadStore.get(e);t?.fieldId&&await this.schedulePersistance(t.fieldId)}}async clearFieldFromStores(e){const t=this.getFieldData(e);if(t?.uploads){const e=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const t of e)await this.uploadStore.delete(t)}await this.fieldStore.delete(e)}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}updateFieldState(e){const t=this.fieldElements.get(e),o=this.getFieldData(e);if(!t||!o)return;const s=t.element,a=o.uploads?.size||0,r=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=a>0?"true":"false",s.dataset.uploadCount=a.toString(),s.dataset.hasGroups=r?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${a} item${1!==a?"s":""}`)}updateUploadProgress(e,t,o,s){const a=this.fieldElements.get(e);if(!a?.ui?.progress?.progress)return;const r=a.ui.progress,i=o>0?t/o*100:0;r.fill&&(r.fill.style.width=`${i}%`),r.text&&(r.text.textContent=s),r.count&&(r.count.textContent=`${t}/${o}`),r.progress.hidden=t===o}updateFieldStatus(e,t){const o=this.getFieldData(e);o&&(o.state=t,this.saveFieldData(o))}updateUploadStatus(e,t){const o=this.uploadStore.get(e);o&&(o.status=t,this.uploadStore.save(o),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),o=this.uploadStore.get(e);if(!o||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${o.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(o.status),o.status)}showUploadProgress(e,t=!0){const o=this.uploadElements.get(e);if(!o?.element)return;const s=o.element.querySelector(".progress");s&&(t?(s.style.removeProperty("animation"),s.hidden=!1):(s.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{s.hidden=!0}),300)))}updateUploadItemProgress(e,t,o=null){const s=this.uploadElements.get(e);if(!s?.element)return;const a=s.element.querySelector(".progress");if(!a)return;const r=a.querySelector(".fill"),i=a.querySelector(".details"),n=a.querySelector(".icon");r&&(r.style.width=`${t}%`),o&&i&&(i.textContent=this.getStatusText(o)),o&&n&&(n.innerHTML=this.getStatusIcon(o).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),o=this.getFieldData(e);if(!t?.ui?.dropZone||!o)return;const s=o.uploads?.size||0,a="post_group"===o.config.destination?20:o.config?.maxFiles||999;t.ui.dropZone.hidden=s>=a,t.element.classList.toggle("at-max-uploads",s>=a),"post_group"===o.config.destination&&s>=a&&this.a11y.announce("Maximum of 20 uploads reached. Please submit current uploads before adding more.")}createSortableForGrid(e,t,o=null){if(!e||e.sortableInstance)return;const s=new Sortable(e,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:t,pull:!0,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleDrop(e,t),onSelect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&!t.checked&&(t.checked=!0,t.dispatchEvent(new Event("change",{bubbles:!0})))},onDeselect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&t.checked&&(t.checked=!1,t.dispatchEvent(new Event("change",{bubbles:!0})))},onAdd:e=>this.updateSortableState(e.to),onRemove:e=>this.updateSortableState(e.from)});e.sortableInstance=s;const a=o?`${t}-group-${o}`:`${t}-preview`;return this.sortableInstances.set(a,s),s}createGroup(e,t=null){const o=this.getFieldData(e),s=this.fieldElements.get(e);if(!o||!s)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const a=this.createGroupElement(t,e);if(!a)return null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,a),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(a,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(a);const r=a.querySelector(".item-grid.group");this.groupElements.set(t,{element:a,grid:r,fieldId:e}),o.groups||(o.groups=[]);return o.groups.find((e=>e.id===t))||(o.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(o)),this.addGroupSelectionHandler(e,t),r&&this.createSortableForGrid(r,e,t),{id:t,element:a,grid:r}}createGroupElement(e,t){let o=window.getTemplate("imageGroup");if(!o)return;o.dataset.groupId=e,o.dataset.fieldId=t;let s=window.getTemplate("groupMetadata");const a=o.querySelector(".fields");if(a&&s){a.append(s);const r=a.querySelector('[name="post_title"]'),i=a.querySelector('[name="post_excerpt"]');r&&(r.id=`${e}_title`,r.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const n=this.getFieldData(t);if(n&&""!==n.config.content){let e=o.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else o.querySelector("details")?.remove();const r=o.querySelector(".item-grid.group");return r&&(r.dataset.groupId=e),o}deleteGroup(e,t=!0){const o=this.groupElements.get(e);if(!o)return;const s=this.getFieldData(o.fieldId);if(!s)return;const a=s.groups?.find((t=>t.id===e));let r=!0;t&&a?.uploads?.length>0&&(r=!window.confirm("Delete uploads in group?")),t&&r&&a?.uploads&&a.uploads.forEach((e=>{this.removeFromGroup(e)})),s.groups&&(s.groups=s.groups.filter((t=>t.id!==e)),this.saveFieldData(s)),o.element&&(o.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${o.fieldId}-group-${e}`,n=this.sortableInstances.get(i);n?.destroy&&n.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(o.fieldId)}addToGroup(e,t=null,o=!0){const s=this.uploadStore.get(e),a=this.uploadElements.get(e);if(!s||!a)return;const r=this.getFieldData(s.fieldId),i=this.fieldElements.get(s.fieldId);if(!r||!i)return;if(!t&&a.location===i.ui.preview||t===a.location)return;if(s.groupId){const t=r.groups?.find((e=>e.id===s.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(s.groupId))}const n=a.element.querySelector('[name*="select-item"]');n&&(n.checked=!1);let l=a.element.querySelector('[name="featured"]');if(l&&(l.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,s.groupId=null;else{const o=t.dataset.groupId;l&&(l.name=o+"_"+l.name);const a=r.groups?.find((e=>e.id===o));a&&(a.uploads||(a.uploads=[]),a.uploads.push(e),s.groupId=o)}a.location=t,t.append(a.element),this.uploadStore.save(s),o&&this.saveFieldData(r),this.updateSortableState(t),a.location&&a.location!==t&&this.updateSortableState(a.location)}removeFromGroup(e){const t=this.uploadStore.get(e),o=this.uploadElements.get(e);if(!t||!o)return;const s=this.getFieldData(t.fieldId),a=this.fieldElements.get(t.fieldId);if(!s||!a)return;if(t.groupId){const o=s.groups?.find((e=>e.id===t.groupId));o&&(o.uploads=o.uploads.filter((t=>t!==e)),0===o.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}a.ui?.preview&&(a.ui.preview.appendChild(o.element),o.location=a.ui.preview);const r=o.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.uploadStore.save(t),this.updateSortableState(a.ui.preview)}removeUpload(e,t){const o=this.getFieldData(e),s=this.uploadStore.get(t),a=this.uploadElements.get(t);if(!o||!s)return;if(o.uploads?.delete(t),s.groupId){const e=o.groups?.find((e=>e.id===s.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(s.groupId))}a?.element?.remove(),this.clearUpload(t),this.saveFieldData(o),this.updateFieldState(e),this.maybeLockUploads(e);const r=this.selectionHandlers.get(e);r&&r.deselect(t),this.a11y.announce("Upload removed")}handleGroupMetaChange(e){const t=this.getGroupFromElement(e);if(!t)return;const o=this.getFieldData(t.fieldId),s=o?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!s)return;s.changes||(s.changes={});let a=e.name;a.includes("group")&&(a=a.replace(`${s.id}_`,"").replace(`${s.id}[`,"").replace("]","")),s.changes[a]=e.value,this.saveFieldData(o),this.schedulePersistance(t.fieldId)}handleAction(e){const t=e.dataset.action,o=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":const t=this.fieldElements.get(o);t&&(t.element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(o));break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),o=t?.dataset.uploader;if(!o)return;const s=this.selected.get(o);if(s&&0!==s.size){const e=this.createGroup(o);if(!e)return;s.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(o);t?.clearSelection(),this.a11y.announce(`Created group with ${s.size} items`)}else this.createGroup(o);this.schedulePersistance(o)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const o=t.dataset.groupId,s=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(o),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(s)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const o=t.dataset.uploadId,s=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(s,o),this.a11y.announce("Item removed"),this.schedulePersistance(s))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fieldElements.get(e);if(!t?.element)return;const o=new window.jvbHandleSelection({container:t.element,ui:{selectAll:t.element.querySelector('[name="select-all-uploads"]'),bulkControls:t.element.querySelector(".selection-actions"),count:t.element.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return o.subscribe(((t,o)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.syncSortableSelection(e,o.selectedItems),this.selected.set(e,o.selectedItems);break;case"select-all":this.handleSelectAll(o.container,o.selected)}})),this.selectionHandlers.set(e,o),o}addGroupSelectionHandler(e,t){const o=`${e}_${t}`;if(this.selectionHandlers.has(o))return this.selectionHandlers.get(o);const s=this.groupElements.get(t);if(!s?.element)return;const a=new window.jvbHandleSelection({container:s.element,ui:{selectAll:s.element.querySelector(this.selectors.groups.selectAll),bulkControls:s.element.querySelector(this.selectors.groups.actions),count:s.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return a.subscribe(((t,o)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,o.selectedItems);break;case"select-all":this.handleSelectAll(o.container,o.selected)}})),this.selectionHandlers.set(o,a),a}handleSelectAll(e,t){}getFieldData(e){const t=this.fieldStore.get(e);return t?(Array.isArray(t.uploads)?t.uploads=new Set(t.uploads):t.uploads||(t.uploads=new Set),Array.isArray(t.groups)||(t.groups=[]),t):null}async saveFieldData(e){await this.fieldStore.save({...e,timestamp:Date.now()})}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const o={field:{selector:this.selectors.field.field,key:"uploader",getRuntimeData:e=>this.fieldElements.get(e),getStoreData:e=>this.getFieldData(e)},upload:{selector:this.selectors.items.item,key:"uploadId",getRuntimeData:e=>this.uploadElements.get(e),getStoreData:e=>this.uploadStore.get(e)},group:{selector:this.selectors.groups.container,key:"groupId",getRuntimeData:e=>this.groupElements.get(e),getStoreData:e=>{const t=this.groupElements.get(e);if(!t)return null;const o=this.getFieldData(t.fieldId);return o?.groups?.find((t=>t.id===e))}}},s=o[t];if(!s)return null;const a=e.closest(s.selector);if(!a)return null;const r=a.dataset[s.key];return{...s.getRuntimeData(r),...s.getStoreData(r)}}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){const t=this.getFromElement(e,"field");return t?.id??null}getUploadIdFromElement(e){const t=this.getFromElement(e,"upload");return t?.id??null}getGroupIdFromElement(e){const t=this.getFromElement(e,"group");return t?.id??null}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]||0}createUploadElement(e,t=!1){let o=window.getTemplate("uploadItem");if(!o)return;o.dataset.uploadId=e.id,o.dataset.subtype=e.subtype||"image";let[s,a,r,i,n]=[o.querySelector('[name="featured"]'),o.querySelector("img"),o.querySelector("video"),o.querySelector("label > span"),o.querySelector("details")];switch(s&&(s.value=e.id),e.subtype){case"image":a&&(a.src=e.preview,a.alt=e.meta?.originalName||""),r?.remove(),i?.remove();break;case"video":r&&(r.src=e.preview),a?.remove(),i?.remove();break;case"document":const t=e.meta?.originalName||"",o=t.split(".").pop()?.toLowerCase()||"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},n=window.getIcon(s[o]||"file");i&&(i.innerText=t,i.prepend(n)),a?.remove(),r?.remove()}if(n){let e=window.getTemplate("uploadMeta");e&&n.append(e)}return o.draggable=t,o.querySelectorAll("input").forEach((t=>{let o=t.id;if(o){let s=o+e.id,a=t.parentNode.querySelector(`label[for="${o}"]`);t.id=s,a&&(a.htmlFor=s)}})),o}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),250)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const o=await t.arrayBuffer(),s=this.uploadStore.get(e)||{id:e};s.blobData={buffer:o,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(s)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const o=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([o],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}async getFilesForForm(e){const t=e.querySelectorAll("[data-upload-field]"),o=[];for(const e of t){const t=this.determineFieldId(e),s=await this.getFilesForField(t);o.push(...s)}return o}async getFilesForField(e){const t=this.getFieldData(e);if(!t?.uploads)return[];const o=[],s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s){const s=this.uploadStore.get(e);if(!s)continue;const a=await this.getBlobData(e);a&&o.push({file:a,uploadId:e,fieldName:t.config.name,meta:s.meta||{}})}return o}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.fieldStoreReady=!0,this.checkIfBothStoresReady()}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.uploadStoreReady=!0,this.checkIfBothStoresReady();break;case"item-saved":this.showSaveIndicator(t.key)}}checkIfBothStoresReady(){this.fieldStoreReady&&this.uploadStoreReady&&!this.hasCheckedForUploads&&(this.hasCheckedForUploads=!0,this.checkForStoredUploads())}async checkForStoredUploads(){const e=this.fieldStore.getAll().filter((e=>{if(!e.uploads)return!1;return(e.uploads instanceof Set?Array.from(e.uploads):Array.isArray(e.uploads)?e.uploads:[]).some((e=>{const t=this.uploadStore.get(e);return t&&!t.operationId&&["completed","processed","local_processing","processed-original"].includes(t.status)}))}));0!==e.length&&await this.showRecoveryNotification(e)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),o=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let s,a=window.getTemplate("restoreNotification");if(!a)return void console.error("Restore notification template not found");if(o>0){s=`${o} ${o>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else s=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const r=a.querySelector(".restore-details");r&&(r.textContent=s);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const o=e.querySelector("h3");o&&(o.textContent=t.config.name||"Unnamed Field");const s=e.querySelector(".item-grid.restore");for(let e of t.uploads){const o=this.uploadStore.get(e);let a=window.getTemplate("uploadItem");if(!a)continue;const r=await this.getBlobData(o.id);if(r)try{const e=this.createPreviewUrl(r);let[s,i,n,l,d]=[a.querySelector('[name="featured"]'),a.querySelector("img"),a.querySelector("video"),a.querySelector("label > span"),a.querySelector("details")];a.dataset.uploadId=o.id,a.dataset.fieldId=t.id;let c=this.getSubtypeFromMime(r.type);switch(a.dataset.subtype=c,c){case"image":[i.src,i.alt]=[e,r.name??o.meta?.originalName??""],n.remove(),l.remove();break;case"video":n.src=e,i.remove(),l.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}l.innerText=o.originalFile.name,l.prepend(t),i.remove(),n.remove()}a.dataset.previewUrl=e}catch(e){console.warn("Failed to create preview for upload:",o.id,e)}const i=a.querySelector("summary span");i&&(i.textContent=o.meta?.originalName||"Unknown file");const n=a.querySelector("details");n&&o.meta&&(n.textContent=`${this.formatBytes(o.meta.size)} • ${o.meta.type}`),a.querySelectorAll("input").forEach((e=>{let t=e.id;if(t){let s=t+o.id,a=e.parentNode.querySelector(`label[for="${t}"]`);e.id=s,a&&(a.htmlFor=s)}})),s&&s.appendChild(a)}a.querySelector(".wrap").appendChild(s)}document.querySelector(".field.upload").appendChild(a),a=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(a),this.restoreSelection=new window.jvbHandleSelection({container:a,ui:{selectAll:a.querySelector("#select-all-restore"),count:a.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}async handleRestoreAll(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{let o=e.dataset.uploadId,s=e.dataset.fieldId;t.push({uploadId:o,fieldId:s})})),await this.restoreSelectedUploads(t),this.cleanupRestore()}showSaveIndicator(e){}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){await this.fieldStore.clear(),await this.uploadStore.clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((o=>{try{o(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.uploadElements.clear(),this.fieldElements.clear(),this.groupElements.clear(),this.selected.clear(),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 6fa9985..5f0d1c5 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '90c2ce15e482c81ed55a');
+<?php return array('dependencies' => array(), 'version' => '9b079150b16e2c23daab');
diff --git a/build/feed/view.js b/build/feed/view.js
index cee6ec0..a550270 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.cache=new window.jvbCache("feed"),this.error=window.jvbError,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.initElements(),this.initFilters(),this.loadWhenAble())}loadWhenAble(){"requestIdleCallback"in window?requestIdleCallback((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),100)}initElements(){this.currentTaxonomies=new Set,this.taxonomyFilters={},this.elements={filterTrigger:"[data-filter]",filters:{content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},selectedTax:".selected-items",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=this.ui.filterContainer.querySelectorAll('[name="content"]'),this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content.length>0?this.contentTypes=Array.from(this.ui.content).map((e=>e.value)):this.contentTypes=[this.container.dataset.content],this.ui.taxonomies.length>0?this.taxonomies=Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):this.taxonomies=[]}async initTaxonomies(){this.selector=window.jvbSelector;const e=document.querySelectorAll('[data-filter="taxonomy"]');this.selector.isInitializing=!0,e.forEach((e=>{const t=e.dataset.taxonomy;this.currentTaxonomies.add(t),this.selector.registerFilterButton(e,{button:e,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax}),this.addTaxonomyPreloadListeners(e,t)})),this.selector.isInitializing=!1,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}addTaxonomyPreloadListeners(e,t){const i=()=>{this.selector.preloadTaxonomy(t)};e.addEventListener("mouseenter",i,{once:!0}),e.addEventListener("pointerdown",i,{once:!0}),e.addEventListener("focus",i,{once:!0})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;t.size>0?this.taxonomyFilters[i]=Array.from(t.keys()):delete this.taxonomyFilters[i];let s={page:1};Object.keys(this.taxonomyFilters).length>0&&(s.taxonomy=this.taxonomyFilters),this.updateFilter(s)}clearAllTaxonomies(){this.taxonomyFilters={},window.removeChildren(this.ui.selectedTax),this.updateFilter({taxonomy:null,page:1})}initFilters(){this.filters={content:this.contentTypes[0],orderby:"date",order:"desc",page:1},this.config.context&&(this.filters.context=this.config.context),this.config.source&&(this.filters.source=this.config.source),this.processCachedFilters(),this.processURLFilters(),this.syncUIToFilters()}syncUIToFilters(){Object.entries(this.filters).forEach((([e,t])=>{const i=this.ui.filterContainer.querySelector(`[data-filter="${e}"][value="${t}"]`);i&&(i.checked=!0)})),this.updateContentFor(this.filters.content)}nextPage(){this.store.setFilter("page",this.store.filters.page++)}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content",delayFetch:!0});this.store=e.feed,this.store.subscribe(((e,t)=>{"data-loaded"===e&&(this.renderItems(),this.ui.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse.has_more&&(this.ui.loadMore.hidden=!this.store.lastResponse.has_more))}))}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse&&this.store.lastResponse.has_more&&this.nextPage()}))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get(`f_${t}`);if(i){this.filters[t]=i;let e=this.ui.filters[t];e&&(e.checked=!0)}}));let t=!1;if(e.forEach(((e,i)=>{if(i.startsWith("f_tax_")){t=!0;const s=i.replace("f_tax_","");this.taxonomyFilters[s]||(this.taxonomyFilters[s]=[]),this.taxonomyFilters[s]=e.split(",").map(Number)}})),t)for(let[e,t]in Object.entries(this.taxonomyFilters)){let i=this.ui.filterContainer.querySelector(`[data-taxonomy="${e}"]`);i&&(i.dataset.fieldId?(this.selector.get(i.dataset.fieldId).selectedTerms=new Set(t),this.selector.initFieldDisplay(i.dataset.fieldId)):this.selector.registerField(i,{button:i,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax,selectedItems:t}))}return!0}updateURL(){const e=new URLSearchParams;["content","order","orderby","match"].forEach((t=>{this.filters[t]&&e.set(`f_${t}`,this.filters[t])})),Object.entries(this.taxonomyFilters).forEach((([t,i])=>{i.length>0&&e.set(`f_tax_${t}`,i.join(","))}));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;window.history.pushState({filters:this.filters},"",t)}renderItems(){let e=this.store.getFiltered();if(1===this.store.filters.page&&window.removeChildren(this.ui.grid),0===e.length)return void this.a11y.announceItems(0,this.store.filters.page>0);const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.appendChild(r)}r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.observeImages(this.ui.grid),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,this.store.filters.page>1,this.store.hasMore))};e.length>0?i(0):this.a11y.announceItems(0,this.store.filters.page>1,!1),this.ui.filters.match.hidden=0===Object.keys(this.taxonomyFilters).length,this.ui.clearFilter.hidden=0===Object.keys(this.taxonomyFilters).length}createItemElement(e){let t=window.getTemplate("feed-item");return Object.hasOwn(t.dataset,"timeline")?this.createTimelineElement(e,t):t}createTimelineElement(e,t){var i,s;let[r,a,o,n,l,d,h,c,m,u]=[t,t.querySelector("a"),t.querySelector("img.before"),t.querySelector("img.after"),t.querySelector("summary span:last-of-type"),t.querySelector("p.started time"),t.querySelector("p.updated time"),t.querySelector("p.total b"),t.querySelector(".term-list"),Object.values(e.fields.order)],f=u.length-1,y=e.images[u[0].post_thumbnail],g=e.images[u[f].post_thumbnail];return[r.dataset.id,a.href,o.src,o.dataset.small,o.dataset.medium,n.src,n.dataset.small,n.dataset.medium,l.textContent,d.textContent,h.textContent,c.textContent]=[e.id,e.url,y.tiny,y.small,y.medium,g.tiny,g.small,g.medium,`${l.textContent} ${f} Tx`,null!==(i=u[0].date)&&void 0!==i?i:e.date,null!==(s=u[f].date)&&void 0!==s?s:"",`${f} Treatments`],t}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}addPlaceholders(){let e=this.contentTypes.length;const t=document.createDocumentFragment();for(let i=0;i<12;i++){let i,s=window.getTemplate("placeholderTemplate"),r=Math.floor(Math.random()*e);i=this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[r])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),s.append(i),t.append(s)}this.ui.grid.append(t)}updateFilter(e){let t=["taxonomy","favourites","match",...Object.keys(this.filters)];e=Object.keys(e).filter((e=>t.includes(e))).reduce(((t,i)=>(t[i]=e[i],t)),{}),window.getDifferences.map(this.filters,e)&&(this.filters={...this.filters,...e},this.updateURL(),this.store.setFilters(e))}updateContentFor(e){this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]').forEach((t=>{const i=t.dataset.for?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)})),this.ui.filterContainer.querySelectorAll("[data-for]").forEach((t=>{const i=t.dataset.for?.split(",")||[];i.length>0&&(t.hidden=!i.includes(e),t.hidden&&t.checked&&(t.checked=!1))}));const t=this.ui.filterContainer.querySelector('[name="orderby"]:checked');this.updateOrderDirectionVisibility(t?.value)}updateOrderDirectionVisibility(e){const t=this.ui.filterContainer.querySelector(".order-direction");if(t){const i=t.dataset.forOrder?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)}}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.imageObserver=null,this.resizeObserver=null,"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{this.loadImage(e.target),this.imageObserver.unobserve(e.target)}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?this.resizeObserver=new ResizeObserver((()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})):window.addEventListener("resize",(()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}loadImage(e){const t=this.getAppropriateImageSize(e);t&&t!==e.src&&(e.src=t,e.dataset.loaded="true")}getAppropriateImageSize(e){return window.innerWidth<768&&e.dataset.small?e.dataset.small:e.dataset.medium?e.dataset.medium:e.src}observeImages(e){e.querySelectorAll("img[data-small], img[data-medium]").forEach((e=>{e.dataset.loaded||this.imageObserver.observe(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(function(){window.feedBlock=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.cache=new window.jvbCache("feed"),this.error=window.jvbError,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.initElements(),this.initFilters(),this.loadWhenAble())}loadWhenAble(){"requestIdleCallback"in window?requestIdleCallback((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),100)}initElements(){this.currentTaxonomies=new Set,this.taxonomyFilters={},this.elements={filterTrigger:"[data-filter]",filters:{content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},selectedTax:".selected-items",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=this.ui.filterContainer.querySelectorAll('[name="content"]'),this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content.length>0?this.contentTypes=Array.from(this.ui.content).map((e=>e.value)):this.contentTypes=[this.container.dataset.content],this.ui.taxonomies.length>0?this.taxonomies=Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):this.taxonomies=[]}async initTaxonomies(){this.selector=window.jvbSelector;const e=document.querySelectorAll('[data-filter="taxonomy"]');this.selector.isInitializing=!0,e.forEach((e=>{const t=e.dataset.taxonomy;this.currentTaxonomies.add(t),this.selector.registerFilterButton(e,{button:e,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax}),this.addTaxonomyPreloadListeners(e,t)})),this.selector.isInitializing=!1,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}addTaxonomyPreloadListeners(e,t){const i=()=>{this.selector.preloadTaxonomy(t)};e.addEventListener("mouseenter",i,{once:!0}),e.addEventListener("pointerdown",i,{once:!0}),e.addEventListener("focus",i,{once:!0})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;t.size>0?this.taxonomyFilters[i]=Array.from(t.keys()):delete this.taxonomyFilters[i];let s={page:1};Object.keys(this.taxonomyFilters).length>0&&(s.taxonomy=this.taxonomyFilters),this.updateFilter(s)}clearAllTaxonomies(){this.taxonomyFilters={},window.removeChildren(this.ui.selectedTax),this.updateFilter({taxonomy:null,page:1})}initFilters(){this.filters={content:this.contentTypes[0],orderby:"date",order:"desc",page:1},this.config.context&&(this.filters.context=this.config.context),this.config.source&&(this.filters.source=this.config.source),this.processCachedFilters(),this.processURLFilters(),this.syncUIToFilters()}syncUIToFilters(){Object.entries(this.filters).forEach((([e,t])=>{const i=this.ui.filterContainer.querySelector(`[data-filter="${e}"][value="${t}"]`);i&&(i.checked=!0)})),this.updateContentFor(this.filters.content)}nextPage(){this.store.setFilter("page",this.store.filters.page++)}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content",delayFetch:!0});this.store=e.feed,this.store.subscribe(((e,t)=>{"data-loaded"===e&&(this.renderItems(),this.ui.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse.has_more&&(this.ui.loadMore.hidden=!this.store.lastResponse.has_more))}))}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse&&this.store.lastResponse.has_more&&this.nextPage()}))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get(`f_${t}`);if(i){this.filters[t]=i;let e=this.ui.filters[t];e&&(e.checked=!0)}}));let t=!1;if(e.forEach(((e,i)=>{if(i.startsWith("f_tax_")){t=!0;const s=i.replace("f_tax_","");this.taxonomyFilters[s]||(this.taxonomyFilters[s]=[]),this.taxonomyFilters[s]=e.split(",").map(Number)}})),t)for(let[e,t]in Object.entries(this.taxonomyFilters)){let i=this.ui.filterContainer.querySelector(`[data-taxonomy="${e}"]`);i&&(i.dataset.fieldId?(this.selector.get(i.dataset.fieldId).selectedTerms=new Set(t),this.selector.initFieldDisplay(i.dataset.fieldId)):this.selector.registerField(i,{button:i,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax,selectedItems:t}))}return!0}updateURL(){const e=new URLSearchParams;["content","order","orderby","match"].forEach((t=>{this.filters[t]&&e.set(`f_${t}`,this.filters[t])})),Object.entries(this.taxonomyFilters).forEach((([t,i])=>{i.length>0&&e.set(`f_tax_${t}`,i.join(","))}));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;window.history.pushState({filters:this.filters},"",t)}renderItems(){let e=this.store.getFiltered();if(1===this.store.filters.page&&window.removeChildren(this.ui.grid),0===e.length)return void this.a11y.announceItems(0,this.store.filters.page>0);const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.appendChild(r)}r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.observeImages(this.ui.grid),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,this.store.filters.page>1,this.store.hasMore))};e.length>0?i(0):this.a11y.announceItems(0,this.store.filters.page>1,!1),this.ui.filters.match.hidden=0===Object.keys(this.taxonomyFilters).length,this.ui.clearFilter.hidden=0===Object.keys(this.taxonomyFilters).length}createItemElement(e){let t=window.getTemplate("feed-item");return Object.hasOwn(t.dataset,"timeline")?this.createTimelineElement(e,t):t}createTimelineElement(e,t){var i,s;let[r,a,o,n,l,d,h,c,m,u]=[t,t.querySelector("a"),t.querySelector("img.before"),t.querySelector("img.after"),t.querySelector("summary span:last-of-type"),t.querySelector("p.started time"),t.querySelector("p.updated time"),t.querySelector("p.total b"),t.querySelector(".term-list"),Object.values(e.fields.order)],f=u.length-1,y=e.images[u[0].post_thumbnail],g=e.images[u[f].post_thumbnail];return[r.dataset.id,a.href,o.src,o.dataset.small,o.dataset.medium,n.src,n.dataset.small,n.dataset.medium,l.textContent,d.textContent,h.textContent,c.textContent]=[e.id,e.url,y.tiny,y.small,y.medium,g.tiny,g.small,g.medium,`${l.textContent} ${f} Tx`,null!==(i=u[0].date)&&void 0!==i?i:e.date,null!==(s=u[f].date)&&void 0!==s?s:"",`${f} Treatments`],t}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}addPlaceholders(){let e=this.contentTypes.length;const t=document.createDocumentFragment();for(let i=0;i<12;i++){let i,s=window.getTemplate("placeholderTemplate"),r=Math.floor(Math.random()*e);i=this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[r])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),s.append(i),t.append(s)}this.ui.grid.append(t)}updateFilter(e){let t=["taxonomy","favourites","match",...Object.keys(this.filters)];e=Object.keys(e).filter((e=>t.includes(e))).reduce(((t,i)=>(t[i]=e[i],t)),{}),window.getDifferences.map(this.filters,e)&&(this.filters={...this.filters,...e},this.updateURL(),this.store.setFilters(e))}updateContentFor(e){this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]').forEach((t=>{const i=t.dataset.for?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)})),this.ui.filterContainer.querySelectorAll("[data-for]").forEach((t=>{const i=t.dataset.for?.split(",")||[];i.length>0&&(t.hidden=!i.includes(e),t.hidden&&t.checked&&(t.checked=!1))}));const t=this.ui.filterContainer.querySelector('[name="orderby"]:checked');this.updateOrderDirectionVisibility(t?.value)}updateOrderDirectionVisibility(e){const t=this.ui.filterContainer.querySelector(".order-direction");if(t){const i=t.dataset.forOrder?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)}}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.imageObserver=null,this.resizeObserver=null,"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{this.loadImage(e.target),this.imageObserver.unobserve(e.target)}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?this.resizeObserver=new ResizeObserver((()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})):window.addEventListener("resize",(()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}loadImage(e){const t=this.getAppropriateImageSize(e);t&&t!==e.src&&(e.src=t,e.dataset.loaded="true")}getAppropriateImageSize(e){return window.innerWidth<768&&e.dataset.small?e.dataset.small:e.dataset.medium?e.dataset.medium:e.src}observeImages(e){e.querySelectorAll("img[data-small], img[data-medium]").forEach((e=>{e.dataset.loaded||this.imageObserver.observe(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
diff --git a/build/forms/view.asset.php b/build/forms/view.asset.php
index 7c0da0d..14684c2 100644
--- a/build/forms/view.asset.php
+++ b/build/forms/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '6084ed3247c497c65c42');
+<?php return array('dependencies' => array(), 'version' => '918c373929631a4cc439');
diff --git a/build/forms/view.js b/build/forms/view.js
index 428e09c..ac99e02 100644
--- a/build/forms/view.js
+++ b/build/forms/view.js
@@ -1 +1 @@
-(()=>{class o{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0})})),this.controller.subscribe(((o,t)=>{"form-submit"===o&&this.handleFormSubmission(t)}))}async handleFormSubmission(o){let t=o.formId,e=o.config,r=o.fullData,s=e.element,n={"X-WP-Nonce":jvbSettings.nonce,"Content-Type":"application/json"};s.closest(".jvb-form-block"),this.controller.showFormStatus(t,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",headers:n,body:JSON.stringify(r)});if(!o.ok){this.controller.showFormStatus(t,"error");const e=await o.json().catch((()=>({})));throw new Error(e.message||`Request failed with status ${o.status}`)}this.controller.showFormStatus(t,"submitted"),this.controller.showSummary(t,".jvb-form-block")}catch(o){throw o}finally{this.controller.store.delete(t)}}}document.addEventListener("DOMContentLoaded",(function(){new o}))})();
\ No newline at end of file
+(()=>{class o{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0,autoUpload:!1})})),this.controller.subscribe(((o,r)=>{"form-submit"===o&&this.handleFormSubmission(r)}))}async handleFormSubmission(o){let r=o.formId,e=o.config,t=o.fullData,n=e.element;const s=new FormData;for(const[o,r]of Object.entries(t))"_wpnonce"!==o&&"_wp_http_referer"!==o&&(Array.isArray(r)?r.forEach((r=>s.append(`${o}[]`,r))):"object"==typeof r&&null!==r?s.append(o,JSON.stringify(r)):s.append(o,r));window.jvbUploads&&(await window.jvbUploads.getFilesForForm(n)).forEach((({file:o,fieldName:r,uploadId:e,meta:t})=>{s.append(`${r}[]`,o)}));let l={};n.closest(".jvb-form-block"),this.controller.showFormStatus(r,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",credentials:"same-origin",headers:l,body:s}),e=await o.json();if(!o.ok)return this.controller.showFormStatus(r,"error"),void this.controller.handleFormError(n,e);if(this.controller.showFormStatus(r,"submitted"),this.controller.showSummary(r,".jvb-form-block"),window.jvbUploads){const o=n.querySelectorAll("[data-upload-field]");for(const r of o){const o=window.jvbUploads.determineFieldId(r);await window.jvbUploads.clearFieldFromStores(o)}}}catch(o){console.error("Form submission error:",o),this.controller.showFormStatus(r,"error"),this.controller.handleFormError(n,{message:"Network error. Please check your connection and try again.",code:"network_error"})}finally{this.controller.store.delete(r)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((r=>{"auth-loaded"===r&&new o}))}))})();
\ No newline at end of file
diff --git a/build/gmbreviews/render.php b/build/gmbreviews/render.php
index 43d203d..ee519f8 100644
--- a/build/gmbreviews/render.php
+++ b/build/gmbreviews/render.php
@@ -57,12 +57,12 @@
 		ob_start();
 		?>
 		<div class="gmb-reviews">
-			<div class="row btw">
+			<div class="row center">
 			<?php
 			if ($showStats && !empty($average) && !empty($total)) {
 				?>
 				<p>
-					<span class="stars" aria-label="<?= $average ?> out of 5 stars">
+					<span class="stars" title="<?= $average ?> out of 5 stars">
 						<?php
 						$fullStars = floor($average);
 						$hasHalfStar = ($average - $fullStars) >= 0.5;
@@ -91,20 +91,21 @@
 			}
 			?>
 
+
+		</div>
 			<?php
 			if ($showReviewLink && !empty($reviewUrl)) {
 				?>
 				<a href="<?=esc_url($reviewUrl)?>"
-					class="button"
-				   	target="_blank"
-					rel="noopener noreferrer">
+				   class="button"
+				   target="_blank"
+				   rel="noopener noreferrer">
 					<?= jvbIcon('star', ['style' => 'fill']) ?>
 					Leave Your Review
 				</a>
 				<?php
 			}
 			?>
-		</div>
 
 		<ul>
 			<?php
@@ -145,6 +146,14 @@
 							<?php } ?>
 
 							<div class="row start wrap">
+								<?php if ($showRating && $rating > 0) { ?>
+									<div class="stars" title="<?= $rating ?> out of 5 stars">
+										<?php
+										for ($i = 1; $i <= 5; $i++) {
+											echo ($i <= $rating) ? jvbIcon('star', ['style' => 'fill']) : jvbIcon('star', ['style' => 'light']);
+										} ?>
+									</div>
+								<?php } ?>
 								<p><?= esc_html($reviewer)?></p>
 								<?php
 								// Date
@@ -155,14 +164,7 @@
 										<?= esc_html($formatted_date) ?>
 									</time>
 								<?php } ?>
-								<?php if ($showRating && $rating > 0) { ?>
-									<div class="stars" aria-label="<?= $rating ?> out of 5 stars">
-										<?php
-										for ($i = 1; $i <= 5; $i++) {
-											echo ($i <= $rating) ? jvbIcon('star', ['style' => 'fill']) : jvbIcon('star', ['style' => 'light']);
-										} ?>
-									</div>
-								<?php } ?>
+
 							</div>
 						</cite>
 					</blockquote>
diff --git a/build/gmbreviews/style-index-rtl.css b/build/gmbreviews/style-index-rtl.css
index 89a29d1..16e3bff 100644
--- a/build/gmbreviews/style-index-rtl.css
+++ b/build/gmbreviews/style-index-rtl.css
@@ -1 +1 @@
-.gmb-reviews{max-width:none}.gmb-reviews>.row.btw{max-width:var(--wide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--wider);padding:0}.gmb-reviews ul li{background-color:var(--base-100);margin:2rem 0;padding:1rem;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){right:-2rem}.gmb-reviews ul li:nth-of-type(2n){left:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:none}.gmb-reviews>.row.center{margin:0 auto;max-width:var(--content);--gap:.5rem 6rem}.gmb-reviews>.row.center p{width:-moz-fit-content;width:fit-content}.gmb-reviews .button{display:flex;height:-moz-max-content;height:max-content;margin:0 auto 2rem;width:66.6%}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:flex-start}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--full);padding:0}.gmb-reviews ul li{max-width:none;padding:4rem 1rem;width:100%}.gmb-reviews ul li:nth-of-type(odd){background-color:var(--base-50)}.gmb-reviews ul li:nth-of-type(odd) blockquote{--background:var(--base-50)}.gmb-reviews ul li:nth-of-type(2n){background-color:var(--base-100)}.gmb-reviews ul li:nth-of-type(2n) blockquote{--background:var(--base-100)}.gmb-reviews blockquote{margin:0 auto;max-width:var(--content);padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{right:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{right:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
diff --git a/build/gmbreviews/style-index.css b/build/gmbreviews/style-index.css
index e8a3bc2..2352340 100644
--- a/build/gmbreviews/style-index.css
+++ b/build/gmbreviews/style-index.css
@@ -1 +1 @@
-.gmb-reviews{max-width:none}.gmb-reviews>.row.btw{max-width:var(--wide)}.gmb-reviews>.row.btw .button{height:-moz-max-content;height:max-content;width:100%}.gmb-reviews>.row.btw p{width:-moz-fit-content;width:fit-content}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:center}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--wider);padding:0}.gmb-reviews ul li{background-color:var(--base-100);margin:2rem 0;padding:1rem;position:relative}@media(min-width:768px){.gmb-reviews ul li:nth-of-type(odd){left:-2rem}.gmb-reviews ul li:nth-of-type(2n){right:-2rem}}.gmb-reviews blockquote{margin:0;padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
+.gmb-reviews{max-width:none}.gmb-reviews>.row.center{margin:0 auto;max-width:var(--content);--gap:.5rem 6rem}.gmb-reviews>.row.center p{width:-moz-fit-content;width:fit-content}.gmb-reviews .button{display:flex;height:-moz-max-content;height:max-content;margin:0 auto 2rem;width:66.6%}.gmb-reviews .stars{align-items:center;display:inline-flex;flex-wrap:nowrap;justify-content:flex-start}.gmb-reviews ul{list-style:none;margin:0;max-width:var(--full);padding:0}.gmb-reviews ul li{max-width:none;padding:4rem 1rem;width:100%}.gmb-reviews ul li:nth-of-type(odd){background-color:var(--base-50)}.gmb-reviews ul li:nth-of-type(odd) blockquote{--background:var(--base-50)}.gmb-reviews ul li:nth-of-type(2n){background-color:var(--base-100)}.gmb-reviews ul li:nth-of-type(2n) blockquote{--background:var(--base-100)}.gmb-reviews blockquote{margin:0 auto;max-width:var(--content);padding:0}.gmb-reviews blockquote .content,.gmb-reviews blockquote .content:after{border-width:4px 1px}.gmb-reviews blockquote .content:before{border-width:8px;bottom:-4px}.gmb-reviews blockquote cite{position:relative}.gmb-reviews blockquote cite img{left:-8rem;position:absolute;top:0;width:4.5rem}.gmb-reviews blockquote cite p{margin:0}.gmb-reviews blockquote cite .wrap{--wrap:wrap}.gmb-reviews blockquote cite .wrap p,.gmb-reviews blockquote cite .wrap time{max-width:49%}.gmb-reviews blockquote cite .wrap .stars{width:100%}.gmb-reviews blockquote time{white-space:nowrap}.gmb-reviews .stars .icon{background-color:var(--action-0)}.gmb-reviews article{background-color:var(--base);border-radius:var(--radius-outer);padding:1rem}.gmb-reviews article header{--align:center}.gmb-reviews article header>img{left:0;position:relative}.gmb-reviews article time{font-style:italic}.gmb-reviews article .review{padding:1.5rem}.gmb-reviews article h4{width:-moz-max-content;width:max-content}.gmb-reviews article .icon{color:var(--action-0)}.gmb-reviews .footer .button{width:100%}
diff --git a/inc/admin/Integrations.php b/inc/admin/Integrations.php
index a774548..4f47744 100644
--- a/inc/admin/Integrations.php
+++ b/inc/admin/Integrations.php
@@ -64,20 +64,28 @@
 		wp_enqueue_script(
 			'jvb-admin-scripts',
 			JVB_URL . 'assets/js/min/integrations.min.js',
-			[],
+			['jvb-auth'],
 			'1.0.0',
 			true
 		);
 
-		wp_localize_script(
-			'jvb-admin-scripts',
-			'jvbSettings',
-			[
-				'api'    => rest_url('jvb/v1/'),
-				'currentUser'	=> get_current_user_id(),
-				'nonce'  => wp_create_nonce('wp_rest'),
-			]
-		);
+		$queue = [
+			'api' => rest_url('jvb/v1/'),
+			'redirect' => wp_login_url(home_url(add_query_arg(null, null))),
+			'labels' => jvbGetLabels(),
+		];
+
+		wp_localize_script('jvb-auth', 'jvbSettings', $queue);
+//
+//		wp_localize_script(
+//			'jvb-admin-scripts',
+//			'jvbSettings',
+//			[
+//				'api'    => rest_url('jvb/v1/'),
+//				'currentUser'	=> get_current_user_id(),
+//				'nonce'  => wp_create_nonce('wp_rest'),
+//			]
+//		);
 	}
 
 	/**
diff --git a/inc/blocks/FormBlock.php b/inc/blocks/FormBlock.php
index b8f4ed9..edffbe7 100644
--- a/inc/blocks/FormBlock.php
+++ b/inc/blocks/FormBlock.php
@@ -5,6 +5,7 @@
 use JVBase\meta\MetaManager;
 use JVBase\managers\CloudflareTurnstile;
 use Exception;
+use JVBase\utility\Features;
 use WP_Block;
 
 if (!defined('ABSPATH')) {
@@ -36,7 +37,6 @@
 	public function __construct()
 	{
 		$this->cache = CacheManager::for('form_blocks', WEEK_IN_SECONDS);
-
 		// Initialize forms from filter
 		$this->forms = $this->registerForms();
 		$this->form_contact = apply_filters('jvb_form_contact', '');
@@ -47,6 +47,28 @@
 		// Register forms data for the block editor
 		add_action('enqueue_block_editor_assets', [$this, 'localizeFormsData']);
 		add_action('init', [$this, 'registerBlock']);
+		add_filter('render_block', [$this, 'maybeEnqueueScripts'], 10, 2);
+	}
+
+	/**
+	 * Enqueue scripts when rendering form block
+	 */
+	public function maybeEnqueueScripts(string $block_content, array $block): string
+	{
+		// Only process our form blocks
+		if ($block['blockName'] !== 'jvb/forms') {
+			return $block_content;
+		}
+
+		// Enqueue Turnstile if needed
+		if (Features::forSite()->hasIntegration('cloudflare')) {
+			$cloudflare = JVB()->connect('cloudflare');
+			if ($cloudflare->isSetUp()) {
+				$cloudflare->enqueueTurnstileScripts();
+			}
+		}
+
+		return $block_content;
 	}
 
 	public function registerBlock()
@@ -264,7 +286,7 @@
 		}
 
 		echo '<form id="' . esc_attr($form_id) . '" data-form-id="'.esc_attr($type).'" data-save="form" data-noautosave>';
-		wp_nonce_field('jvb_form_' . $type);
+//		wp_nonce_field('jvb_form_' . $type);
 	}
 
 	/**
@@ -392,7 +414,7 @@
 	 */
 	protected function renderTurnstile(): void
 	{
-		if (!jvbSiteUsesCloudflare()) {
+		if (!Features::hasIntegration('cloudflare')) {
 			return;
 		}
 
diff --git a/inc/blocks/VideoCoverBlock.php b/inc/blocks/VideoCoverBlock.php
index cb70517..298ee76 100644
--- a/inc/blocks/VideoCoverBlock.php
+++ b/inc/blocks/VideoCoverBlock.php
@@ -57,7 +57,8 @@
 		$date = date('c',strtotime($post->post_date));
 		$title = $attributes['title'] ?? $post->post_title;
 		$description = $attributes['description'] ?? $post->post_excerpt;
-
+		$title = "Legacy Tattoo Removal";
+		$description="A video of Madi Rawson performing laser tattoo removal treatments.";
 		// If no video sources, return empty
 		if (empty($video_sources)) {
 			return '';
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index bef256d..cbfaf3b 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -152,6 +152,9 @@
  */
 function jvbHelpMenu():string
 {
+	if (!Features::forSite()->has('helpMenu')) {
+		return '';
+	}
     $out = get_option(BASE.'help_menu');
 
     if ($out === false) {
diff --git a/inc/integrations/Cloudflare.php b/inc/integrations/Cloudflare.php
index 4a44686..8b2bc13 100644
--- a/inc/integrations/Cloudflare.php
+++ b/inc/integrations/Cloudflare.php
@@ -11,7 +11,6 @@
 {
 	private string $site_key;
 	private string $secret_key;
-	private bool $auto_protect_forms = true;
 	private string $theme = 'light';
 	private string $size = 'normal';
 
@@ -56,13 +55,9 @@
 	{
 		$this->site_key = $this->credentials['site_key'] ?? '';
 		$this->secret_key = $this->credentials['secret_key'] ?? '';
-		$this->auto_protect_forms = $this->credentials['auto_protect_forms'] ?? true;
 		$this->theme = $this->credentials['theme'] ?? 'light';
 		$this->size = $this->credentials['size'] ?? 'normal';
 
-		if ($this->isSetUp()) {
-			$this->initializeTurnstileProtection();
-		}
 	}
 
 	/**
@@ -105,36 +100,6 @@
 		return array_key_exists('success', $result);
 	}
 
-	/**
-	 * Initialize Turnstile protection on various forms
-	 */
-	private function initializeTurnstileProtection(): void
-	{
-		// Skip on local development if configured
-		if (defined('JVB_LOCAL') && strpos(get_home_url(), JVB_LOCAL) !== false) {
-			return;
-		}
-
-		if (!$this->auto_protect_forms) {
-			return;
-		}
-
-		// WordPress login/registration forms
-		add_action('login_enqueue_scripts', [$this, 'enqueueTurnstileScripts']);
-		add_action('login_form', [$this, 'renderTurnstile']);
-		add_action('register_form', [$this, 'renderTurnstile']);
-		add_action('lostpassword_form', [$this, 'renderTurnstile']);
-
-		// Verification hooks
-		add_filter('authenticate', [$this, 'verifyLoginTurnstile'], 99, 3);
-		add_filter('registration_errors', [$this, 'verifyRegisterTurnstile'], 10, 3);
-		add_action('lostpassword_post', [$this, 'verifyLostpasswordTurnstile']);
-
-		// Custom form support
-		add_action('jvb_form_before_submit', [$this, 'renderTurnstile']);
-		add_filter('jvb_form_validate', [$this, 'validateFormTurnstile'], 10, 2);
-	}
-
 	protected function validateCredentials(array $credentials): bool
 	{
 		if (empty($credentials['site_key'])) {
@@ -218,6 +183,7 @@
 	 */
 	public function verifyTurnstile(?string $token = null, string $remote_ip = ''): bool
 	{
+		$this->ensureInitialized();
 		if (!$this->isSetUp()) {
 			return false;
 		}
@@ -265,78 +231,6 @@
 	}
 
 	/**
-	 * Verify login form Turnstile
-	 */
-	public function verifyLoginTurnstile($user, string $username, string $password)
-	{
-		// Skip verification if already logged in or no credentials
-		if (is_user_logged_in() || empty($username) || empty($password)) {
-			return $user;
-		}
-
-		// Skip on AJAX requests for compatibility
-		if (wp_doing_ajax()) {
-			return $user;
-		}
-
-		// If already have an error, return it
-		if (is_wp_error($user)) {
-			return $user;
-		}
-
-		// Verify Turnstile
-		if (!$this->verifyTurnstile()) {
-			return new \WP_Error(
-				'turnstile_verification_failed',
-				'<strong>ERROR</strong>: Please complete the security check.'
-			);
-		}
-
-		return $user;
-	}
-
-	/**
-	 * Verify registration form Turnstile
-	 */
-	public function verifyRegisterTurnstile($errors, string $sanitized_user_login, string $user_email)
-	{
-		if (!$this->verifyTurnstile()) {
-			$errors->add(
-				'turnstile_verification_failed',
-				'<strong>ERROR</strong>: Please complete the security check.'
-			);
-		}
-		return $errors;
-	}
-
-	/**
-	 * Verify lost password form Turnstile
-	 */
-	public function verifyLostpasswordTurnstile($errors): void
-	{
-		if (!$this->verifyTurnstile()) {
-			if (!is_wp_error($errors)) {
-				$errors = new \WP_Error();
-			}
-			$errors->add(
-				'turnstile_verification_failed',
-				'<strong>ERROR</strong>: Please complete the security check.'
-			);
-		}
-	}
-
-	/**
-	 * Validate Turnstile for custom JVB forms
-	 */
-	public function validateFormTurnstile(array $errors, array $form_data): array
-	{
-		if (!$this->verifyTurnstile()) {
-			$errors[] = 'Please complete the security verification.';
-		}
-		return $errors;
-	}
-
-	/**
 	 * Get site key for frontend use
 	 */
 	public function getSiteKey(): string
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index b820041..498c2b8 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -1136,20 +1136,23 @@
 	private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
 	{
 		$site_name = get_bloginfo('name');
-		$reset_url = get_home_url(null, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
+		$reset_url = get_home_url(null, "login?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
 
 		$message = sprintf(
 			"Welcome to %s!\n\n" .
-			"Your account has been created. Please click the link below to set your password:\n\n" .
+			"Your account has been created. Please click the button below to set your password:\n\n" .
+			"%s\n\n" .
+			"Or, copy and paste the link below:\n\n".
 			"%s\n\n" .
 			"Once you've set your password, you can:\n" .
 			"- View your order history\n" .
 			"- Save your favorite items\n" .
 			"- Speed up checkout with saved payment methods\n\n" .
-			"Thanks,\n%s",
+			"If you didn't create this account, please ignore this email.\n\n" .
+			"Thanks,\n",
 			$site_name,
-			$reset_url,
-			$site_name
+			JVB()->email()->button('Reset Password', $reset_url),
+			JVB()->email()->link($reset_url),
 		);
 
 		JVB()->email()->sendEmail(
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 71392bb..12b73ec 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -774,9 +774,6 @@
 			return new WP_Error('rate_limit', 'Rate limit exceeded. Please try again later.');
 		}
 
-		// Debug: Check if credentials are loaded
-		error_log('['.$this->service_name.'] Make Request - Credentials loaded: ' . (!empty($this->credentials) ? 'Yes' : 'No'));
-		error_log('With Credentials: '.print_r($this->credentials, true));
 
 		$attempt = 0;
 		$lastError = null;
@@ -1215,14 +1212,10 @@
 
 	public function handleAjaxResponse()
 	{
-		error_log('Ajax Response: '.print_r($_GET, true));
 
 		$code = $_GET['code'];
 		$state =  $_GET['state'];
 
-		error_log('OAuth Callback - Code: ' . $code);
-		error_log('OAuth Callback - State: ' . $state);
-
 
 		$state_parts = explode('|', $state);
 		$state_key = $state_parts[0] ?? '';
@@ -1230,16 +1223,13 @@
 		$user_id = ($user_id === 0) ? null : $user_id;
 		$return_url = isset($state_parts[2]) ? base64_decode($state_parts[2]) : admin_url('admin.php?page=jvb-integrations');
 
-		error_log('Service: '.print_r($this->service_name, true));
 		$state_data = get_transient('oauth_state_' . $state_key);
-		error_log('State Data: '.print_r($state_data, true));
 		if (!$state_data || $state_data['service'] !== $this->service_name) {
 			wp_die('Invalid state parameter', 'OAuth Error');
 		}
 
 		// Delete the transient to prevent reuse
 		delete_transient('oauth_state_' . $state_key);
-		error_log('Return URL: '.print_r($return_url, true));
 		// Handle error from OAuth provider
 		if (array_key_exists('error', $_GET)) {
 			$error_description = $_GET['error_description'] ?? 'Authorization denied';
@@ -1637,8 +1627,6 @@
 
 		$auth_url = $this->oauth['authorize'] . '?' . http_build_query($params);
 
-		// Debug log for troubleshooting
-		error_log("Generated OAuth URL for {$this->service_name}: " . $auth_url);
 
 		return $auth_url;
 	}
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index 60e4d80..aab2903 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -1849,17 +1849,19 @@
 
 		$message = sprintf(
 			"Welcome to %s!\n\n" .
-			"Your account has been created. Please click the link below to set your password:\n\n" .
+			"Your account has been created. Please click the button below to set your password:\n\n" .
 			"%s\n\n" .
-			"Once you've set your password, you can log in to:\n" .
+			"Or, copy and paste the link below:\n\n".
+			"%s\n\n" .
+			"Once you've set your password, you can:\n" .
 			"- View your order history\n" .
 			"- Save your favorite items\n" .
 			"- Speed up checkout with saved payment methods\n\n" .
 			"If you didn't create this account, please ignore this email.\n\n" .
-			"Thanks,\n%s",
+			"Thanks,\n",
 			$site_name,
-			$reset_url,
-			$site_name
+			JVB()->email()->button('Reset Password', $reset_url),
+			JVB()->email()->link($reset_url),
 		);
 
 		JVB()->email()->sendEmail(
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 49ec9c6..b129c15 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -81,14 +81,15 @@
 		// Statuses
 		if (Features::forContent($this->content)->has('is_calendar')) {
 			$this->skeleton->setCalendar();
-		}else {
-			$this->skeleton->setDefaultStatus();
 		}
 
+		$this->skeleton->setDefaultStatus();
+
 		// Views
 		$this->skeleton
-			->addViews(['grid', 'list', 'table'])
+			->addViews()
 			->defaultView('grid');
+		$this->skeleton->addItemActions();
 
 		// Filters
 		$this->skeleton->addDateFilter();
diff --git a/inc/managers/EmailManager.php b/inc/managers/EmailManager.php
index c1e5202..106a387 100644
--- a/inc/managers/EmailManager.php
+++ b/inc/managers/EmailManager.php
@@ -24,7 +24,7 @@
 class EmailManager
 {
 
-   private array $colours = JVB_EMAIL['colours'];
+   public array $colours = JVB_EMAIL['colours'];
    private string $title = JVB_EMAIL['content']['title'];
    private string $prefix = JVB_EMAIL['content']['subjectPrefix'];
    private string $signature = JVB_EMAIL['content']['signature'];
@@ -82,7 +82,7 @@
      * @param string $header Optional header text for the template
      * @return bool Whether the email was sent successfully
      */
-    public function sendEmail(string $to, string $subject, string $message, string $header = ''):bool
+    public function sendEmail(string $to, string $subject, string $message, string $header = '', array $headers = [], array $attachments = []):bool
     {
         // Make sure the content type is set to HTML
         add_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
@@ -91,7 +91,7 @@
         $formatted_message = $this->getEmailTemplate($message, $header);
 
         // Send the email
-        $result = wp_mail($to, $subject, $formatted_message);
+        $result = wp_mail($to, $subject, $formatted_message, $headers, $attachments);
 
         // Reset content type filter to avoid affecting other emails
         remove_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
@@ -105,13 +105,17 @@
      *
      * @return string
      */
-    private function getEmailTemplate(string $content, string $header = ''):string
+    private function getEmailTemplate(string $content, string $headerText = ''):string
     {
-        $logo = get_custom_logo();
+		$custom_logo_id = get_theme_mod( 'custom_logo' );
+		$logo_thumbnail = wp_get_attachment_image_src( $custom_logo_id, 'custom-logo-thumbnail' );
+
+		$logo = ($logo_thumbnail) ? '<img src="' . esc_url( $logo_thumbnail[0] ) . '" alt="Site Logo" width="150" height="150" style="max-width:120px;height:auto;">' : '';
+
 
         // Default header if none provided
-        if (empty($header)) {
-            $header = $this->title;
+        if (empty($headerText)) {
+			$headerText = $this->title;
         }
 
         return '<!DOCTYPE html>
@@ -120,7 +124,28 @@
             <meta charset="UTF-8">
             <meta name="viewport" content="width=device-width, initial-scale=1.0">
             <title>' . $this->title . ' | ' . $this->site_name .'</title>
-            <style>
+        </head>
+        <body style="font-family:Segoe UI, Tahoma, Geneva, Verdana, sans-serif;background-color: '.$this->colours['light'].';color:'.$this->colours['dark'].';line-height:1.5;margin:0;padding:0;">
+			<div style="background-color:'.$this->colours['dark'].';color:'.$this->colours['light'].';padding:5px;text-align:center;font-size:24px;font-weight:900;letter-spacing:1.5px;text-transform:uppercase;border-radius:5px 5px 0 0;">
+				<a href="' . $this->site_url . '" style="display:inline-block;vertical-align:middle;">
+					' . $logo . '
+				</a>
+				<p style="display:inline-block;vertical-align:middle;margin: 0 auto 0 0;">' . $headerText . '</p>
+			</div>
+			<div style="padding:4rem 0;width:100%;max-width:600px;margin:0 auto;">
+				' . $content . '
+				'.$this->signature().'
+			</div>
+			<div style="border-radius:0 0 5px 5px;text-align:center;margin-top:20px;font-size:12px;background-color:'.$this->colours['light-200'].'; color: '.$this->colours['dark-200'].';padding:10px;line-height:1.6;">
+				' . $this->footer . '
+			</div>
+        </body>
+        </html>';
+    }
+
+	private function oldStyle(){
+		$oldStyle ='
+		<style>
                 body, table, td, p, a, li, blockquote {
                     line-height: 1.5;
                 }
@@ -161,8 +186,7 @@
                     vertical-align: middle;
                 }
                 .header p {
-                    margin: 0;
-                    margin-left: auto;
+                    margin: 0 auto 0 0;
                 }
 
                 .content {
@@ -194,12 +218,24 @@
                     text-align: center;
                     margin-top: 20px;
                     font-size: 12px;
-                    backgorund-color: ' . $this->colours['light-200'] . ';
+                    background-color: ' . $this->colours['light-200'] . ';
                     color: ' . $this->colours['dark-200'] . ';
                     padding: 10px;
                     line-height: 1.6;
                 }
 
+                .notice {
+                	text-align: center;
+                	border-radius: 0 8px 8px 0;
+                	margin: 1rem 0;
+                	border-left: 4px solid '.$this->colours['action-0'].';
+                	padding: 1rem;
+                	background-color: '.$this->colours['light-100'].';
+                }
+                .notice strong {
+                	color: '.$this->colours['action-0'].';
+                }
+
                 .divider {
                     border-top: 1px solid ' . $this->colours['dark-200'] . ';
                     margin: 25px 0;
@@ -300,26 +336,8 @@
     }
 }
             </style>
-        </head>
-        <body>
-            <div class="email-container">
-
-                <div class="header">
-                    <a href="' . $this->site_url . '">
-                        ' . $logo . '
-                    </a>
-                    <p>' . $header . '</p>
-                </div>
-                <div class="content">
-                    ' . $content . '
-                </div>
-                <div class="footer">
-                    ' . $this->footer . '
-                </div>
-            </div>
-        </body>
-        </html>';
-    }
+		';
+	}
 
     /**
      * New user registration email to user
@@ -336,8 +354,6 @@
 			$user->user_login,
 		);
 		$message = apply_filters('jvbNewUserEmail', $message, $user);
-
-		$message .= $this->signature;
 		$wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, JVB_EMAIL['types']['newUser']['subject']?:'New User');
 
         // Change the subject line
@@ -362,7 +378,6 @@
         $message .= '<p><strong>Username:</strong> ' . $user->user_login . '</p>';
         $message .= '<p><strong>Email:</strong> ' . $user->user_email . '</p>';
 		$message = apply_filters('jvbNewUserAdminEmail', $message, $user);
-        $message .= $this->signature;
         $emailData['message'] = $this->getEmailTemplate($message, 'New User Registration');
         $emailData['subject'] = $this->prefix .'New ' . str_replace(BASE, '', array_values($user->roles)[0]).': '.$user->display_name;
 
@@ -390,15 +405,15 @@
 			%s
 			<p>Or copy and paste this link into your browser:</p>
 			%s
-			<div class="divider"></div>
+			%s
 			<p>This password reset link is only valid for 24 hours.</p>',
 			$user->display_name,
 			$user_login,
-			JVB()->email()->button($reset_url,'Reset Password'),
-			JVB()->email()->link($reset_url)
+			$this->button($reset_url,'Reset Password'),
+			$this->link($reset_url),
+			$this->divider()
 		);
 		$content = apply_filters('jvbPasswordResetEmail', $content, $user_login, $user, $reset_url);
-		$content .= $this->signature;
         return $this->getEmailTemplate($content, 'Password Reset');
     }
 
@@ -432,16 +447,16 @@
         <p>Ideally you already know this: someone asked to change the email for your account.</p>
         <p><strong>Old Email:</strong> %s</p>
         <p><strong>New Email:</strong> %s</p>
-        <div class="divider"></div>
+        %s
         <p>If this is news to you, or you did not request this - please contact us immediately. You can <a href="sms:+18258239916">text us</a> or reply to this email."></a></p>
         %s',
 			$newUser['first_name'],
 			$oldUser['user_email'],
 			$newUser['user_email'],
-			JVB()->email()->button(wp_login_url(), 'Log In To Your Account')
+			$this->divider(),
+			$this->button(wp_login_url(), 'Log In To Your Account')
 		);
 		$content = apply_filters('jvbEmailChangeRequestEmail', $content, $oldUser, $newUser);
-        $content .= $this->signature;
         $email_change_email['message'] = $this->getEmailTemplate($content, 'Email Address Changed');
 		$prefix = JVB_EMAIL['types']['emailChange']['showPrefix']??true;
 		$prefix = ($prefix) ? $this->prefix : '';
@@ -469,15 +484,14 @@
 			%s
 			<p>Or copy and paste this link into your browser:</p>
 			%s',
-			JVB()->email()->button($confirm_url, 'Confirm this Email'),
-			JVB()->email()->link($confirm_url)
+			$this->button($confirm_url, 'Confirm this Email'),
+			$this->link($confirm_url)
 		);
 
 		$content = apply_filters('jvbEmailChangedEmail', $content, $confirm_url);
 
-        $content .= '<div class="divider"></div>';
+        $content .= $this->divider();
         $content .= '<p>If you did not request this change, you can safely ignore this email and nothing will change.</p>';
-        $content .= $this->signature;
 
         return $this->getEmailTemplate($content, 'Confirm Email Change');
     }
@@ -499,10 +513,9 @@
 			<p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p>
 			%s',
 			$oldUser['first_name'],
-			JVB()->email()->button(wp_login_url(), 'Log In to Your Account')
+			$this->button(wp_login_url(), 'Log In to Your Account')
 		);
 		$content = apply_filters('jvbPasswordChangeEmail', $content, $oldUser, $newUser);
-        $content .= $this->signature;
 
         $pass_change_email['message'] = $this->getEmailTemplate($content, 'Password Changed');
 		$prefix = JVB_EMAIL['types']['passwordChange']['showPrefix']??true;
@@ -545,14 +558,13 @@
 			<p>Or copy and paste this link into your browser:</p>
 			%s',
 			$request_name,
-			JVB()->email()->button($confirm_url, 'Confirm'),
-			JVB()->email()->link($confirm_url)
+			$this->button($confirm_url, 'Confirm'),
+			$this->link($confirm_url)
 		);
 		$message = apply_filters('jvbPersonalDataExport', $message, $request_type, $confirm_url, $email_data);
 
-        $message .= '<div class="divider"></div>';
+        $message .= $this->divider();
         $message .= '<p>If you did not make this request, you can safely ignore this email.</p>';
-        $message .= $this->signature;
 
         return $this->getEmailTemplate($message, 'Action Confirmation');
     }
@@ -577,28 +589,30 @@
 			%s
 			<p>Or you can copy and paste this link into your browser:</p>
 			%s
-			<div class="divider"></div>
+			%s
 			<p><strong>Important:</strong> For privacy and security, this link will expire at %s.</p>',
-			JVB()->email()->button($download_url, 'Download Your Data'),
-			JVB()->email()->link($download_url),
+			$this->button($download_url, 'Download Your Data'),
+			$this->link($download_url),
+			$this->divider(),
 			$expiresAt
 		);
 		$message = apply_filters('jvbPersonalDataExported', $message, $download_url, $expiresAt, $email_data);
-        $message .= $this->signature;
 
         return $this->getEmailTemplate($message, 'Your Personal Data Export');
     }
 
 	public function signature():string
 	{
-		return $this->signature;
+		return '<p><i>'.$this->signature.'</i></p>';
 	}
 
 	public function button(string $link, string $title):string
 	{
 		return sprintf(
-			'<p style="text-align: center;"><a href="%s" class="button">%s</a></p>',
+			'<p style="text-align: center;"><a href="%s" style="display:inline-block;padding:16px 10px;border-radius:4px;background-color:%s;color:%s;text-decoration:none;font-weight:bold;margin:15px 0;letter-spacing:1px;text-transform:uppercase;">%s</a></p>',
 			$link,
+			$this->colours['action-0'],
+			$this->colours['action-contrast'],
 			$title
 		);
 	}
@@ -606,11 +620,263 @@
 	public function link(string $link):string
 	{
 		return sprintf(
-			'<p style="user-select:all;">%s</p>',
+			'<code style="color:%s;border:1px solid %s;background-color:%s;border-radius:4px;user-select:all;">%s</code>',
+			$this->colours['dark-200'],
+			$this->colours['dark-100'],
+			$this->colours['light-100'],
 			$link
 		);
 	}
 
+	public function divider():string
+	{
+		return '<div style="border-top:1px solid '.$this->colours['dark-200'].';margin:25px 0;"></div>';
+	}
+
+	public function notice(string $text):string
+	{
+		return '<div style="border-radius: 0 8px 8px 0; margin: 1rem 0; border-left: 4px solid '.$this->colours['action-0'].';padding:1rem;background-color:'.$this->colours['light-100'].';">
+		'.str_replace('<strong>', '<strong style="color:'.$this->colours['action-0'].'">',$text).'
+		</div>';
+	}
+
+	public function callout(string $text):string
+	{
+		return sprintf(
+			'<div style="padding:2rem;margin:2rem 3rem;background-color:%s;color:%s;">%s</div>',
+			$this->colours['action-0'],
+			$this->colours['action-contrast'],
+			str_replace('<a', '<a style="background-color:'.$this->colours['action-contrast'].';padding: 0 .125rem;border-radius:4px;"', $text)
+		);
+	}
+
+	public function table(array $summarize, string $title = '', array $actions = []):string
+	{
+		if (empty($summarize)){
+			return '';
+		}
+
+		if (!empty($title)) {
+			$title = sprintf(
+			'<h2 style="color:%s;border-bottom:2px solid %s;padding-bottom:15px;margin-bottom:20px;text-align:center;">%s</h2>',
+			$this->colours['dark-200'],
+			$this->colours['dark-200'],
+			$title
+			);
+		}
+		$content = '';
+		foreach ($summarize as $index=> $item) {
+			if (!array_key_exists('label', $item) && !array_key_exists('value', $item)) {
+				continue;
+			}
+			$content .= sprintf(
+				'<div style="padding:10px 0;border-bottom:1px solid %s;background-color:%s;">
+					<span style="display:inline-block;vertical-align:top;font-weight:600;color:%s;width:%s;">%s</span>
+					<div style="display:inline-block;vertical-align:top;width:%s;">%s</div>
+				</div>',
+				$this->colours['dark-200'],
+				($index%2 === 0) ? $this->colours['light-100'] : $this->colours['light-50'],
+				$this->colours['dark-200'],
+				'19%',
+				$item['label'],
+				'80%',
+				$item['value']
+			);
+		}
+
+		return sprintf(
+			'<div style="max-width:500px;margin:40px auto;padding:30px;background-color:%s;border-radius:10px;border:2px dashed %s;">%s%s</div>',
+			$this->colours['light-100'],
+			$this->colours['dark-200'],
+			$title,
+			$content
+		);
+	}
+
+	public function card(string $content, string $title = ''):string
+	{
+		$titleHtml = '';
+		if (!empty($title)) {
+			$titleHtml = sprintf(
+				'<h3 style="margin:0 0 15px 0;color:%s;font-size:18px;font-weight:600;">%s</h3>',
+				$this->colours['dark'],
+				esc_html($title)
+			);
+		}
+
+		return sprintf(
+			'<div style="background-color:%s;border:1px solid %s;border-radius:8px;padding:20px;margin:15px 0;">%s%s</div>',
+			$this->colours['light-50'],
+			$this->colours['light-200'],
+			$titleHtml,
+			$content
+		);
+	}
+
+	public function stat(string $number, string $label, string $description = ''):string
+	{
+		$desc = $description ? sprintf(
+			'<p style="margin:5px 0 0 0;font-size:12px;color:%s;">%s</p>',
+			$this->colours['dark-200'],
+			esc_html($description)
+		) : '';
+
+		return sprintf(
+			'<div style="text-align:center;padding:20px;background-color:%s;border-radius:8px;margin:10px 0;">
+            <div style="font-size:36px;font-weight:700;color:%s;margin:0 0 5px 0;">%s</div>
+            <div style="font-size:14px;font-weight:600;color:%s;text-transform:uppercase;letter-spacing:1px;">%s</div>
+            %s
+        </div>',
+			$this->colours['light-100'],
+			$this->colours['action-0'],
+			esc_html($number),
+			$this->colours['dark-200'],
+			esc_html($label),
+			$desc
+		);
+	}
+
+	public function grid(array $items, int $columns = 2):string
+	{
+		$width = floor(100 / $columns) - 2; // 2% gap
+
+		$html = '<div style="display:table;width:100%;margin:20px 0;">';
+		foreach ($items as $index => $item) {
+			if ($index > 0 && $index % $columns === 0) {
+				$html .= '</div><div style="display:table;width:100%;margin:20px 0;">';
+			}
+			$html .= sprintf(
+				'<div style="display:table-cell;width:%d%%;padding:10px;vertical-align:top;">%s</div>',
+				$width,
+				$item
+			);
+		}
+		$html .= '</div>';
+
+		return $html;
+	}
+
+	public function image(string $src, string $alt = '', int $maxWidth = 600):string
+	{
+		return sprintf(
+			'<div style="text-align:center;margin:20px 0;">
+            <img src="%s" alt="%s" style="max-width:%dpx;width:100%%;height:auto;border-radius:8px;" />
+        </div>',
+			esc_url($src),
+			esc_attr($alt),
+			$maxWidth
+		);
+	}
+
+	public function h1(string $text):string
+	{
+		return sprintf(
+			'<h1 style="color:%s;font-size:24px;font-weight:700;margin:20px 0 10px 0;">%s</h1>',
+			$this->colours['dark'],
+			$text
+		);
+	}
+
+	public function h2(string $text):string
+	{
+		return sprintf(
+			'<h2 style="color:%s;font-size:20px;font-weight:600;margin:20px 0 10px 0;border-bottom:2px solid %s;padding-bottom:10px;">%s</h2>',
+			$this->colours['dark'],
+			$this->colours['dark-200'],
+			$text
+		);
+	}
+
+	public function h3(string $text):string
+	{
+		return sprintf(
+			'<h3 style="color:%s;font-size:18px;font-weight:600;margin:15px 0 10px 0;">%s</h3>',
+			$this->colours['dark-200'],
+			$text
+		);
+	}
+
+	public function list(array $items, bool $ordered = false):string
+	{
+		$tag = $ordered ? 'ol' : 'ul';
+		$style = $ordered
+			? 'list-style-type:decimal;padding-left:20px;margin:10px 0;'
+			: 'list-style-type:disc;padding-left:20px;margin:10px 0;';
+
+		$html = sprintf('<%s style="%s">', $tag, $style);
+		foreach ($items as $item) {
+			$html .= sprintf(
+				'<li style="margin:5px 0;line-height:1.6;">%s</li>',
+				$item
+			);
+		}
+		$html .= sprintf('</%s>', $tag);
+
+		return $html;
+	}
+
+	public function codeBlock(string $code, string $language = ''):string
+	{
+		return sprintf(
+			'<pre style="background-color:%s;border:1px solid %s;border-radius:4px;padding:15px;overflow-x:auto;font-family:\'Courier New\',monospace;font-size:13px;line-height:1.4;"><code>%s</code></pre>',
+			$this->colours['light-100'],
+			$this->colours['dark-200'],
+			esc_html($code)
+		);
+	}
+
+	public function badge(string $text, string $type = 'default'):string
+	{
+		$colors = [
+			'success' => ['bg' => '#d4edda', 'text' => '#155724'],
+			'warning' => ['bg' => '#fff3cd', 'text' => '#856404'],
+			'error' => ['bg' => '#f8d7da', 'text' => '#721c24'],
+			'info' => ['bg' => '#d1ecf1', 'text' => '#0c5460'],
+			'default' => ['bg' => $this->colours['light-200'], 'text' => $this->colours['dark-200']]
+		];
+
+		$color = $colors[$type] ?? $colors['default'];
+
+		return sprintf(
+			'<span style="display:inline-block;padding:4px 12px;border-radius:12px;background-color:%s;color:%s;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">%s</span>',
+			$color['bg'],
+			$color['text'],
+			esc_html($text)
+		);
+	}
+
+	public function alert(string $text, string $type = 'info'):string
+	{
+		$configs = [
+			'success' => ['bg' => '#d4edda', 'border' => '#28a745', 'text' => '#155724', 'icon' => '✓'],
+			'warning' => ['bg' => '#fff3cd', 'border' => '#ffc107', 'text' => '#856404', 'icon' => '⚠'],
+			'error' => ['bg' => '#f8d7da', 'border' => '#dc3545', 'text' => '#721c24', 'icon' => '✕'],
+			'info' => ['bg' => '#d1ecf1', 'border' => $this->colours['action-0'], 'text' => '#0c5460', 'icon' => 'ℹ']
+		];
+
+		$config = $configs[$type] ?? $configs['info'];
+
+		return sprintf(
+			'<div style="border-radius:8px;margin:1rem 0;border-left:4px solid %s;padding:1rem;background-color:%s;color:%s;">
+            <strong style="font-size:16px;">%s</strong> %s
+        </div>',
+			$config['border'],
+			$config['bg'],
+			$config['text'],
+			$config['icon'],
+			$text
+		);
+	}
+
+	public function spacer(int $height = 20):string
+	{
+		return sprintf('<div style="height:%dpx;"></div>', $height);
+	}
+
+	public function prefix():string
+	{
+		return $this->prefix;
+	}
 }
 
 
diff --git a/inc/managers/ErrorHandler.php b/inc/managers/ErrorHandler.php
index 166229e..9e6cc9a 100644
--- a/inc/managers/ErrorHandler.php
+++ b/inc/managers/ErrorHandler.php
@@ -249,14 +249,31 @@
      *
      * @return bool Whether the notification is sent successfully
      */
-    protected function notifyAdmin(string $component, string $message, array $context):bool
-    {
-        $admin_email = get_option('admin_email');
-        $subject = "[edmonton.ink Critical Error] {$component}";
-        $body = "Error: {$message}\n\nContext: " . print_r($context, true);
+	protected function notifyAdmin(string $component, string $message, array $context):bool
+	{
+		$admin_email = get_option('admin_email');
+		$subject = "[" . get_bloginfo('name') . " Critical Error] {$component}";
 
-        return JVB()->email()->sendEmail($admin_email, $subject, $body);
-    }
+		$body = JVB()->email()->alert(
+			'A critical error has occurred and requires immediate attention',
+			'error'
+		);
+
+		$body .= JVB()->email()->h2('Error Details');
+		$body .= JVB()->email()->card(
+			'<p><strong>Component:</strong> ' . esc_html($component) . '</p>' .
+			'<p><strong>Message:</strong></p>' .
+			JVB()->email()->codeBlock($message),
+			'Error Information'
+		);
+
+		if (!empty($context)) {
+			$body .= JVB()->email()->h3('Additional Context');
+			$body .= JVB()->email()->codeBlock(json_encode($context, JSON_PRETTY_PRINT));
+		}
+
+		return JVB()->email()->sendEmail($admin_email, $subject, $body, 'CRITICAL ERROR');
+	}
 
     /**
      * Gather summary of the most important errors
@@ -350,79 +367,92 @@
      * Send daily error summary email to administrator
      * @return bool Whether email is sent
      */
-    public function sendErrorSummary():bool
-    {
-        // Get summary data
-        $summary = $this->gatherErrorSummary();
+	public function sendErrorSummary():bool
+	{
+		$summary = $this->gatherErrorSummary();
 
-        // Only send if there are errors
-        if (empty($summary['frequent']) && empty($summary['critical'])) {
-            return false;
-        }
+		if (empty($summary['frequent']) && empty($summary['critical'])) {
+			return false;
+		}
 
-        $admin_email = get_option('admin_email');
-        $site_name = get_bloginfo('name');
-        $today = date('Y-m-d');
-        $yesterday = date('Y-m-d', strtotime('-1 day'));
+		$admin_email = get_option('admin_email');
+		$site_name = get_bloginfo('name');
+		$yesterday = date('Y-m-d', strtotime('-1 day'));
+		$subject = "[{$site_name}] Daily Error Summary - " . date('Y-m-d');
 
-        $subject = "[{$site_name}] Daily Error Summary - {$today}";
+		// Header with alert
+		$body = JVB()->email()->h1('Daily Error Summary');
+		$body .= sprintf('<p>Error summary for <strong>%s</strong></p>', $yesterday);
 
-        // Build email body
-        $body = "= Error Summary for {$yesterday} =\n\n";
+		// Summary stats in a grid
+		if (!empty($summary['stats'])) {
+			$stats = [
+				JVB()->email()->stat($summary['stats']->total_errors, 'Total Errors'),
+				JVB()->email()->stat($summary['stats']->critical_count, 'Critical', 'Requires attention'),
+				JVB()->email()->stat($summary['stats']->error_count, 'Errors'),
+				JVB()->email()->stat($summary['stats']->warning_count, 'Warnings')
+			];
+			$body .= JVB()->email()->grid($stats, 4);
+		}
 
-        // Add frequent errors section
-        if (!empty($summary['frequent'])) {
-            $body .= "== Most Frequent Errors ==\n\n";
+		// Alert if critical errors exist
+		if (!empty($summary['critical'])) {
+			$body .= JVB()->email()->alert(
+				sprintf('Found %d critical errors that need immediate attention', count($summary['critical'])),
+				'error'
+			);
+		}
 
-            foreach ($summary['frequent'] as $index => $error) {
-                $body .= ($index + 1) . ". [{$error->component}] {$error->error_type}\n";
-                $body .= "   Message: " . wp_trim_words($error->message, 20, '...') . "\n";
-                $body .= "   Count: {$error->count}\n\n";
-            }
-        }
+		$body .= JVB()->email()->spacer(20);
 
-        // Add critical errors section
-        if (!empty($summary['critical'])) {
-            $body .= "== Recent Critical Errors ==\n\n";
+		// Frequent errors section
+		if (!empty($summary['frequent'])) {
+			$body .= JVB()->email()->h2('Most Frequent Errors');
 
-            foreach ($summary['critical'] as $index => $error) {
-                $body .= ($index + 1) . ". [{$error->component}] {$error->error_type}\n";
-                $body .= "   Time: {$error->created_at}\n";
-                $body .= "   Message: " . $error->message . "\n\n";
+			foreach ($summary['frequent'] as $error) {
+				$cardContent = JVB()->email()->badge($error->count . 'x', 'warning') . ' ';
+				$cardContent .= '<strong>' . esc_html($error->error_type) . '</strong>';
+				$cardContent .= '<p style="margin:10px 0 5px 0;font-size:13px;">' . esc_html(wp_trim_words($error->message, 15)) . '</p>';
+				$cardContent .= '<p style="margin:0;font-size:12px;color:' . JVB()->email()->colours['dark-200'] . ';">
+                Source: ' . esc_html($error->source) . ' |
+                Logged in: ' . $error->logged_in_count . ' |
+                Logged out: ' . $error->logged_out_count . '
+            </p>';
 
-                // Include context for critical errors if available
-                if (!empty($error->context)) {
-                    $context = json_decode($error->context, true);
-                    if (is_array($context)) {
-                        $body .= "   Context:\n";
-                        foreach ($context as $key => $value) {
-                            if (is_array($value) || is_object($value)) {
-                                $value = json_encode($value);
-                            }
-                            $body .= "     - {$key}: {$value}\n";
-                        }
-                    }
-                    $body .= "\n";
-                }
-            }
-        }
+				$body .= JVB()->email()->card($cardContent, $error->component);
+			}
+		}
 
-        // Add dashboard link if available
-        $admin_url = admin_url('admin.php?page=jvb-error-logs');
-        $body .= "View detailed error logs in the dashboard: {$admin_url}\n\n";
+		// Critical errors section
+		if (!empty($summary['critical'])) {
+			$body .= JVB()->email()->spacer(30);
+			$body .= JVB()->email()->h2('Recent Critical Errors');
 
-        // Send the email
-        $sent = JVB()->email()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY');
+			foreach ($summary['critical'] as $error) {
+				$cardContent = '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>';
+				$cardContent .= '<p><strong>Message:</strong></p>';
+				$cardContent .= JVB()->email()->codeBlock($error->message);
 
-        // Log that summary was sent
-        if ($sent) {
-            error_log("[ErrorHandler] Daily error summary sent to {$admin_email}");
-        } else {
-            error_log("[ErrorHandler] Daily error summary was not sent.");
-        }
+				// Include context if available
+				if (!empty($error->context)) {
+					$context = json_decode($error->context, true);
+					if (is_array($context)) {
+						$cardContent .= '<p><strong>Context:</strong></p>';
+						$cardContent .= JVB()->email()->codeBlock(json_encode($context, JSON_PRETTY_PRINT));
+					}
+				}
 
-        return $sent;
-    }
+				$body .= JVB()->email()->card($cardContent, $error->component . ': ' . $error->error_type);
+			}
+		}
+
+		// Dashboard link
+		$admin_url = admin_url('admin.php?page=jvb-error-logs');
+		$body .= JVB()->email()->spacer(30);
+		$body .= JVB()->email()->button($admin_url, 'View Detailed Logs');
+
+		return JVB()->email()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY');
+	}
 
     /**
      * Get HTML version of the error summary for nicer emails
diff --git a/inc/managers/FormManager.php b/inc/managers/FormManager.php
deleted file mode 100644
index 5552e4d..0000000
--- a/inc/managers/FormManager.php
+++ /dev/null
@@ -1,479 +0,0 @@
-<?php
-namespace JVBase\managers;
-
-use JVBase\meta\MetaManager;
-use Exception;
-
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
-}
-/**
- * TODO: this is old, I think.
- * Form Manager Class
- * Mainly used for front-end forms.
- * Handles form rendering and processing using MetaManager
- */
-class FormManager
-{
-
-	protected array $forms;
-    protected array $fields = [];
-    protected string $turnstile_site_key = '';
-    protected string $turnstile_secret_key = '';
-    protected object $meta;
-    protected object $cache;
-
-
-    /**
-     * Constructor
-     */
-    public function __construct()
-    {
-        // Add form processing actions
-        add_action('admin_post_nopriv_jvb_forms', [$this, 'processForm']);
-        add_action('admin_post_jvb_forms', [$this, 'processForm']);
-
-        // Add query var for form submission
-        add_filter('query_vars', [$this, 'addQueryVars']);
-
-		$this->forms = apply_filters('jvbForms', []);
-		if (empty($this->forms)) {
-			return;
-		}
-
-		$this->formContact = apply_filters('jvbFormContact', '');
-
-        // Setup Turnstile
-        $this->turnstile_site_key = JVB_CLOUDFLARE_SITE_KEY;
-        $this->turnstile_secret_key = JVB_CLOUDFLARE_SECRET_KEY;
-        $this->meta = new MetaManager(null, 'form');
-        $this->cache = CacheManager::for('forms', WEEK_IN_SECONDS);
-    }
-
-    /**
-     * Add query vars for form submission
-     * @param array $vars
-     *
-     * @return array
-     */
-    public function addQueryVars(array $vars):array
-    {
-        $vars[] = 'jvb_submitted';
-        $vars[] = 'jvb_form_error';
-        return $vars;
-    }
-
-
-    /**
-     * @param string $type
-     *
-     * @return false|string
-     */
-    public function renderForm(string $type):string|false
-    {
-        if (!array_key_exists($type, $this->forms)) {
-            return false;
-        }
-        $submitted = get_query_var('jvb_submitted', false);
-        $error = get_query_var('jvb_form_error', false);
-
-        ob_start();
-
-        // Handle success state - return only success message
-        if ($submitted) {
-            $submission_id = sanitize_text_field($submitted);
-            $submission_data = $this->cache->get('submission_' . $submission_id);
-
-            echo '<div class="form-success">';
-            echo '<h2>'.$this->forms[$type]['success_title']??'We got it'.'!</h2>';
-            if (!empty($this->forms[$type]['success_message'])) {
-				foreach ($this->forms[$type]['success_message'] as $message) {
-					echo '<p>'.$message.'</p>';
-				}
-			}
-
-            if ($submission_data) {
-                echo '<div class="submission-summary">';
-                echo '<h3>Your submission:</h3>';
-                echo '<ul>';
-                foreach ($submission_data as $key => $value) {
-                    if (!in_array($key, ['action', 'form_id', 'form_type', 'timestamp', '_wpnonce'])) {
-                        if (is_array($value)) {
-                            $value = implode(', ', $value);
-                        }
-                        echo '<li><strong>' . esc_html(ucfirst($key)) . ':</strong> ' . esc_html($value) . '</li>';
-                    }
-                }
-                echo '</ul>';
-                echo '</div>';
-            }
-
-            echo ($this->formContact !== '') ? '<p>'.$this->formContact.'</p>' : '';
-            echo '</div>';
-
-            return ob_get_clean();
-        }
-
-        // Handle error state - show error message above form
-        if ($error) {
-            echo '<div class="form-error">';
-            echo '<h2>Whoops!</h2>';
-            echo '<p>Something went wrong there. Sorry about that.</p>';
-			echo ($this->formContact !== '') ? '<p>'.$this->formContact.'</p>' : '';
-            echo '</div>';
-        }
-
-        $id = uniqid($type);
-
-        $this->renderFormStart($type, $id);
-
-        $this->renderFields($type);
-
-        $this->renderTurnstile();
-        $this->renderFormEnd($type, $id);
-
-        return ob_get_clean();
-    }
-
-    /**
-     * @param string $type
-     * @param string $id
-     *
-     * @return void
-     */
-    protected function renderFormStart(string $type, string $id):void
-    {
-        ?>
-        <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();
-    }
-
-
-    /**
-     * @return void
-     */
-    protected function renderFields(string $type): void
-    {
-		if (!array_key_exists($type, $this->forms)) {
-			return;
-		}
-
-
-        if (empty($this->forms[$type]['fields'])) {
-            return;
-        }
-
-		if (array_key_exists('sections', $this->forms[$type])) {
-			$this->renderSections($type);
-			return;
-		} else {
-			foreach ($this->forms[$type]['fields'] as $field_name => $field_config) {
-				$this->meta->render('form', $field_name, $field_config, false, false);
-			}
-		}
-
-
-    }
-
-	protected function renderSections(string $type):void
-	{
-		echo '<div class="container">';
-		$nav = '<nav class="tabs row start" role="tablist">';
-		$i = 1;
-		foreach ($this->forms[$type]['sections'] as $slug => $section) {
-			$nav .= '<button type="button" class="tab';
-
-			$ariaActive = 'false';
-			if ($i === 1) {
-				$nav .= ' active';
-				$ariaActive = 'true';
-			}
-			$nav .= '" data-tab="'.$slug.'" role="tab" aria-selected="'.$ariaActive.'">
-				<h2>'.$section.'</h2></button>';
-			$i++;
-		}
-		echo $nav.'</nav>';
-
-		$fields = $this->forms[$type]['fields'];
-
-        $i = 0;
-        foreach ($this->forms[$type]['sections'] as $slug => $section) {
-			$class = ($i == 0) ? ' active' : '';
-			?>
-			<section id="<?= $slug ?>" class="tab-content<?=$class?>" data-tab="<?=$slug?>" role="tabpanel">
-				<?php if (!empty($section['title'])) : ?>
-					<h2><?= esc_html($section['title']); ?></h2>
-				<?php endif; ?>
-
-				<?php if (!empty($section['description'])) : ?>
-					<p class="section-description">
-						<?= wp_kses_post($section['description']); ?>
-					</p>
-				<?php endif; ?>
-
-
-				<?php
-				$sectionFields = array_filter($fields, function ($f) use ($slug) {
-					return array_key_exists('section', $f) && $f['section'] == $slug;
-				});
-				foreach ($sectionFields as $field => $config) : ?>
-					<?php
-						$this->meta->render('form', $field, $config, false, false);
-					?>
-				<?php endforeach; ?>
-			</section>
-			<?php
-			$i++;
-		}
-	}
-
-
-    /**
-     * @param string $type
-     * @param string $id
-     *
-     * @return void
-     */
-    protected function renderFormEnd(string $type, string $id):void
-    {
-        $submit = $this->forms[$type]['submit']??'Submit';
-        // Add hidden fields
-        ?>
-        <input type="hidden" name="action" value="jvb_forms">
-        <input type="hidden" name="form_id" value="<?= $id ?>">
-		<input type="hidden" name="form_type" value="<?=$type?>">
-        <input type="hidden" name="timestamp" value="<?= time() ?>">
-
-        <div class="form-actions">
-        <button type="submit"><?= $submit ?></button>
-        </div>
-        </form>
-<?php
-    }
-
-    /**
-     * Render Cloudflare Turnstile
-     * @return void
-     */
-	protected function renderTurnstile(): void
-	{
-		if (!jvbSiteUsesCloudflare()) {
-			return;
-		}
-
-		$cloudflare = JVB()->connect('cloudflare');
-		if ($cloudflare->isSetUp()) {
-			$cloudflare->renderTurnstile();
-		}
-	}
-
-    /**
-     * Process form submission
-     * @return void
-     * @throws Exception
-     */
-    public function processForm():void
-    {
-        // Verify nonce
-        if (!isset($_POST['_wpnonce'])) {
-            wp_redirect(home_url());
-            exit;
-        }
-
-        $form_id = sanitize_text_field($_POST['form_id']);
-
-        if (!wp_verify_nonce($_POST['_wpnonce'], 'jvb_form_' . $form_id)) {
-            wp_redirect(home_url());
-            exit;
-        }
-
-		$type = sanitize_text_field($_POST['form_type']);
-		if (!array_key_exists($type, $this->forms)) {
-			wp_redirect(home_url());
-		}
-        error_log('Form Post Data: '.print_r($_POST, true));
-
-        // Verify Turnstile
-        if (!$this->verifyTurnstile()) {
-            $referer = wp_get_referer() ?: home_url($path);
-            wp_redirect(add_query_arg('jvb_form_error', urlencode('Please complete the security check.'), $referer));
-            exit;
-        }
-
-        // Check rate limits
-        $ip_address = $_SERVER['REMOTE_ADDR'];
-        $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
-        $rate_check = $this->checkRateLimit($ip_address, $email);
-
-        if ($rate_check !== true) {
-            $error_message = $rate_check === 'hourly_limit' ?
-                'Too many submissions in the last hour' :
-                'Too many submissions in the last 24 hours';
-            wp_redirect(add_query_arg('jvb_form_error', urlencode($error_message), wp_get_referer()));
-            exit;
-        }
-
-        // Process form data
-        $form_data = [];
-
-        foreach ($this->forms[$type]??[] as $field_name => $field_config) {
-            // Skip fields that weren't submitted (like hidden conditional fields)
-            if (!isset($_POST[$field_name])) {
-                continue;
-            }
-
-            $value = $_POST[$field_name];
-            if (!$this->meta->validator->validate($value, $field_config)) {
-                error_log('Validation unsuccessful');
-                throw new Exception("Validation failed for {$field_name}");
-            }
-
-            $form_data[$field_name] = $this->meta->sanitizer->sanitize($value, $field_config);
-        }
-
-        // Send email
-        $email_sent = $this->sendEmail($type, $form_data);
-
-        if (!$email_sent) {
-            $referer = wp_get_referer() ?: home_url();
-            wp_redirect(add_query_arg('jvb_form_error', urlencode('Failed to send your message. Please try again later.'), $referer));
-            exit;
-        }
-
-        $this->cache->set('submission_' . $form_id, $form_data, HOUR_IN_SECONDS);
-
-        // Redirect back to form with success parameter
-        $redirect = wp_get_referer() ?: home_url($path);
-        wp_redirect(add_query_arg('jvb_submitted', $form_id, $redirect));
-        exit;
-    }
-
-    /**
-     * Send email with form data
-     */
-    /**
-     * @param string $type
-     * @param array $form_data
-     *
-     * @return bool
-     */
-    protected function sendEmail(string $type, array $form_data):bool
-    {
-        // Set up email data
-        $to = get_bloginfo('admin_email');
-        $subject = $this->forms[$type]['subject']??'New Form Entry';
-
-        // Build email body
-        $body = '<h2>Hey team!</h2>';
-        $body .= '<p><strong>Date:</strong> ' . date_i18n(get_option('date_format') . ' ' . get_option('time_format')) . '</p>';
-        $body .= '<div class="divider">';
-
-        foreach ($form_data as $field_name => $value) {
-            // Skip internal fields
-            if (in_array($field_name, ['action', 'form_id', 'form_type', 'timestamp', '_wpnonce'])) {
-                continue;
-            }
-
-            // Get field label from config
-            $label = $this->forms[$type][$field_name]['label'] ?? $field_name;
-
-            // Format value for display
-            if (is_array($value)) {
-                $value = implode(', ', $value);
-            }
-
-            $body .= '<p><strong>' . esc_html($label) . ':</strong> ' . nl2br(esc_html($value)) . '</p>';
-        }
-
-        // Add reply-to if email field exists
-        if (isset($form_data['email'])) {
-            $name = isset($form_data['name']) ? $form_data['name'] : '';
-            $headers[] = 'Reply-To: ' . $name . ' <' . $form_data['email'] . '>';
-        }
-
-        // Send email
-        return JVB()->email()->sendEmail($to, $subject, $body, $headers);
-    }
-
-    /**
-     * Verify Cloudflare Turnstile token
-     * @return bool
-     */
-    protected function verifyTurnstile():bool
-    {
-        if (empty($_POST['cf-turnstile-response'])) {
-            return false;
-        }
-
-
-		$cloudflare = JVB()->connect('cloudflare');
-		if (!$cloudflare->isSetUp()){
-			return true;
-		}
-
-		$token = $_POST['cf-turnstile-response'];
-		$ip = $_SERVER['REMOTE_ADDR'];
-		return $cloudflare->verifyTurnstile($token, $ip);
-    }
-
-    /**
-     * Check rate limits for form submissions
-     * @param string $ip_address
-     * @param string $email
-     *
-     * @return string|true
-     */
-    protected function checkRateLimit(string $ip_address, string $email):string|bool
-    {
-        // Check submissions in last hour
-        $hour_limit = 3;
-        $day_limit = 10;
-
-        $submissions = get_transient('jvb_form_submissions_' . md5($ip_address));
-        if (!$submissions) {
-            $submissions = [
-                'hour' => [],
-                'day' => [],
-                'email' => []
-            ];
-        }
-
-        // Clean old submissions
-        $now = time();
-        $submissions['hour'] = array_filter($submissions['hour'], function ($time) use ($now) {
-            return ($now - $time) < 3600; // Last hour
-        });
-        $submissions['day'] = array_filter($submissions['day'], function ($time) use ($now) {
-            return ($now - $time) < 86400; // Last 24 hours
-        });
-        $submissions['email'] = array_filter($submissions['email'], function ($data) use ($now) {
-            return ($now - $data['time']) < 86400;
-        });
-
-        // Check limits
-        if (count($submissions['hour']) >= $hour_limit) {
-            return 'hourly_limit';
-        }
-        if (count($submissions['day']) >= $day_limit) {
-            return 'daily_limit';
-        }
-
-        // Add new submission
-        $submissions['hour'][] = $now;
-        $submissions['day'][] = $now;
-
-        if (!empty($email)) {
-            $submissions['email'][] = [
-                'email' => $email,
-                'time' => $now
-            ];
-        }
-
-        // Store updated submissions
-        set_transient('jvb_form_submissions_' . md5($ip_address), $submissions, DAY_IN_SECONDS);
-
-        return true;
-    }
-}
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index f336e23..05bfe83 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -67,8 +67,14 @@
 		// Allow other features to register handlers
 		do_action('jvbLoginManagerInit', $this);
 		add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
+		add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1);
 	}
 
+	public function excludeLoginSitemap(array $ids): array
+	{
+		$ids[] = $this->getLoginPage();
+		return $ids;
+	}
 	/**************************************************************************
 	   * SETUP & CONFIGURATION
 	**************************************************************************/
diff --git a/inc/managers/MagicLinkManager.php b/inc/managers/MagicLinkManager.php
index 880fe9c..f366c96 100644
--- a/inc/managers/MagicLinkManager.php
+++ b/inc/managers/MagicLinkManager.php
@@ -507,60 +507,54 @@
 
 	protected function getLoginEmailTemplate(string $name, string $magic_url): string
 	{
-		$content = '<h2>Hey ' . esc_html($name) . '!</h2>';
-		$content .= '<p>Click the button below to sign in to your account. This link expires in 15 minutes.</p>';
-		$content .= JVB()->email()->button($magic_url, 'Sign In');
-		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
+		$content = JVB()->email()->h2('Hey ' . esc_html($name) . '!');
+		$content .= '<p>Click the button below to sign in to your account instantly - no password needed!</p>';
+		$content .= JVB()->email()->button($magic_url, 'Sign In Now');
+		$content .= '<p>Or copy and paste this link into your browser:</p>';
 		$content .= JVB()->email()->link($magic_url);
-		$content .= '<p>If you didn\'t request this, you can safely ignore this email. The link will expire in 15 minutes.</p>';
-		$content .= JVB()->email()->signature();
-
+		$content .= JVB()->email()->divider();
+		$content .= '<p>If you didn\'t request this, you can safely ignore this email. This link expires in 15 minutes.</p>';
 		return $content;
 	}
 
 	protected function getSignupEmailTemplate(string $name, string $magic_url): string
 	{
-		$content = '<h2>Welcome' . ($name ? ', ' . esc_html($name) : '') . '!</h2>';
-		$content .= '<p>You\'re almost there! Click the button below to complete your registration and access your account.</p>';
+		$content = JVB()->email()->h2('Welcome' . ($name ? ', ' . esc_html($name) : '') . '!');
+		$content .= '<p>Click the button below to complete your registration and access your account!</p>';
 		$content .= JVB()->email()->button($magic_url, 'Complete Registration');
-		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
+		$content .= '<p>Or copy and paste this link:</p>';
 		$content .= JVB()->email()->link($magic_url);
-		$content .= '<p>This link expires in 15 minutes.</p>';
-		$content .= JVB()->email()->signature();
-
+		$content .= JVB()->email()->spacer(10);
+		$content .= '<p><small>This link expires in 24 hours.</small></p>';
 		return $content;
 	}
 
 	protected function getReferralEmailTemplate(string $name, string $referrer_name, string $magic_url, string $reward_text, array $context): string
 	{
-		$content = '<h2>Hey' . ($name ? ' ' . esc_html($name) : '') . '!</h2>';
-		$content .= '<p><strong>' . esc_html($referrer_name) . '</strong> thinks you\'d love ' . get_bloginfo('name') . '!</p>';
+		$content = JVB()->email()->h2('Hey' . ($name ? ' ' . esc_html($name) : '') . '!');
+		$content .= sprintf(
+			'<p><strong>%s</strong> thinks you\'d love %s!</p>',
+			esc_html($referrer_name),
+			esc_html(get_bloginfo('name'))
+		);
 
-		if (array_key_exists('message', $context) && $context['message']!== '') {
-			$content .= wpautop($context['message']);
+		if (!empty($context['message'])) {
+			$content .= JVB()->email()->callout(nl2br(esc_html($context['message'])));
 		}
+
 		if ($reward_text) {
-			$content .= '<p>' . esc_html($reward_text) . '</p>';
+			$content .= JVB()->email()->alert(
+				'<strong>Special Welcome Offer:</strong> ' . esc_html($reward_text),
+				'success'
+			);
 		}
 
 		$content .= JVB()->email()->button($magic_url, 'Join Now');
-		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
+		$content .= '<p>Or copy and paste this link:</p>';
 		$content .= JVB()->email()->link($magic_url);
-		$content .= '<p>This link expires in 14 days.</p>';
-		$content .= JVB()->email()->signature();
+		$content .= JVB()->email()->spacer(10);
+		$content .= '<p><small>This invitation expires in 14 days.</small></p>';
 
 		return $content;
 	}
-
-	protected function getResetEmailTemplate(string $name, string $magic_url): string
-	{
-		$content = '<h2>Hey ' . esc_html($name) . '!</h2>';
-		$content .= '<p>We received a request to reset your password. Click the button below to sign in and update your password.</p>';
-		$content .= JVB()->email()->button($magic_url, 'Reset Password');
-		$content .= '<p>Or copy and paste this link into your browser of choice:</p>';
-		$content .= JVB()->email()->link($magic_url);
-		$content .= '<p>If you didn\'t request this, you can safely ignore this email. This link expires in 15 minutes.</p>';
-		$content .= JVB()->email()->signature();
-		return $content;
-	}
 }
diff --git a/inc/managers/NotificationManager.php b/inc/managers/NotificationManager.php
index 1554ad6..5656303 100644
--- a/inc/managers/NotificationManager.php
+++ b/inc/managers/NotificationManager.php
@@ -1043,46 +1043,47 @@
      *
      * @return string HTML email content
      */
-    protected function generateDigestContent(WP_User $user, string $frequency, array $notifications, array $content_updates):string
-    {
-        $content = sprintf('<p>Hey %s,</p>', $user->first_name ?: $user->display_name);
+	protected function generateDigestContent(WP_User $user, string $frequency, array $notifications, array $content_updates):string
+	{
+		$content = sprintf('<p>Hey %s,</p>', $user->first_name ?: $user->display_name);
 
-        // Intro text based on frequency
-        switch ($frequency) {
-            case 'daily':
-                $content .= '<p>Here\'s what happened in Edmonton\'s tattoo scene today:</p>';
-                break;
-            case 'weekly':
-                $content .= '<p>Here\'s what you missed in Edmonton\'s tattoo scene this week:</p>';
-                break;
-            case 'monthly':
-                $content .= sprintf('<p>Here\'s your monthly roundup of what happened in %s in Edmonton\'s tattoo scene:</p>', date('F'));
-                break;
-        }
+		// Intro text based on frequency
+		switch ($frequency) {
+			case 'daily':
+				$content .= '<p>Here\'s what happened in Edmonton\'s tattoo scene today:</p>';
+				break;
+			case 'weekly':
+				$content .= '<p>Here\'s what you missed in Edmonton\'s tattoo scene this week:</p>';
+				break;
+			case 'monthly':
+				$content .= sprintf('<p>Here\'s your monthly roundup of what happened in %s in Edmonton\'s tattoo scene:</p>', date('F'));
+				break;
+		}
 
-        // Process artist content updates - the most visually interesting part
-        $content .= $this->generateContentUpdatesSection($content_updates);
+		// Process artist content updates - the most visually interesting part
+		$content .= $this->generateContentUpdatesSection($content_updates);
 
-        // Process regular notifications
-        if (!empty($notifications)) {
-            $content .= $this->generateNotificationsSection($notifications);
-        }
+		// Process regular notifications
+		if (!empty($notifications)) {
+			$content .= $this->generateNotificationsSection($notifications);
+		}
 
-        // Add footer content
-        $content .= '<div class="divider"></div>';
-        $content .= sprintf(
-            '<p>You\'re receiving this %s digest because you follow artists on edmonton.ink. ' .
-            'You can <a href="%s" class="text-link">adjust your notification settings</a> at any time.</p>',
-            $frequency,
-            esc_url(add_query_arg([
-                'utm_source'   => 'email',
-                'utm_medium'   => 'digest',
-                'utm_campaign' => $this->campaign
-            ], site_url('/dash/settings/')))
-        );
+		// Add footer content
+		$content .= JVB()->email()->divider();
+		$settings_url = add_query_arg([
+			'utm_source'   => 'email',
+			'utm_medium'   => 'digest',
+			'utm_campaign' => $this->campaign
+		], site_url('/dash/settings/'));
 
-        return $content;
-    }
+		$content .= sprintf(
+			'<p>You\'re receiving this %s digest because you follow artists on edmonton.ink. You can %s at any time.</p>',
+			$frequency,
+			'<a href="' . esc_url($settings_url) . '">adjust your notification settings</a>'
+		);
+
+		return $content;
+	}
 
     /**
      * Generate HTML section for content updates
@@ -1234,46 +1235,39 @@
      *
      * @return string HTML content
      */
-    protected function generateNotificationsSection(array $notifications):string
-    {
-        if (empty($notifications)) {
-            return '';
-        }
+	protected function generateNotificationsSection(array $notifications):string
+	{
+		if (empty($notifications)) {
+			return '';
+		}
 
-        $content = '<h3>Other Updates</h3>';
-        $content .= '<ul style="padding-left: 20px;">';
+		$items = [];
 
-        // Group notifications by type
-        $by_type = [];
-        foreach ($notifications as $notification) {
-            if (!isset($by_type[$notification->type])) {
-                $by_type[ $notification->type ] = [];
-            }
-            $by_type[ $notification->type ][] = $notification;
-        }
+		// Group notifications by type
+		$by_type = [];
+		foreach ($notifications as $notification) {
+			if (!isset($by_type[$notification->type])) {
+				$by_type[$notification->type] = [];
+			}
+			$by_type[$notification->type][] = $notification;
+		}
 
-        // Process each type
-        foreach ($by_type as $type => $type_notifications) {
-            $config = $this->notification_types[ $type ] ?? [];
-            $icon   = $config['icon'] ?? 'info';
+		// Process each type
+		foreach ($by_type as $type => $type_notifications) {
+			foreach ($type_notifications as $notification) {
+				$message = $notification->message;
+				if (empty($message)) {
+					$message = $this->generateNotificationMessage($notification);
+				}
 
-            foreach ($type_notifications as $notification) {
-                $message = $notification->message;
-                if (empty($message)) {
-                    $message = $this->generateNotificationMessage($notification);
-                }
+				if (!empty($message)) {
+					$items[] = ['label' => '', 'value' => $message];
+				}
+			}
+		}
 
-                if (!empty($message)) {
-                    $content .= sprintf('<li>%s</li>', $message);
-                }
-            }
-        }
-
-        $content .= '</ul>';
-        $content .= '<div class="divider"></div>';
-
-        return $content;
-    }
+		return JVB()->email()->table($items, 'Other Updates');
+	}
 
     /**
      * Generate a message for a notification when none is provided
diff --git a/inc/managers/OperationQueue.php b/inc/managers/OperationQueue.php
index 1d7d67c..182ca97 100644
--- a/inc/managers/OperationQueue.php
+++ b/inc/managers/OperationQueue.php
@@ -1684,91 +1684,101 @@
      * Send daily metrics report to admin
      * @return void
      */
-    public function emailDailyMetricsReport():void
-    {
+	public function emailDailyMetricsReport():void
+	{
+		$metrics_table = $this->wpdb->prefix . $this->metricsTable;
+		$yesterday = date('Y-m-d', strtotime('-1 day'));
 
-        $metrics_table = $this->wpdb->prefix . $this->metricsTable;
-        $yesterday     = date('Y-m-d', strtotime('-1 day'));
+		$metrics = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT * FROM $metrics_table WHERE date = %s",
+			$yesterday
+		));
 
-        // Get yesterday's metrics
-        $metrics = $this->wpdb->get_results($this->wpdb->prepare(
-            "SELECT * FROM $metrics_table WHERE date = %s",
-            $yesterday
-        ));
+		if (empty($metrics)) {
+			return;
+		}
 
-        if (empty($metrics)) {
-            return; // No metrics to report
-        }
+		$admin_email = get_option('admin_email');
+		$site_name = get_bloginfo('name');
+		$subject = "[$site_name] Daily Queue Performance - " . $yesterday;
 
-        $admin_email = get_option('admin_email');
-        $site_name   = get_bloginfo('name');
+		// Calculate totals
+		$total_ops = 0;
+		$total_success = 0;
+		$total_failed = 0;
+		$total_items = 0;
 
-        $subject = "[$site_name] Daily Queue Performance Report - " . date('Y-m-d', strtotime('-1 day'));
+		foreach ($metrics as $metric) {
+			$total_ops += $metric->total_operations;
+			$total_success += $metric->successful_operations;
+			$total_failed += $metric->failed_operations;
+			$total_items += $metric->total_items_processed;
+		}
 
-        $message = "Daily Queue Performance Report for $yesterday\n\n";
+		$success_rate = round(($total_success / max(1, $total_ops)) * 100, 1);
 
-        $message       .= "SUMMARY:\n";
-        $total_ops     = 0;
-        $total_success = 0;
-        $total_failed  = 0;
-        $total_items   = 0;
+		$message = JVB()->email()->h1('Daily Queue Performance Report');
+		$message .= sprintf('<p>Report for <strong>%s</strong></p>', $yesterday);
 
-        foreach ($metrics as $metric) {
-            $total_ops     += $metric->total_operations;
-            $total_success += $metric->successful_operations;
-            $total_failed  += $metric->failed_operations;
-            $total_items   += $metric->total_items_processed;
-        }
+		// Summary stats in grid
+		$stats = [
+			JVB()->email()->stat($total_ops, 'Operations'),
+			JVB()->email()->stat($total_success, 'Successful', '✓'),
+			JVB()->email()->stat($total_failed, 'Failed', $total_failed > 0 ? '⚠' : ''),
+			JVB()->email()->stat($success_rate . '%', 'Success Rate')
+		];
+		$message .= JVB()->email()->grid($stats, 4);
 
-        $message .= "- Total Operations: $total_ops\n";
-        $message .= "- Successful: $total_success\n";
-        $message .= "- Failed: $total_failed\n";
-        $message .= "- Success Rate: " . round(($total_success / max(1, $total_ops)) * 100, 2) . "%\n";
-        $message .= "- Total Items Processed: $total_items\n\n";
+		$message .= JVB()->email()->spacer(20);
 
-        $message .= "DETAILS BY OPERATION TYPE:\n";
+		// Alert if success rate is low
+		if ($success_rate < 90) {
+			$message .= JVB()->email()->alert(
+				sprintf('Success rate of %s%% is below the 90%% threshold', $success_rate),
+				'warning'
+			);
+		}
 
-        foreach ($metrics as $metric) {
-            $message .= "• $metric->type:\n";
-            $message .= "  - Operations: $metric->total_operations\n";
-            $message .= "  - Success: $metric->successful_operations\n";
-            $message .= "  - Failed: $metric->failed_operations\n";
+		$message .= JVB()->email()->h2('Details by Operation Type');
 
-            if ($metric->average_duration) {
-                $message .= "  - Avg. Duration: " . round($metric->average_duration, 2) . " seconds\n";
-            }
+		// Details for each operation type
+		foreach ($metrics as $metric) {
+			$details = [];
+			$details[] = ['label' => 'Total', 'value' => $metric->total_operations];
+			$details[] = ['label' => 'Success', 'value' => JVB()->email()->badge($metric->successful_operations, 'success')];
+			$details[] = ['label' => 'Failed', 'value' => $metric->failed_operations > 0 ? JVB()->email()->badge($metric->failed_operations, 'error') : '0'];
 
-            $message .= "  - Items Processed: $metric->total_items_processed\n";
+			if ($metric->average_duration) {
+				$details[] = ['label' => 'Avg Duration', 'value' => round($metric->average_duration, 2) . 's'];
+			}
 
-            if ($metric->peak_queue_size) {
-                $message .= "  - Peak Queue Size: $metric->peak_queue_size\n";
-            }
+			$details[] = ['label' => 'Items Processed', 'value' => number_format($metric->total_items_processed)];
 
-            if ($metric->peak_memory_usage) {
-                $memory_mb = round($metric->peak_memory_usage / 1024 / 1024, 2);
-                $message   .= "  - Peak Memory Usage: $memory_mb MB\n";
-            }
+			if ($metric->peak_memory_usage) {
+				$memory_mb = round($metric->peak_memory_usage / 1024 / 1024, 2);
+				$details[] = ['label' => 'Peak Memory', 'value' => $memory_mb . ' MB'];
+			}
 
-            if ($metric->peak_cpu_usage) {
-                $cpu_percent = round($metric->peak_cpu_usage * 50, 2); // Assuming 2 cores
-                $message     .= "  - Peak CPU Usage: $cpu_percent%\n";
-            }
+			$message .= JVB()->email()->card(
+				JVB()->email()->table($details),
+				esc_html($metric->type)
+			);
+		}
 
-            $message .= "\n";
-        }
+		// Current queue status
+		$pending_count = $this->getCurrentQueueSize();
+		if ($pending_count > 0) {
+			$message .= JVB()->email()->spacer(20);
+			$message .= JVB()->email()->notice(
+				sprintf('<strong>Current Queue:</strong> %d operations pending', $pending_count)
+			);
+		}
 
-        // Add any outstanding queue items
-        $pending_count = $this->getCurrentQueueSize();
-        if ($pending_count > 0) {
-            $message .= "CURRENT QUEUE STATUS:\n";
-            $message .= "- $pending_count operations currently pending in the queue\n\n";
-        }
+		$message .= JVB()->email()->spacer(20);
+		$message .= JVB()->email()->button(admin_url('admin.php?page=jvb-queue'), 'View Queue Dashboard');
 
-        $message .= "This is an automated report. Please check the admin dashboard for more details.";
-
-        // Send email
-		JVB()->email()->sendEmail($admin_email, $subject, $message);
-    }
+		JVB()->email()->sendEmail($admin_email, $subject, $message, 'QUEUE REPORT');
+	}
 
     /**
      * @return int
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 2f7468a..ade87e0 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -142,11 +142,17 @@
 		];
 
 		if (Features::hasIntegration('cloudflare') && JVB()->connect('cloudflare')->isSetUp()) {
-			$requirements[] = 'cloudflare-turnstile';
+			JVB()->connect('cloudflare')->enqueueTurnstileScripts();
 		}
 		if (is_singular(BASE.'dash')) {
 			$requirements[] = 'jvb-form';
 			$requirements[] = 'jvb-view';
+
+			wp_enqueue_script('jvb-referral-admin',
+			JVB_URL.'assets/js/min/referralAdmin.min.js',
+			['jvb-referral'],
+			'1.0.0',
+			true);
 		}
 		wp_enqueue_script(
 			'jvb-referral',
@@ -653,11 +659,8 @@
 	{
 		$yesterday = date('Y-m-d', strtotime('-1 day'));
 
-		// Get new referrals from yesterday
 		$new_referrals = $this->wpdb->get_results($this->wpdb->prepare(
-			"SELECT
-            r.*,
-            u.display_name as referrer_name
+			"SELECT r.*, u.display_name as referrer_name
         FROM {$this->referrals_table} r
         JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
         WHERE DATE(r.referred_at) = %s
@@ -665,48 +668,46 @@
 			$yesterday
 		));
 
-		// Only send if there's at least 1 new referral
 		if (empty($new_referrals)) {
 			return;
 		}
 
-		// Build email content
-		$content = '<h2>Daily Referral Report</h2>';
-		$content .= '<p><strong>' . count($new_referrals) . '</strong> new referral' .
-			(count($new_referrals) !== 1 ? 's' : '') . ' yesterday (' . $yesterday . ')</p>';
+		$content = JVB()->email()->h1('Daily Referral Report');
+		$content .= JVB()->email()->stat(
+			count($new_referrals),
+			count($new_referrals) === 1 ? 'New Referral' : 'New Referrals',
+			'From ' . $yesterday
+		);
 
-		$content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
-		$content .= '<thead><tr style="background: #f5f5f5;">';
-		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referee</th>';
-		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Email</th>';
-		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Referrer</th>';
-		$content .= '<th style="padding: 10px; text-align: left; border: 1px solid #ddd;">Code</th>';
-		$content .= '</tr></thead><tbody>';
+		$content .= JVB()->email()->spacer(20);
+		$content .= JVB()->email()->h2('New Referrals');
 
+		// Build list of referrals
 		foreach ($new_referrals as $ref) {
-			$content .= '<tr>';
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($ref->referee_name));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($ref->referee_email));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($ref->referrer_name));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($ref->referral_code));
-			$content .= '</tr>';
+			$cardContent = sprintf(
+				'<p><strong>%s</strong> (%s)</p>',
+				esc_html($ref->referee_name),
+				esc_html($ref->referee_email)
+			);
+			$cardContent .= sprintf(
+				'<p style="font-size:13px;color:%s;">Referred by: %s | Code: %s</p>',
+				JVB()->email()->colours['dark-200'],
+				esc_html($ref->referrer_name),
+				JVB()->email()->badge($ref->referral_code, 'info')
+			);
+
+			$content .= JVB()->email()->card($cardContent);
 		}
 
-		$content .= '</tbody></table>';
-
-		// Get admin email
 		$to = get_option('admin_email');
-		$subject = sprintf('[%s] %d New Referral%s',
+		$subject = sprintf(
+			'[%s] %d New Referral%s',
 			get_bloginfo('name'),
 			count($new_referrals),
-			count($new_referrals) !== 1 ? 's' : '');
+			count($new_referrals) !== 1 ? 's' : ''
+		);
 
-
-		JVB()->email()->sendEmail($to, $subject, $content);
+		JVB()->email()->sendEmail($to, $subject, $content, 'DAILY REPORT');
 	}
 
 	/**
@@ -717,19 +718,50 @@
 		$top_referrers = $this->getTopReferrers(10, 'week');
 		$total_referrals = $this->wpdb->get_var(
 			"SELECT COUNT(*) FROM {$this->referrals_table}
-             WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)"
+         WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)"
 		);
 
 		if ($total_referrals == 0) {
 			return;
 		}
 
+		$content = JVB()->email()->h1('Weekly Referral Summary');
+		$content .= JVB()->email()->stat(
+			$total_referrals,
+			'Total Referrals',
+			'This week'
+		);
+
+		$content .= JVB()->email()->spacer(30);
+		$content .= JVB()->email()->h2('Top 10 Referrers');
+
+		// Leaderboard style
+		$rank = 1;
+		foreach ($top_referrers as $referrer) {
+			$rankBadge = $rank <= 3
+				? JVB()->email()->badge('#' . $rank, $rank === 1 ? 'success' : 'info')
+				: '<span style="font-weight:600;color:' . JVB()->email()->colours['dark-200'] . ';">#' . $rank . '</span>';
+
+			$cardContent = sprintf(
+				'<p>%s <strong>%s</strong></p>',
+				$rankBadge,
+				esc_html($referrer->user_name)
+			);
+
+			$stats = [
+				JVB()->email()->stat($referrer->referral_count, 'Total Referrals'),
+				JVB()->email()->stat($referrer->treated_count, 'Treated')
+			];
+			$cardContent .= JVB()->email()->grid($stats, 2);
+
+			$content .= JVB()->email()->card($cardContent);
+			$rank++;
+		}
+
 		$to = get_option('admin_email');
 		$subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y');
 
-		$message = $this->generateWeeklyReportEmail($top_referrers, $total_referrals);
-
-		JVB()->email()->sendEmail($to, $subject, $message);
+		JVB()->email()->sendEmail($to, $subject, $content, 'WEEKLY SUMMARY');
 	}
 
 	/**
@@ -760,52 +792,6 @@
 	}
 
 	/**
-	 * Generate HTML email for daily report
-	 *
-	 * @param array $referrals
-	 * @param string $period
-	 * @return string
-	 */
-	protected function generateReportEmail(array $referrals, string $period): string
-	{
-		$count = count($referrals);
-
-		$content = sprintf('<p>You have <strong>%d new referral%s</strong> today.</p>',
-			$count,
-			$count !== 1 ? 's' : ''
-		);
-
-		$content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
-		$content .= '<thead><tr style="background: #f5f5f5; text-align: left;">';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Referred By</th>';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">New User</th>';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Email</th>';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Status</th>';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Time</th>';
-		$content .= '</tr></thead><tbody>';
-
-		foreach ($referrals as $referral) {
-			$content .= '<tr>';
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($referral->referrer_name ?? 'Unknown'));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($referral->referee_name));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($referral->referee_email));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html(ucfirst($referral->status)));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html(date('g:i A', strtotime($referral->referred_at))));
-			$content .= '</tr>';
-		}
-
-		$content .= '</tbody></table>';
-		$content .= '<p><small>See attached CSV for full details.</small></p>';
-
-		return jvbGetEmailTemplate($content, 'Daily Referral Report');
-	}
-
-	/**
 	 * Generate HTML email for weekly report
 	 *
 	 * @param array $top_referrers
@@ -820,31 +806,22 @@
 			$total_referrals !== 1 ? 's' : ''
 		);
 
-		$content .= '<h3>Top 10 Referrers This Week</h3>';
-		$content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
-		$content .= '<thead><tr style="background: #f5f5f5; text-align: left;">';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Rank</th>';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">User</th>';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Total Referrals</th>';
-		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Treated</th>';
-		$content .= '</tr></thead><tbody>';
-
+		$referrers = [];
 		$rank = 1;
 		foreach ($top_referrers as $referrer) {
-			$content .= '<tr>';
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>', $rank++);
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
-				esc_html($referrer->user_name));
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>',
-				$referrer->referral_count);
-			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>',
-				$referrer->treated_count);
-			$content .= '</tr>';
+			$referrers[] = [
+				'label' => '#' . $rank++ . ' - ' . esc_html($referrer->user_name),
+				'value' => sprintf(
+					'<strong>Total Referrals:</strong> %d | <strong>Treated:</strong> %d',
+					$referrer->referral_count,
+					$referrer->treated_count
+				)
+			];
 		}
 
-		$content .= '</tbody></table>';
+		$content .= JVB()->email()->table($referrers, 'Top 10 Referrers This Week');
 
-		return jvbGetEmailTemplate($content, 'Weekly Referral Summary');
+		return $content;
 	}
 
 	/**
@@ -1102,6 +1079,7 @@
 				'autocomplete'=>'off',
 				'data-referrer' => $referrer_name
 			]).'
+				'.$turnstile.'
 				<button type="button" class="button-secondary check-code-btn">
 					'.jvbIcon('check-circle', ['size' => 16]).' Verify Code
 				</button>
@@ -1113,7 +1091,6 @@
 				<p class="helper-text">
 					We\'ll send you a link to complete your registration.
 				</p>
-				'.$turnstile.'
 			</form>
 			<div class="success-content" hidden>
 				<h3>Check Your Email!</h3>
@@ -1232,8 +1209,9 @@
 			<h4>Your Referral Link</h4>
 			<div class="copy-group row btw nowrap">
 				<code id="referral-link" class="copy-target"><?= esc_url($share_url) ?></code>
-				<button type="button" class="copy-btn" data-target="referral-link" aria-label="Copy referral link">
-					<?php echo jvbIcon('copy', ['size' => 16]); ?>
+				<button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link">
+					<?= jvbIcon('copy'); ?>
+					<?= jvbIcon('check-circle'); ?>
 				</button>
 			</div>
 			<p class="hint">Quickest and easiest: autofills your code.</p>
@@ -1242,8 +1220,9 @@
 			<h4>Your Code</h4>
 			<div class="copy-group row btw nowrap">
 				<code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code>
-				<button type="button" class="copy-btn" data-target="referral-code" aria-label="Copy referral code">
-					<?php echo jvbIcon('copy', ['size' => 16]); ?>
+				<button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code">
+					<?= jvbIcon('copy'); ?>
+					<?= jvbIcon('check-circle'); ?>
 				</button>
 			</div>
 			<p class="hint">Manually copy and paste the code</p>
@@ -2476,20 +2455,20 @@
 
 		<!-- Referral Code Card -->
 		<div class="card">
-			<h3>Share Code</h3>
-			<div class="row btw nowrap">
-				<code class="code"><?= esc_html($referral_code) ?></code>
-				<button class="button copy-btn" data-code="<?= esc_attr($referral_code) ?>">
-					Copy Code
-				</button>
-			</div>
 			<h3>Share Link</h3>
 			<div class="row btw nowrap">
-				<code class="share-link">
-					<?= home_url('/?ref=' . $referral_code) ?>
-				</code>
-				<button class="button copy-btn" data-code="<?= home_url('/?ref=' . $referral_code) ?>">
-					Copy Link
+				<code id="referral-link" class="copy-target"><?= home_url('/?ref=' . $referral_code) ?></code>
+				<button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link">
+					<?= jvbIcon('copy'); ?>
+					<?= jvbIcon('check-circle'); ?>
+				</button>
+			</div>
+			<h3>Share Code</h3>
+			<div class="row btw nowrap">
+				<code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code>
+				<button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code">
+					<?= jvbIcon('copy'); ?>
+					<?= jvbIcon('check-circle'); ?>
 				</button>
 			</div>
 		</div>
@@ -2812,7 +2791,7 @@
 		$referrer_first_name = $referrer ? strtok($referrer->display_name, ' ') : 'Your friend';
 
 		// Get reward text
-		$reward_text = $this->getRewardText(false); // Just "20% off" or "$25 off"
+		$reward_text = $this->getRewardText(); // Just "20% off" or "$25 off"
 
 		$booking_url = apply_filters('jvb_referral_booking_url', home_url('/contact'));
 		$estimate_url = apply_filters('jvb_referral_estimate_url', home_url('/estimate'));
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
index 529dd76..538a5e8 100644
--- a/inc/managers/SEO/BreadcrumbManager.php
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -45,14 +45,30 @@
 			return [];
 		}
 
-		$key = get_queried_object_id() ?: 'home';
-		$crumbs = $this->cache->get($key);
+		switch (true) {
+			case is_singular():
+				$key = get_queried_object_id();
+				break;
+			case is_post_type_archive():
+				$obj = get_queried_object();
+				$key = $obj->name;
+				break;
+			case is_tax():
+				$obj = get_queried_object();
+				$key = $obj->taxonomy;
+				break;
+			default:
+				$key = 'home';
+				break;
+		}
 
+		$crumbs = $this->cache->get($key);
 		if ($crumbs !== false) {
 			return $crumbs;
 		}
 
 		$crumbs = $this->buildCrumbs();
+		$crumbs = apply_filters('jvbBreadcrumbs',$crumbs);
 		$this->cache->set($key, $crumbs);
 
 		return $crumbs;
@@ -73,7 +89,6 @@
 		];
 
 		$obj = get_queried_object();
-
 		if (is_tax()) {
 			$crumbs = $this->addTaxonomyCrumbs($crumbs, $obj);
 		} elseif (is_singular()) {
@@ -166,12 +181,12 @@
 	 */
 	private function addArchiveCrumbs(array $crumbs, object $obj): array
 	{
-		$type = is_singular() ? $obj->post_type : $obj -> name;
+		$type = is_singular() ? $obj->post_type : $obj->name;
 		$name = jvbNoBase($type);
 		if (array_key_exists($name, JVB_CONTENT)) {
 			$crumbs[] = [
 				'name' => JVB_CONTENT[$name]['breadcrumb'] ?? JVB_CONTENT[$name]['plural'],
-				'url'  => get_post_type_archive_link($type),
+				'url'  => get_post_type_archive_link($type)
 			];
 		}
 
@@ -241,6 +256,7 @@
 		$out .= '<ol itemscope itemtype="https://schema.org/BreadcrumbList">';
 
 		$position = 1;
+		$total = count($crumbs);
 		foreach ($crumbs as $crumb) {
 			$label = '<span itemprop="name">' . strtolower($crumb['name']) . '</span>';
 
@@ -253,8 +269,7 @@
 
 			// Add link if URL exists and not current page
 			if ($crumb['url'] !== false) {
-				$isCurrent = isset($crumb['id']) && $crumb['id'] === get_queried_object_id();
-				if (!$isCurrent) {
+				if ($total !== $position) {
 					$aOpen = '<a itemprop="item" href="' . esc_url($crumb['url']) . '" title="' . esc_attr($crumb['name']) . '">';
 					$aClose = '</a>';
 				}
diff --git a/inc/managers/SEO/SchemaOutputManager.php b/inc/managers/SEO/SchemaOutputManager.php
index 051eeb7..140716b 100644
--- a/inc/managers/SEO/SchemaOutputManager.php
+++ b/inc/managers/SEO/SchemaOutputManager.php
@@ -50,6 +50,69 @@
 
 		// Output our schema
 		add_action('wp_head', [$this, 'outputSchema'], 1);
+		add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeHiddenSingles'], 10, 1);
+	}
+
+	/**
+	 * Exclude posts from sitemap based on hide_single and is_timeline flags
+	 *
+	 * @param array $ids Array of post IDs to exclude
+	 * @return array Modified array with hidden posts added
+	 */
+	public function excludeHiddenSingles(array $ids): array
+	{
+		$hiddenTypes = [];
+		$timelineTypes = [];
+
+		// Find post types with hide_single or is_timeline flags
+		foreach (JVB_CONTENT as $slug => $config) {
+			$postType = BASE . $slug;
+
+			if (!empty($config['hide_single'])) {
+				$hiddenTypes[] = $postType;
+			}
+
+			if (!empty($config['is_timeline'])) {
+				$timelineTypes[] = $postType;
+			}
+		}
+
+		$hiddenIds = [];
+
+		// Get all posts from hide_single types
+		if (!empty($hiddenTypes)) {
+			$hiddenIds = $this->cache->remember(
+				'hidden_single_posts',
+				function() use ($hiddenTypes) {
+					return get_posts([
+						'post_type' => $hiddenTypes,
+						'posts_per_page' => -1,
+						'fields' => 'ids',
+						'post_status' => 'publish',
+					]);
+				}
+			);
+		}
+
+		// Get child posts from timeline types
+		if (!empty($timelineTypes)) {
+			$timelineChildIds = $this->cache->remember(
+				'timeline_child_posts',
+				function() use ($timelineTypes) {
+					return get_posts([
+						'post_type' => $timelineTypes,
+						'posts_per_page' => -1,
+						'fields' => 'ids',
+						'post_status' => 'publish',
+						'post_parent__not_in' => [0], // Only get posts with a parent
+					]);
+				}
+			);
+
+			$hiddenIds = array_merge($hiddenIds, $timelineChildIds);
+		}
+
+		return array_merge($ids, $hiddenIds);
 	}
 
 	/**
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index 78f3a19..e86780a 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -2,7 +2,7 @@
 add_action('init', 'jvbRegisterScripts', 5);
 
 function jvbRegisterScripts() {
-	$version = '1.0.9';
+	$version = '1.0.99';
 	$strategy = [
 		'strategy'	=> 'defer',
 		'in_footer'	=> true
@@ -150,7 +150,7 @@
 	wp_register_script(
 		'jvb-integrations',
 		JVB_URL.'assets/js/min/integrations.min.js',
-		[],
+		['jvb-auth'],
 		$version,
 		$strategy
 	);
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index c29c326..fa20385 100644
--- a/inc/managers/_setup.php
+++ b/inc/managers/_setup.php
@@ -64,7 +64,9 @@
 //require(JVB_DIR . '/inc/managers/SchemaManager.php');
 //require(JVB_DIR . '/inc/managers/SEOMetaManager.php');
 require(JVB_DIR . '/inc/managers/SEO/_setup.php');
-require(JVB_DIR . '/inc/managers/DirectoryManager.php');
+if (Features::forSite()->has('is_directory')) {
+	require(JVB_DIR . '/inc/managers/DirectoryManager.php');
+}
 require(JVB_DIR . '/inc/managers/ImageGenerator.php');
 require(JVB_DIR . '/inc/managers/AdminPages.php');
 require(JVB_DIR . '/inc/managers/RoleManager.php');
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index bedbdd1..5695d64 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -6,6 +6,7 @@
 use JVBase\meta\MetaManager;
 use JVBase\managers\CloudflareTurnstile;
 use JVBase\blocks\FormBlock;
+use JVBase\utility\Features;
 use WP_REST_Request;
 use WP_REST_Response;
 use WP_Error;
@@ -32,15 +33,13 @@
 		$this->action = 'form-';
 		$this->cache = CacheManager::for('forms', HOUR_IN_SECONDS);
 
-		// Initialize Cloudflare Turnstile if available
-		$this->turnstile = class_exists('JVBase\managers\CloudflareTurnstile') && jvbSiteUsesCloudflare()
-			? new CloudflareTurnstile()
-			: null;
 
 		// Add query vars
 		add_filter('query_vars', [$this, 'addQueryVars']);
 	}
 
+
+
 	/**
 	 * Register REST routes
 	 */
@@ -51,12 +50,12 @@
 			[
 				'methods' => 'POST',
 				'callback' => [$this, 'submitForm'],
-				'permission_callback' => '__return_true', // Public endpoint
-				[
-					'methods' => 'GET',
-					'callback' => [$this, 'getForms'],
-					'permission_callback' => [$this, 'checkPermission']
-				]
+				'permission_callback' => [$this, 'checkRateLimit'], // Public endpoint, rate limited
+			],
+			[
+				'methods' => 'GET',
+				'callback' => [$this, 'getForms'],
+				'permission_callback' => [$this, 'checkPermission']
 			]
 		]);
 
@@ -95,10 +94,10 @@
 			$form_type = $request->get_param('form_type');
 			$form_id = $request->get_param('form_id');
 			$form_data = $request->get_params();
+			$files = $request->get_file_params();
 
-			error_log('Form submission: '.print_r($request->get_params(), true));
 			// Process the submission
-			$result = $this->handleFormSubmission($form_type, $form_id, $form_data);
+			$result = $this->handleFormSubmission($form_type, $form_id, $form_data, $files);
 
 			if (is_wp_error($result)) {
 				return new WP_REST_Response([
@@ -123,36 +122,65 @@
 		}
 	}
 
+	protected function verifyTurnstile(string $token): bool
+	{
+		if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
+			return true;
+		}
+
+		if (empty($token)) {
+			return false;
+		}
+
+		return JVB()->connect('cloudflare')->verifyTurnstile($token);
+	}
+
 	/**
 	 * Handle the actual form submission logic
 	 */
-	protected function handleFormSubmission(string $form_type, string $form_id, array $form_data): array|WP_Error
+	protected function handleFormSubmission(string $form_type, string $form_id, array $form_data, array $files): array|WP_Error
 	{
 		// Get form configuration
 		$form_config = FormBlock::getForm($form_type);
 
-		error_log('Config: '.print_r($form_config, true));
+		if (!$form_config) {
+			return new WP_Error('invalid_form', 'Form configuration not found.');
+		}
 
+		// Verify Turnstile
+		$turnstile_token = $form_data['cf-turnstile-response'] ?? '';
 
-		// Verify Turnstile if enabled
-		//TODO: Reenable
-//		if (jvbSiteUsesCloudflare() && $this->turnstile) {
-//			error_log('Verifying turnstile...');
-//			$turnstile_token = $form_data['cf-turnstile-response'] ?? '';
-//			if (!$this->turnstile->verifyTurnstile($turnstile_token)) {
-//				return new WP_Error('turnstile_failed', 'Security verification failed. Please try again.');
-//			}
-//		}
+		if (!$this->verifyTurnstile($turnstile_token)) {
+			return new WP_Error('turnstile_failed', 'Security verification failed. Please try again.');
+		}
+
+		// Validate file uploads if present
+		if (!empty($files)) {
+			try {
+				$files = $this->validateFileUploads($files, $form_config);
+			} catch (\Exception $e) {
+				return new WP_Error('file_validation_failed', 'File validation error: ' . $e->getMessage());
+			}
+		}
 
 		// Validate and sanitize form data
-		$processed_data = $this->validateAndSanitizeData($form_config, $form_data);
-		error_log('Processed data: '.print_r($processed_data, true));
+		try {
+			$processed_data = $this->validateAndSanitizeData($form_config, $form_data);
+		} catch (\Exception $e) {
+			return new WP_Error('validation_failed', 'Data validation error: ' . $e->getMessage());
+		}
+
 		if (array_key_exists('success', $processed_data) && $processed_data['success'] === false) {
 			return $processed_data;
 		}
 
-		// Send email notification
-		$email_sent = $this->sendEmailNotification($form_type, $form_config, $processed_data);
+		// Send email notification with attachments
+		try {
+			$email_sent = $this->sendEmailNotification($form_type, $form_config, $processed_data, $files);
+		} catch (\Exception $e) {
+			;
+			return new WP_Error('email_failed', 'Email error: ' . $e->getMessage());
+		}
 
 		if (!$email_sent) {
 			return new WP_Error('email_failed', 'Failed to send your message. Please try again later.');
@@ -239,136 +267,314 @@
 	/**
 	 * Send email notification
 	 */
-	protected function sendEmailNotification(string $form_type, array $form_config, array $form_data): bool
+	/**
+	 * Send email notification
+	 */
+	protected function sendEmailNotification(string $form_type, array $form_config, array $form_data, array $files = []): bool
 	{
 		$admin_email = apply_filters('jvb_form_email_to', $form_config['email_to'], $form_type, $form_data);
 		$subject = apply_filters('jvb_form_email_subject', $form_config['email_subject'], $form_type, $form_data);
 
 		// Get submitter details
 		$submitter_email = $form_data['email'] ?? null;
-		$submitter_name = $form_data['name'] ?? '';
+		$submitter_name = '';
 
-		// Generate unique message ID for threading
-		$form_id = $form_data['form_id'] ?? uniqid();
-		$timestamp = current_time('timestamp');
-		$message_id = sprintf('<%s-%s-%s@%s>',
-			$form_type,
-			$form_id,
-			$timestamp,
-			parse_url(home_url(), PHP_URL_HOST)
-		);
-
-		// Build unified email body
-		$body = '<div style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; line-height: 1.6; color: #333;">';
-		$body .= '<h2 style="color: #2563eb; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem;">New Form Submission: ' . esc_html($form_config['title']) . '</h2>';
-		$body .= '<p style="color: #6b7280;"><strong>Submitted:</strong> ' . current_time('F j, Y \a\t g:i A') . '</p>';
-
-		// Add note about email thread if submitter email exists
-		if ($submitter_email) {
-			$body .= '<div style="background: #f0f9ff; border-left: 4px solid #3b82f6; padding: 1rem; margin: 1rem 0; border-radius: 0 8px 8px 0;">';
-			$body .= '<p style="margin: 0; color: #1e40af;"><strong>💬 Email Thread Created</strong></p>';
-			$body .= '<p style="margin: 0.5rem 0 0 0; color: #1e40af; font-size: 0.9em;">This email includes both the admin and the person who submitted the form. Any replies will be shared between all parties, creating a conversation thread.</p>';
-			$body .= '</div>';
+		if (array_key_exists('first_name', $form_data)) {
+			$submitter_name = $form_data['first_name'];
+			$submitter_name .= array_key_exists('last_name', $form_data) ? ' '.$form_data['last_name'] : '';
+		} elseif (array_key_exists('name', $form_data)) {
+			$submitter_name = $form_data['name'];
 		}
 
-		$body .= '<div style="margin: 2rem 0;">';
-
-		// Add submission details in a nice format
-		foreach ($form_config['fields'] as $field_name => $config) {
-			$label = $config['summaryTitle'] ?? $config['label'];
-			//Submitted Value
-			$value = $form_data[$field_name];
-			if (str_contains($label, '%s')) {
-				switch ($config['type']) {
-					case 'true_false':
-						$replace = ($value === '1') ? $config['isTrue'] : $config['isFalse'];
-						break;
-				}
-				$label = sprintf(
-					$label,
-					$replace
-				);
-			}
-
-			$body .= '<div style="margin-bottom: 1rem; padding: 0.75rem; background: #f9fafb; border-radius: 6px;">';
-			$body .= '<strong style="color: #374151; display: block; margin-bottom: 0.25rem;">' . esc_html($label) . ':</strong>';
-			$body .= '<span style="color: #6b7280;">' . nl2br(esc_html($value)) . '</span>';
-			$body .= '</div>';
-		}
-
-		$body .= '</div>';
-
-		// Add footer
-		$body .= '<div style="border-top: 1px solid #e5e7eb; padding-top: 1rem; margin-top: 2rem;">';
-		$body .= '<p style="color: #9ca3af; font-size: 0.8em; margin: 0;"><em>';
-		$body .= 'This message was sent through the ' . esc_html($form_config['title']) . ' on ' . get_bloginfo('name');
+		// Email headers
+		$headers = [];
 
 		if ($submitter_email) {
-			$body .= '<br>To continue this conversation, simply reply to this email.';
-		}
-
-		$body .= '</em></p>';
-		$body .= '</div>';
-		$body .= '</div>';
-
-		// Prepare email headers for unified thread with enhanced threading
-		$headers = [
-			'Content-Type: text/html; charset=UTF-8',
-		];
-
-		// Set up recipients and reply-to based on whether submitter email exists
-		if ($submitter_email) {
-			// Primary recipient: admin
 			$to = $admin_email;
-
-			// Add submitter as CC so they both see the email
-			$cc_name = $submitter_name ? $submitter_name : 'Form Submitter';
+			$cc_name = $submitter_name ?: 'Submitter';
 			$headers[] = 'CC: ' . $cc_name . ' <' . $submitter_email . '>';
 
-			// Set reply-to to include both emails for unified thread
 			$site_name = get_bloginfo('name');
 			$admin_name = get_bloginfo('name') . ' Team';
-
 			$headers[] = 'Reply-To: ' . $admin_name . ' <' . $admin_email . '>, ' . $cc_name . ' <' . $submitter_email . '>';
-
-			// Set from address to be more professional
-			$headers[] = 'From: ' . $site_name . ' Forms <' . $admin_email . '>';
-
+			$headers[] = 'From: ' . $site_name . ' <' . $admin_email . '>';
 		} else {
-			// No submitter email, just send to admin
 			$to = $admin_email;
 			$site_name = get_bloginfo('name');
-			$headers[] = 'From: ' . $site_name . ' Forms <' . $admin_email . '>';
+			$headers[] = 'From: ' . $site_name . ' <' . $admin_email . '>';
 			$headers[] = 'Reply-To: ' . $admin_email;
 		}
 
-		// Allow filtering of email data
-		$email_data = apply_filters('jvb_form_unified_email_data', [
-			'to' => $to,
-			'subject' => $subject,
-			'body' => $body,
-			'headers' => $headers,
-			'submitter_included' => !empty($submitter_email),
-			'submitter_email' => $submitter_email,
-			'message_id' => $message_id
-		], $form_type, $form_data);
+		// Build email body
+		$body = JVB()->email()->h2($form_config['title']);
+		$body .= '<p style="font-size:13px;color:' . JVB()->email()->colours['dark-200'] . ';">
+        Submitted: ' . current_time('F j, Y \a\t g:i A') . '
+    </p>';
 
-		// Send the unified email
-		$email_sent = JVB()->email()->sendEmail($email_data['to'], $email_data['subject'], $email_data['body'], implode(';',$email_data['headers']));
-
-		// Log the email sending for debugging
-		if ($email_sent) {
-			error_log("Form email sent successfully. Recipients: {$to}" .
-				($submitter_email ? " (CC: {$submitter_email})" : "") .
-				" | Message-ID: {$message_id}");
-		} else {
-			error_log("Failed to send form email to: {$to}" .
-				($submitter_email ? " (CC: {$submitter_email})" : ""));
+		// Add thread notice if CC'd
+		if ($submitter_email) {
+			$body .= JVB()->email()->notice(
+				'<strong>Email Thread Created</strong><br>Reply to this email to continue the conversation with ' .
+				esc_html($submitter_name ?: 'the submitter') . '.'
+			);
 		}
 
+		// Build form data array for table
+		$form = [];
+		foreach ($form_config['fields'] as $field_name => $config) {
+			// Skip file upload fields
+			if (in_array($config['type'], ['upload'])) {
+				continue;
+			}
+
+			$value = $form_data[$field_name] ?? '';
+
+			// Skip empty fields
+			if (empty($value)) {
+				continue;
+			}
+
+			$form[] = [
+				'label' => $config['summaryTitle'] ?? $config['label'],
+				'value' => $this->formatFieldValueForEmail($field_name, $value, $config)
+			];
+		}
+
+		$body .= JVB()->email()->spacer(20);
+		$body .= JVB()->email()->table($form);
+
+		// Show attachment info
+		$attachments = $this->processFileAttachments($files);
+		if (!empty($attachments)) {
+			$body .= JVB()->email()->spacer(10);
+			$body .= JVB()->email()->alert(
+				sprintf('%d file(s) attached to this email', count($attachments)),
+				'info'
+			);
+		}
+
+		// Send the email
+		$email_sent = JVB()->email()->sendEmail(
+			$to,
+			$subject,
+			$body,
+			'',
+			$headers,
+			$attachments
+		);
+
 		return $email_sent;
 	}
 
+	/**
+	 * Format field value for email display
+	 */
+	protected function formatFieldValueForEmail(string $field_name, mixed $value, array $field_config): string
+	{
+		if (empty($value)) {
+			return '';
+		}
+
+		$type = $field_config['type'] ?? 'text';
+		$type = $field_config['subType']?:$type;
+
+		switch ($type) {
+			case 'phone':
+			case 'tel':
+				// Format phone number as xxx-xxx-xxxx
+				$cleaned = preg_replace('/\D/', '', $value);
+				if (strlen($cleaned) === 10) {
+					return substr($cleaned, 0, 3) . '-' . substr($cleaned, 3, 3) . '-' . substr($cleaned, 6);
+				}
+				return $value;
+
+			case 'checkbox':
+			case 'set':
+				// Convert array of values to comma-separated labels
+				if (!is_array($value)) {
+					$value = explode(',', $value);
+				}
+				$labels = [];
+				foreach ($value as $val) {
+					$val = trim($val);
+					if (isset($field_config['options'][$val])) {
+						$labels[] = $field_config['options'][$val];
+					} else {
+						$labels[] = $val; // Fallback to value if no label found
+					}
+				}
+				return implode(', ', $labels);
+
+			case 'radio':
+			case 'select':
+				// Return label instead of value
+				if (isset($field_config['options'][$value])) {
+					return $field_config['options'][$value];
+				}
+				return $value;
+
+			case 'textarea':
+			case 'wysiwyg':
+				// Keep line breaks for email display
+				return $value; // nl2br is already applied in the email body
+
+			case 'true_false':
+				$true_text = $field_config['true_text'] ?? 'Yes';
+				$false_text = $field_config['false_text'] ?? 'No';
+				return ($value === '1' || $value === 1 || $value === true) ? $true_text : $false_text;
+
+			case 'email':
+				return $value; // Will be clickable in email client
+
+			case 'url':
+				return $value; // Will be clickable in email client
+
+			default:
+				return is_array($value) ? implode(', ', $value) : $value;
+		}
+	}
+
+	/**
+	 * Process uploaded files for email attachments
+	 * Files are in PHP's tmp directory and will be automatically cleaned up after request
+	 */
+	protected function processFileAttachments(array $files): array
+	{
+		$attachments = [];
+
+		if (empty($files)) {
+			return $attachments;
+		}
+
+		foreach ($files as $field_name => $field_files) {
+			// Skip metadata fields
+			if (str_ends_with($field_name, '_meta')) {
+				continue;
+			}
+
+			// Handle single file
+			if (isset($field_files['tmp_name']) && !is_array($field_files['tmp_name'])) {
+				if (!empty($field_files['tmp_name']) && is_uploaded_file($field_files['tmp_name'])) {
+					$attachments[] = $field_files['tmp_name'];
+				}
+				continue;
+			}
+
+			// Handle multiple files (field[])
+			if (is_array($field_files) && isset($field_files['tmp_name'])) {
+				foreach ($field_files['tmp_name'] as $index => $tmp_name) {
+					if (!empty($tmp_name) && is_uploaded_file($tmp_name)) {
+						$attachments[] = $tmp_name;
+					}
+				}
+			}
+		}
+
+		return $attachments;
+	}
+
+	protected function validateFileUploads(array $files, array $form_config): array
+	{
+		$validated_files = [];
+		$max_file_size = 10 * 1024 * 1024; // 10MB default
+		$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
+
+		foreach ($files as $field_name => $field_files) {
+			// Skip metadata fields
+			if (str_ends_with($field_name, '_meta')) {
+				continue;
+			}
+
+			// Get field config for this upload field
+			$field_config = $form_config['fields'][$field_name] ?? null;
+
+			// Override defaults if field config specifies them
+			if ($field_config) {
+				$max_file_size = $field_config['max_file_size'] ?? $max_file_size;
+				$allowed_types = $field_config['allowed_types'] ?? $allowed_types;
+			}
+
+			// Handle multiple files uploaded to same field (field[])
+			if (isset($field_files['tmp_name']) && is_array($field_files['tmp_name'])) {
+				$valid_indices = [];
+
+				foreach ($field_files['tmp_name'] as $index => $tmp_name) {
+					$file = [
+						'tmp_name' => $tmp_name,
+						'size' => $field_files['size'][$index] ?? 0,
+						'type' => $field_files['type'][$index] ?? '',
+						'name' => $field_files['name'][$index] ?? '',
+						'error' => $field_files['error'][$index] ?? UPLOAD_ERR_OK
+					];
+
+					// Validate this file
+					if ($this->isValidFile($file, $max_file_size, $allowed_types)) {
+						$valid_indices[] = $index;
+					}
+				}
+
+				// Keep only valid files in the original structure
+				if (!empty($valid_indices)) {
+					$validated_files[$field_name] = [
+						'tmp_name' => [],
+						'size' => [],
+						'type' => [],
+						'name' => [],
+						'error' => []
+					];
+
+					foreach ($valid_indices as $index) {
+						$validated_files[$field_name]['tmp_name'][] = $field_files['tmp_name'][$index];
+						$validated_files[$field_name]['size'][] = $field_files['size'][$index];
+						$validated_files[$field_name]['type'][] = $field_files['type'][$index];
+						$validated_files[$field_name]['name'][] = $field_files['name'][$index];
+						$validated_files[$field_name]['error'][] = $field_files['error'][$index];
+					}
+				}
+			}
+			// Handle single file
+			elseif (isset($field_files['tmp_name']) && !is_array($field_files['tmp_name'])) {
+				if ($this->isValidFile($field_files, $max_file_size, $allowed_types)) {
+					$validated_files[$field_name] = $field_files;
+				}
+			}
+		}
+
+		return $validated_files;
+	}
+
+	/**
+	 * Validate a single file
+	 */
+	protected function isValidFile(array $file, int $max_file_size, array $allowed_types): bool
+	{
+		// Check for upload errors
+		if ($file['error'] !== UPLOAD_ERR_OK) {
+			error_log("File upload error: " . $file['error'] . " for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		// Check file size
+		if ($file['size'] > $max_file_size) {
+			error_log("File too large: " . $file['size'] . " bytes (max: $max_file_size) for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		// Check file type
+		if (!in_array($file['type'], $allowed_types)) {
+			error_log("Invalid file type: " . $file['type'] . " for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		// Security check - verify it's actually uploaded
+		if (!is_uploaded_file($file['tmp_name'])) {
+			error_log("Security check failed: file not uploaded via POST for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		return true;
+	}
+
 
 	/**
 	 * Record submission for rate limiting
diff --git a/inc/rest/routes/LoginRoutes.php b/inc/rest/routes/LoginRoutes.php
index 8c663c9..234f231 100644
--- a/inc/rest/routes/LoginRoutes.php
+++ b/inc/rest/routes/LoginRoutes.php
@@ -152,11 +152,11 @@
 		// Attempt login
 		$user = wp_signon([
 			'user_login'	=> $username,
-			'user_email' 	=> $username,
 			'user_password' => $password,
 			'remember' => $remember
 		], false);
 
+
 		if (is_wp_error($user)) {
 			// Track failed attempt
 			$this->trackFailedLogin($username);
@@ -167,7 +167,6 @@
 				401
 			) : false;
 		}
-
 		// Clear failed attempts on success
 		$this->clearFailedAttempts($username);
 
@@ -175,6 +174,8 @@
 		wp_set_current_user($user->ID);
 		wp_set_auth_cookie($user->ID, $remember);
 
+
+
 		// Store session fingerprint for hijacking protection
 		if ($request) {
 			$this->storeSessionFingerprint($user->ID, $request);
@@ -267,13 +268,12 @@
 	 */
 	protected function getSessionId(int $user_id): string
 	{
-		// Use WordPress session tokens
-		$sessions = WP_Session_Tokens::get_instance($user_id);
 		$token = wp_get_session_token(); // Current session token
 
 		if (!$token) {
-			// Fallback to user-specific hash that changes on password reset
-			return md5($user_id . get_user_meta($user_id, 'session_tokens', true));
+			// Fallback to a hash based on user ID and current timestamp
+			// This will be replaced once the session token is available
+			return md5($user_id . time());
 		}
 
 		return md5($token);
@@ -532,7 +532,7 @@
 			update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
 		}
 
-		$redirect = $this->getRedirect($user, $request->get_param('redirect_to'), 'register');
+		$redirect = $this->getRedirect($user, $request->get_param('redirect_to')??get_home_url(null,'/dash'), 'register');
 
 		// Handle token handlers
 		do_action('jvbUserRegistered', $user_id, $email, $data);
diff --git a/inc/rest/routes/QueueRoutes.php b/inc/rest/routes/QueueRoutes.php
index 0346eee..8ea77cd 100644
--- a/inc/rest/routes/QueueRoutes.php
+++ b/inc/rest/routes/QueueRoutes.php
@@ -193,7 +193,8 @@
 			'progress_count' => intval($operation['progress_count'] ?? 0),
 			'count' => intval($operation['count'] ?? 1),
 			'retries' => intval($operation['retries'] ?? 0),
-			'data' => json_decode($operation['request_data'] ?? '{}', true)
+			'data' => json_decode($operation['request_data'] ?? '{}', true),
+			'result'	=> json_decode($operation['result']??'{}', true)
 		];
 
 		// Convert timestamps to ISO 8601 format with proper timezone
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
index aec6c55..fd0627b 100644
--- a/inc/rest/routes/ReferralRoutes.php
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -640,8 +640,21 @@
 		if ($result['success']) {
 			$this->cache->clear();
 		}
-		error_log('Result: '.print_r($result, true));
-		return $result;
+
+		// Build summary message
+		$textResult = 'Sent invitations. ';
+		$textResult .= 'Success: ' . count($result['result']['success']) . '. ';
+		$textResult .= 'Failed: ' . count($result['result']['failed']) . '.';
+
+		return [
+			'success'   => true,
+			'message'   => $textResult,
+			'details'   => [
+				'successful' => $result['result']['success'],
+				'failed'     => $result['result']['failed'],
+				'total'      => count($data['invitations'])
+			]
+		];
 	}
 
 	/**
@@ -649,20 +662,31 @@
 	 */
 	public function handleClientUpload(WP_REST_Request $request): WP_REST_Response
 	{
-		$files = $request->get_file_params();
-
-		if (!isset($files['file'])) {
+		// Access files from $_FILES directly for REST API uploads
+		if (empty($_FILES['file'])) {
 			return new WP_REST_Response([
 				'success' => false,
 				'message' => 'No file uploaded'
 			], 400);
 		}
 
-		$file = $files['file'];
+		$file = $_FILES['file'];
+
+		// Check for upload errors
+		if ($file['error'] !== UPLOAD_ERR_OK) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File upload error: ' . $file['error']
+			], 400);
+		}
 
 		// Validate file type
 		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
-		if (!in_array($file['type'], $allowed_types)) {
+		$finfo = finfo_open(FILEINFO_MIME_TYPE);
+		$mime_type = finfo_file($finfo, $file['tmp_name']);
+		finfo_close($finfo);
+
+		if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
 			return new WP_REST_Response([
 				'success' => false,
 				'message' => 'File must be a CSV'
@@ -679,7 +703,7 @@
 
 		// Import using JaneAppClientImporter
 		$importer = new JaneAppClientImporter();
-		$default_role = get_option(BASE . 'client_import_role', JVB_USER);
+		$default_role = get_option(BASE . 'referral_role', JVB_USER);
 
 		$options = [
 			'update_existing' => true,
@@ -716,7 +740,7 @@
 		return new WP_REST_Response([
 			'success' => true,
 			'message' => $message,
-			'items' => $result,
+			'stats' => $result,
 			'skipped_details' => $details
 		]);
 	}
@@ -726,20 +750,31 @@
 	 */
 	public function handleSalesUpload(WP_REST_Request $request): WP_REST_Response
 	{
-		$files = $request->get_file_params();
-
-		if (!isset($files['file'])) {
+		// Access files from $_FILES directly for REST API uploads
+		if (empty($_FILES['file'])) {
 			return new WP_REST_Response([
 				'success' => false,
 				'message' => 'No file uploaded'
 			], 400);
 		}
 
-		$file = $files['file'];
+		$file = $_FILES['file'];
+
+		// Check for upload errors
+		if ($file['error'] !== UPLOAD_ERR_OK) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'File upload error: ' . $file['error']
+			], 400);
+		}
 
 		// Validate file type
 		$allowed_types = ['text/csv', 'application/vnd.ms-excel', 'text/plain'];
-		if (!in_array($file['type'], $allowed_types)) {
+		$finfo = finfo_open(FILEINFO_MIME_TYPE);
+		$mime_type = finfo_file($finfo, $file['tmp_name']);
+		finfo_close($finfo);
+
+		if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
 			return new WP_REST_Response([
 				'success' => false,
 				'message' => 'File must be a CSV'
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index 8390fc3..10b30cb 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -238,7 +238,7 @@
 	/**
 	 * Add a view type (grid, table, list, timeline)
 	 */
-	public function addViews(?array $views):self
+	public function addViews(?array $views = null):self
 	{
 		if (!$views) {
 			$views = $this->defaultViews;
@@ -612,7 +612,8 @@
 			return;
 		}
 		?>
-		<div class="all-filters col start" data-ignore>
+		<details class="all-filters col start" data-ignore>
+			<summary>Filters</summary>
 			<?php
 
 			$this->renderSearch();
@@ -624,7 +625,7 @@
 				$this->renderColumnSelector();
 			}
 			?>
-		</div>
+		</details>
 		<?php
 	}
 
@@ -1036,6 +1037,7 @@
 			$temp = array_filter($this->fields, function ($field) {
 				return in_array($field, $this->timelineUniqueFields);
 			}, ARRAY_FILTER_USE_KEY);
+			jvbDump($temp);
 			$form = new MetaForm();
 			echo '<template class="timelineItem">';
 			$form->renderImagePreview(null,['fields' => $temp]);
@@ -1111,7 +1113,7 @@
 		}
 		ob_start();
 		?>
-		<div class="item-actions">
+		<div class="item-actions row btw abs">
 			<?php
 			foreach ($this->itemActions as $action) {
 				$config = $this->defaultItemActions[$action];
@@ -1698,7 +1700,7 @@
 
 	protected function getApplicableStatuses(string $prefix) {
 		foreach ($this->statuses as $status) {
-			if ($status === 'all' || !in_array($status, $this->allowedStatuses)) {
+			if ($status === 'all' || !array_key_exists($status, $this->allowedStatuses)) {
 				continue;
 			}
 			$config = $this->allowedStatuses[$status];
@@ -1721,9 +1723,8 @@
 				   value="<?= esc_attr($status)?>"
 				   id="<?=$prefix?>set-<?= esc_attr($status) ?>"
 				<?= $disabled?>>
-			<label for="<?=$prefix?>set-<?=esc_attr($status)?>">
-				<?= jvbDashIcon($config['icon'], ['title' => $config['label']]) ?>
-				<span><?= esc_html($config['label'])?></span>
+			<label for="<?=$prefix?>set-<?=esc_attr($status)?>" title="<?=esc_html($config['label'])?>">
+				<?= jvbDashIcon($config['icon']) ?>
 			</label>
 			<?php
 		}
diff --git a/src/feed/view.js b/src/feed/view.js
index 5e3b453..710dd6c 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -681,6 +681,11 @@
 	}
 }
 
-document.addEventListener('DOMContentLoaded', function() {
-	window.feedBlock = new FeedBlock();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			window.feedBlock = new FeedBlock();
+		}
+	});
+
 });
diff --git a/src/forms/view.js b/src/forms/view.js
index e4a88ea..957a5df 100644
--- a/src/forms/view.js
+++ b/src/forms/view.js
@@ -8,7 +8,7 @@
 		this.controller = new window.jvbForm();
 
 		document.querySelectorAll('.jvb-form-block form').forEach(form => {
-			this.controller.registerForm(form, {autosave: true});
+			this.controller.registerForm(form, {autosave: true,autoUpload: false});
 		});
 
 		this.controller.subscribe((event, data) => {
@@ -24,9 +24,39 @@
 		let formData = data.fullData;
 		let form = formConfig.element;
 
+		// Create FormData instead of JSON
+		const submitData = new FormData();
+
+		// Add all regular form fields
+		for (const [key, value] of Object.entries(formData)) {
+			// Skip the nonce field - we don't need it for REST API with __return_true
+			if (key === '_wpnonce' || key === '_wp_http_referer') {
+				continue;
+			}
+			if (Array.isArray(value)) {
+				value.forEach(v => submitData.append(`${key}[]`, v));
+			} else if (typeof value === 'object' && value !== null) {
+				// For nested objects, you might want to JSON.stringify them
+				submitData.append(key, JSON.stringify(value));
+			} else {
+				submitData.append(key, value);
+			}
+		}
+
+		// Get uploaded files from UploadManager
+		if (window.jvbUploads) {
+			const files = await window.jvbUploads.getFilesForForm(form);
+
+			// Append files with their field names
+			files.forEach(({file, fieldName, uploadId, meta}) => {
+				// Use fieldName[] for multiple files per field
+				submitData.append(`${fieldName}[]`, file);
+			});
+		}
+
+
 		let headers = {
-			'X-WP-Nonce': jvbSettings.nonce,
-			'Content-Type': 'application/json'
+			// 'X-WP-Nonce': window.auth.getNonce()
 		};
 
 		let block = form.closest('.jvb-form-block');
@@ -35,26 +65,48 @@
 		try {
 			const response = await fetch(`${jvbSettings.api}forms`, {
 				method: 'POST',
+				credentials: 'same-origin',
 				headers,
-				body: JSON.stringify(formData)
+				body: submitData // Send FormData instead of JSON
 			});
+			const result = await response.json();
 
 			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.handleFormError(form, result); // ✓ Show error to user
+				return; // Stop processing
 			}
 
 			this.controller.showFormStatus(formId, 'submitted');
 			this.controller.showSummary(formId, '.jvb-form-block');
+
+			// Clean up uploaded files from IndexedDB after successful submission
+			if (window.jvbUploads) {
+				const uploadFields = form.querySelectorAll('[data-upload-field]');
+				for (const field of uploadFields) {
+					const fieldId = window.jvbUploads.determineFieldId(field);
+					await window.jvbUploads.clearFieldFromStores(fieldId);
+				}
+			}
 		} catch (error) {
-			throw error;
+			console.error('Form submission error:', error);
+			this.controller.showFormStatus(formId, 'error');
+
+			// Show user-friendly network error - IMPROVED
+			this.controller.handleFormError(form, {
+				message: 'Network error. Please check your connection and try again.',
+				code: 'network_error'
+			});
 		} finally {
 			this.controller.store.delete(formId);
 		}
 	}
 }
 
-document.addEventListener('DOMContentLoaded', function() {
-	new FormBlock();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe(event => {
+		if (event === 'auth-loaded') {
+			new FormBlock();
+		}
+	});
 });
diff --git a/src/glossary/view.js b/src/glossary/view.js
index abe67a6..f68d59a 100644
--- a/src/glossary/view.js
+++ b/src/glossary/view.js
@@ -173,6 +173,7 @@
 	}
 }
 
+
 // Initialize when DOM is ready
 if (document.readyState === 'loading') {
 	document.addEventListener('DOMContentLoaded', () => {
diff --git a/src/gmbreviews/render.php b/src/gmbreviews/render.php
index 43d203d..ee519f8 100644
--- a/src/gmbreviews/render.php
+++ b/src/gmbreviews/render.php
@@ -57,12 +57,12 @@
 		ob_start();
 		?>
 		<div class="gmb-reviews">
-			<div class="row btw">
+			<div class="row center">
 			<?php
 			if ($showStats && !empty($average) && !empty($total)) {
 				?>
 				<p>
-					<span class="stars" aria-label="<?= $average ?> out of 5 stars">
+					<span class="stars" title="<?= $average ?> out of 5 stars">
 						<?php
 						$fullStars = floor($average);
 						$hasHalfStar = ($average - $fullStars) >= 0.5;
@@ -91,20 +91,21 @@
 			}
 			?>
 
+
+		</div>
 			<?php
 			if ($showReviewLink && !empty($reviewUrl)) {
 				?>
 				<a href="<?=esc_url($reviewUrl)?>"
-					class="button"
-				   	target="_blank"
-					rel="noopener noreferrer">
+				   class="button"
+				   target="_blank"
+				   rel="noopener noreferrer">
 					<?= jvbIcon('star', ['style' => 'fill']) ?>
 					Leave Your Review
 				</a>
 				<?php
 			}
 			?>
-		</div>
 
 		<ul>
 			<?php
@@ -145,6 +146,14 @@
 							<?php } ?>
 
 							<div class="row start wrap">
+								<?php if ($showRating && $rating > 0) { ?>
+									<div class="stars" title="<?= $rating ?> out of 5 stars">
+										<?php
+										for ($i = 1; $i <= 5; $i++) {
+											echo ($i <= $rating) ? jvbIcon('star', ['style' => 'fill']) : jvbIcon('star', ['style' => 'light']);
+										} ?>
+									</div>
+								<?php } ?>
 								<p><?= esc_html($reviewer)?></p>
 								<?php
 								// Date
@@ -155,14 +164,7 @@
 										<?= esc_html($formatted_date) ?>
 									</time>
 								<?php } ?>
-								<?php if ($showRating && $rating > 0) { ?>
-									<div class="stars" aria-label="<?= $rating ?> out of 5 stars">
-										<?php
-										for ($i = 1; $i <= 5; $i++) {
-											echo ($i <= $rating) ? jvbIcon('star', ['style' => 'fill']) : jvbIcon('star', ['style' => 'light']);
-										} ?>
-									</div>
-								<?php } ?>
+
 							</div>
 						</cite>
 					</blockquote>
diff --git a/src/gmbreviews/style.scss b/src/gmbreviews/style.scss
index bca3db1..19f3abb 100644
--- a/src/gmbreviews/style.scss
+++ b/src/gmbreviews/style.scss
@@ -1,45 +1,53 @@
 .gmb-reviews {
 	max-width: none;
-	> .row.btw {
-		max-width:var(--wide);
-		.button {
-			width: 100%;
-			height: max-content;
-		}
+	> .row.center {
+		max-width:var(--content);
+		margin: 0 auto;
+		--gap: .5rem 6rem;
+
 		p {
 			width: fit-content;
 		}
 	}
+	.button {
+		width: 66.6%;
+		margin: 0 auto 2rem;
+		display: flex;
+		height: max-content;
+	}
 	.stars {
 		display: inline-flex;
 		align-items: center;
-		justify-content: center;
+		justify-content: flex-start;
 		flex-wrap: nowrap;
 	}
 	ul {
 		list-style: none;
 		margin: 0;
 		padding: 0;
-		max-width: var(--wider);
+		max-width: var(--full);
 		li {
-			margin: 2rem 0;
-			position:relative;
-			background-color: var(--base-100);
-			padding: 1rem;
-			@media (min-width: 768px) {
-				&:nth-of-type(odd) {
-					left: -2rem;
-				}
-				&:nth-of-type(even) {
-					right: -2rem;
+			width: 100%;
+			max-width: none;
+			padding: 4rem 1rem;
+			&:nth-of-type(odd) {
+				background-color: var(--base-50);
+				blockquote {
+					--background: var(--base-50);
 				}
 			}
-
+			&:nth-of-type(even) {
+				background-color: var(--base-100);
+				blockquote {
+					--background: var(--base-100);
+				}
+			}
 		}
 	}
 	blockquote {
-		margin:0;
+		margin:0 auto;
 		padding: 0;
+		max-width: var(--content);
 		.content {
 			border-width: 4px 1px;
 			&::after {
diff --git a/webpack.jvb.js b/webpack.jvb.js
index da6ceae..7983537 100644
--- a/webpack.jvb.js
+++ b/webpack.jvb.js
@@ -36,6 +36,7 @@
 		'quill':               './assets/js/concise/quill.js',
 		'queue':               './assets/js/concise/Queue.js',
 		'referral':            './assets/js/concise/Referral.js',
+		'referralAdmin':            './assets/js/concise/ReferralAdmin.js',
 		'shopManager':         './assets/js/concise/ShopManager.js',
 		'cache':               './assets/js/concise/SimpleCache.js',
 		'schema':              './assets/js/concise/SchemaManager.js',

--
Gitblit v1.10.0