From 46d681c6b825d21b3f698d793c4e630c687d90ad Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 21 May 2026 21:41:53 +0000
Subject: [PATCH] =Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start

---
 inc/blocks/CustomBlocks.php | 3237 +++++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 2,367 insertions(+), 870 deletions(-)

diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 1822fb5..0b5830e 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -4,6 +4,9 @@
 use DateTime;
 use DOMDocument;
 use JVBase\managers\Cache;
+use JVBase\managers\LoginManager;
+use JVBase\managers\SEO\BreadcrumbManager;
+use JVBase\utility\Image;
 use WP_Block;
 use WP_Query;
 
@@ -14,11 +17,23 @@
 class CustomBlocks
 {
     protected Cache $cache;
+
+	protected static ?WP_Query $currentLoop = null;
+	protected static ?int $currentQueryId = null;
+	protected static array $counters = [];
+	protected static ?WP_Query $originalQuery = null;
+	protected array $ignore = ['align','alt','area','backgroundColor','borderColor','buttonText','buttonPosition','buttonUseIcon','categories','className','columns','contentPosition','customOverlayColor','dimRatio','displayAsDropdown','displayAuthor','displayFeaturedImage','displayPostContent','displayPostContentRadio','displayPostDate','excerptLength','featuredImageAlign','fontSize','gradient','height','iconColor','iconColorValue','iconColorValue','iconBackgroundColor','iconBackgroundColorValue','id','imageFill','isDark','isLink','isSearchFieldHidden','isStackedOnMobile','isUserOverlayColor','kind','label','largestFontSize','layout','level','mediaId','mediaLink','mediaSizeSlug','mediaType','metadata','minHeight','minHeightUnit','opacity','opensInNewTab','order','orderBy','ordered','overlayMenu','placeholder','postLayout','postsToShow','query', 'queryId','ref','rel','shouldSyncIcon','showEmpty','showHierarchy','showLabel','showLabels','showOnlyTopLevel','showPostCounts','showTagCounts','size','sizeSlug','slug','smallestFontSize','tagName','taxonomy','term','textAlign','textColor','theme','title','type','url','useFeaturedImage','width','widthUnit',];
+
+	//For custom style output for nested links, etc
+	protected static array $pendingStyles = [];
+	protected static array $pendingClass = [];
     public function __construct()
     {
         $this->cache = Cache::for('blocks', WEEK_IN_SECONDS);
 		$this->cache->connect('post')->connect('taxonomy');
-		add_filter('render_block', [$this, 'render'], 999, 3);
+		$this->cache->flush();
+		add_filter('pre_render_block', [$this, 'prerender'], 10, 3);
+		add_filter('render_block', [$this, 'render'], 10, 2);
 
         add_action('init', [$this, 'registerBlockStyles']);
     }
@@ -62,52 +77,74 @@
                 'label' => __('Callout Alt', 'jvb')
             ]
         );
+        register_block_style(
+            'core/separator',
+            [
+                'name' 	=>'logo',
+                'label' => __('With Logo', 'jvb')
+            ]
+        );
     }
-
-    public function render(string $content, array $block, WP_Block $instance)
-    {
+	protected function checkMethods(?string $content, array $block, ?WP_Block $parent = null, bool $isPrerender = false):?string
+	{
 		$blockName = $this->sanitizeBlockName($block);
-        $method = 'render_'.$blockName;
+
+		$base = ($isPrerender) ? 'prerender_' : 'render_';
+		$method = $base.$blockName;
 		$function = BASE.$method;
 
 		if (function_exists($function)) {
-			return $function($block, $content);
-//			return $this->cache->remember(
-//				get_the_ID(),
-//				function () use ($function, $block, $content) {
-//					return $function($block, $content);
-//				}
-//			);
+			return $function($block, $content, $parent);
 		} else if (method_exists($this, $method)) {
-			return $this->$method($block, $content);
-//
-//			return $this->cache->remember(
-//				get_the_ID(),
-//				function () use ($method, $block, $content) {
-//					return $this->$method($block, $content);
-//				}
-//			);
-        } else if (!empty($block['blockName'])){
-			//TESTING
-			$ignore = [
-				'core/null',
-				'core/post-title',
-				'core/list-item',
-				'core/site-title',
-				'jvb/forms'
-			];
-//			if (!in_array($block['blockName'], $ignore)) {
-//				jvbDump('No method found for '.print_r($block['blockName'], true));
-//			}
+			$content = $this->$method($block, $content, $parent);
+			return $isPrerender ? $this->maybeOutputCustomStyles().$content : $content;
+		} elseif (!empty($blockName) && JVB_TESTING) {
+			if (!in_array($block['blockName'], $this->getIgnore($isPrerender))) {
+				jvbDump('No method found for '.print_r($block['blockName'], true));
+			}
 		}
-        if ($block['blockName'] === 'jvb/feed') {
-            // Enqueue the feed block script (it will automatically load dependencies)
-            $this->localize_feedblock();
-        }
+		return $content;
+	}
+		protected function getIgnore(bool $isPrerender):array
+		{
+			//Ignore for both
+			$base = [
+				'core/null'
+			];
+			if ($isPrerender) {
+				$base = array_merge($base, [
+					'core/query-pagination',
+					'core/query-pagination-previous',
+					'core/query-pagination-next',
+					'core/query-pagination-numbers',
+					'core/query',
+					'core/calendar',
+					'core/archives',
+				]);
+			} else {
+				$base = array_merge($base, [
+
+				]);
+			}
+			return $base;
+		}
+	public function prerender(?string $content, array $block, ?WP_Block $parent = null):?string
+	{
+		$result = $this->checkMethods($content, $block, $parent, true);
+		return $result;
+	}
+
+    public function render(string $content, array $block):string
+    {
+		if ($block['blockName'] === 'jvb/feed') {
+			// Enqueue the feed block script (it will automatically load dependencies)
+			$this->localize_feedblock();
+		}
 		if ($block['blockName'] === 'jvb/forms') {
 			wp_enqueue_style('jvb-form');
 		}
-        return $content;
+
+		return $this->checkMethods($content, $block);
     }
 
     /***********************************
@@ -126,8 +163,10 @@
      */
 
 
-    public function render_core_button(array $block):string
+    public function prerender_core_button(array $block, ?string $content, ?WP_Block $parent):?string
     {
+//		jvbDump($block, 'Button');
+//		jvbDump($parent, 'Parent');
 		preg_match('/href="([^"]*)"/', $block['innerHTML'], $url);
 		preg_match('/>([^<]*)<\/a>/', $block['innerHTML'], $label);
 
@@ -136,76 +175,127 @@
 		}
 		$icon = '';
 		if (str_contains($url[1], 'google.com/maps')) {
-			$icon = 'google-logo';
+			$icon = jvbIcon('google-logo');
 		}
 		if (str_contains($url[1], 'maps.apple.com')) {
-			$icon = 'apple-logo';
+			$icon = jvbIcon('apple-logo');
 		}
 
 		if ($icon !== '') {
 			return sprintf(
 				'<li%s><a href="%s" title="Find Us On %s">%s Maps</a></li>',
-				$this->getClassesAndStyles($block['attrs']),
+				$this->getClassesAndStyles($block['attrs']??[]),
 				esc_url($url[1]),
 				esc_html($label[1]),
-				jvbIcon($icon)
+				$icon
 			);
 		}
 
 		return sprintf(
 			'<li%s><a href="%s">%s</a></li>',
-			$this->getClassesAndStyles($block['attrs']),
+			$this->getClassesAndStyles($block['attrs']??[]),
 			esc_url($url[1]),
 			esc_html($label[1])
 		);
     }
 
-    public function render_core_buttons(array $block):string
+    public function prerender_core_buttons(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        return '<ul'.$this->getClassesAndStyles($block['attrs'], ['buttons','row']).'>'.
+//		jvbDump($block, 'buttons');
+//		jvbDump($parent, 'Parent');
+        return '<ul'.$this->getClassesAndStyles($block['attrs']??[], ['buttons','row']).'>'.
                $this->innerBlocks($block).'</ul>';
     }
 
-    public function render_core_column(array $block):string
+    public function prerender_core_column(array $block, ?string $content, ?WP_Block $parent):?string
     {
+//		jvbDump($block, 'column');
+//		jvbDump($parent, 'Parent');
         $styles = (array_key_exists('attrs', $block) &&
                    array_key_exists('width', $block['attrs'])) ?
             ['flex-basis:'.$block['attrs']['width']]
             : [];
         return '<div'.
-               $this->getClassesAndStyles($block['attrs'], ['col'], $styles).'>'.
+               $this->getClassesAndStyles($block['attrs']??[], ['col'], $styles).'>'.
                $this->innerBlocks($block).'</div>';
     }
 
-    public function render_core_columns(array $block):string
+    public function prerender_core_columns(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        return '<section'.
-               $this->getClassesAndStyles($block['attrs'], ['columns']).'>'.
-               $this->innerBlocks($block).'</section>';
+		jvbDump($block, 'columns');
+		$attrs = $block['attrs']??[];
+		$tagName = array_key_exists('tagName', $attrs) ? $attrs['tagName'] : 'section';
+
+		$classes = ['row', 'nowrap'];
+		if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true){
+			$classes[] = 'stack-small';
+		}
+        return sprintf(
+			'<%s%s>%s</%s>',
+			$tagName,
+		   	$this->getClassesAndStyles($attrs, $classes),
+		   	$this->innerBlocks($block).'</section>',
+			$tagName
+		);
     }
     //core_comment_template
 
-    public function render_core_group(array $block):string
+    public function prerender_core_group(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        $tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
+//		jvbDump($block, 'group');
+//		jvbDump($parent, 'Parent');
+        $tag = (array_key_exists('tagName', $block['attrs']??[])) ? $block['attrs']['tagName'] : 'div';
 
-        $classes = ($tag === 'main') ?
-            '' :
-            $this->getClassesAndStyles($block['attrs'], ['group']);
-        return '<'.$tag.$classes.'>'.$this->innerBlocks($block).'</'.$tag.'>';
+
+        return sprintf(
+			'<%s%s>%s</%s>',
+			$tag,
+			$tag === 'main' ? '' : $this->getClassesAndStyles($block['attrs']??[], ['group']),
+			$this->innerBlocks($block),
+			$tag
+		);
     }
     //core_home_link
     //core_more
     //core_nextpage
+	public function prerender_core_nextpage(array $block, ?string $content, ?WP_Block $parent):?string
+	{
 
-    public function render_core_separator(array $block):string
+		return str_replace('</a>', '</a></li>',str_replace('<a', '<li><a', wp_link_pages([
+			'before' 		=> '<nav class="pagination x-btw"><ul>',
+			'after'			=> '</ul></nav>',
+			'nextpagelink'	=> __('<span>Next </span>'.jvbIcon('caret-circle-right'), 'jvb'),
+			'previouspagelink'	=> __(jvbIcon('caret-circle-left').'<span> Previous</span>', 'jvb'),
+			'next_or_number'=> 'next',
+			'echo' 			=> false
+		])));
+	}
+
+    public function prerender_core_separator(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        return '<hr'.$this->getClassesAndStyles($block['attrs']).'>';
+//		jvbDump($block, 'separator');
+//		jvbDump($parent, 'Parent');
+		$attrs = $block['attrs']??[];
+		$logo = '';
+		if (array_key_exists('className', $attrs) && $attrs['className'] === 'is-style-logo'){
+			$logo = apply_filters('jvbSeparatorLogo', 'logo');
+			if (!empty($logo)) {
+				$logo = jvbIcon($logo);
+			}
+		}
+        return sprintf(
+			'<hr%s>',
+			$this->getClassesAndStyles($attrs),
+//			$logo
+		);
     }
 
-    public function render_core_spacer(array $block):string
+    public function prerender_core_spacer(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        return '<div'.$this->getClassesAndStyles($block['attrs'], ['spacer'], ['height:2rem']).
+
+//		jvbDump($block, 'spsacer');
+//		jvbDump($parent, 'Parent');
+        return '<div'.$this->getClassesAndStyles($block['attrs']??[], ['spacer'], ['height:2rem']).
                ' aria-hidden="true"></div>';
     }
     //core_table_of_contents
@@ -219,11 +309,12 @@
      * Media Blocks
      */
     //core_audio
-    public function render_core_cover(array $block):string
+    public function prerender_core_cover(array $block, ?string $content, ?WP_Block $parent):?string
     {
-
+//		jvbDump($block, 'cover');
+//		jvbDump($parent, 'Parent');
         // Extract block attributes
-        $attrs = $block['attrs'] ?? [];
+        $attrs = $block['attrs'] ?:[];
         $innerContent = $this->innerBlocks($block);
 
 		$position = 'object-position: center;';
@@ -266,15 +357,19 @@
 
     //core_file
 
-    public function render_core_gallery(array $block):string
+    public function prerender_core_gallery(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        return '<ul'.$this->getClassesAndStyles($block['attrs'], ['gallery']).'>'.
+//		jvbDump($block, 'gallery');
+//		jvbDump($parent, 'Parent');
+        return '<ul'.$this->getClassesAndStyles($block['attrs']??[], ['gallery']).'>'.
                $this->innerBlocks($block,'<li>', '</li>').
                '</ul>';
     }
 
-    public function render_core_image(array $block):string
+    public function prerender_core_image(array $block, ?string $content, ?WP_Block $parent):?string
     {
+//		jvbDump($block, 'image');
+//		jvbDump($parent, 'Parent');
         $ID = $this->imageID('', $block);
         if (!$ID) {
             return '';
@@ -287,40 +382,73 @@
                 wp_get_attachment_caption($ID) .
             '</figcaption>' :
             '<figcaption>' . $title . '</figcaption>';
-		$size = array_key_exists('sizeSlug', $block['attrs']) ? $block['attrs']['sizeSlug'] : 'large';
+		$size = array_key_exists('sizeSlug', $block['attrs']??[]) ? $block['attrs']['sizeSlug'] : 'large';
         return '<figure'.
-               $this->getClassesAndStyles($block['attrs']).'>'.
+               $this->getClassesAndStyles($block['attrs']??[]).'>'.
                $this->imageLink(true, $ID, 'tiny', $size) .
                $caption.'</figure>';
     }
 
-    public function render_core_media_text(array $block):string
+    public function prerender_core_media_text(array $block, ?string $content, ?WP_Block $parent):?string
     {
 
+//		jvbDump($block, 'media text');
+//		jvbDump($parent, 'Parent');
         $ID = $this->imageID('', $block);
+		$attrs = $block['attrs']??[];
 
-		$size = array_key_exists('mediaSizeSlug', $block['attrs']) ? $block['attrs']['mediaSizeSlug'] : 'large';
+		$size = array_key_exists('mediaSizeSlug', $attrs) ? $attrs['mediaSizeSlug'] : 'large';
         $imgLink = ($ID) ? $this->imageLink(true, $ID, 'tiny', $size) : '';
 
         $inner = $this->innerBlocks($block);
 
 
-		$classes = ['media-text', 'row'];
-		if (array_key_exists('isStackedOnMobile', $block['attrs'])) {
-			$classes[] = 'nowrap';
+		$classes = ['media-text', 'row', 'nowrap'];
+		if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true) {
+			$classes[] = 'stack-small';
 		}
-        $content = '<div'.$this->getClassesAndStyles($block['attrs'], $classes).'>';
-        $content .= (array_key_exists(
-            'mediaPosition',
-            $block['attrs']
-        ) && $block['attrs']['mediaPosition'] == 'right') ?
-            '<div>'.$inner.'</div><figure>'.$imgLink.'</figure>' :
-            '<figure>'.$imgLink.'</figure><div>'.$inner.'</div>';
-        $content .= '</div>';
-        return $content;
+
+		$inside = array_key_exists('mediaPosition', $attrs) && $attrs['mediaPosition'] === 'right'
+			? sprintf(
+				'<div>%s</div><figure>%s</figure>',
+				$inner, $imgLink
+			) : sprintf(
+				'<figure>%s</figure><div>%s</div>',
+				$imgLink, $inner
+			);
+
+        return sprintf(
+			'<div%s>%s</div>',
+			$this->getClassesAndStyles($attrs, $classes),
+			$inside
+		);
     }
     //core_video
 
+	public function prerender_core_video(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		jvbDump($block, 'video');
+//		jvbDump($parent, 'Parent');
+		$ID = $this->imageID('', $block);
+		if (!$ID) {
+			return '';
+		}
+
+		jvbDump($ID);
+
+		$title = (get_the_title($ID) !== '') ? '<b>'.get_the_title($ID).'</b>' : '';
+		$caption = (wp_get_attachment_caption($ID)) ?
+			'<figcaption>' .
+			$title .
+			wp_get_attachment_caption($ID) .
+			'</figcaption>' :
+			'<figcaption>' . $title . '</figcaption>';
+		$size = array_key_exists('sizeSlug', $block['attrs']??[]) ? $block['attrs']['sizeSlug'] : 'large';
+		return '<figure'.
+			$this->getClassesAndStyles($block['attrs']??[]).'>'.
+			$this->imageLink(true, $ID, 'tiny', $size) .
+			$caption.'</figure>';
+	}
 
     /**
      * Reusable blocks
@@ -330,40 +458,50 @@
     /**
      * Text Blocks
     */
-    //render_core_code
-    //render_core_details
-    //render_core_footnotes
-    //render_core_classic
-    public function render_core_heading(array $block):string
+    //prerender_core_code
+    //prerender_core_details
+    //prerender_core_footnotes
+    //prerender_core_classic
+    public function prerender_core_heading(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        $level = (array_key_exists('level', $block['attrs'])) ? $block['attrs']['level'] : '2';
+        $level = (array_key_exists('level', $block['attrs']??[])) ? $block['attrs']['level'] : '2';
 		$content = $this->inside($block);
         $id = sanitize_title(wp_strip_all_tags($this->stripTagContents('small', $content)));
-        return '<h'.$level.' id="'.$id.'"'.$this->getClassesAndStyles($block['attrs']).'>'.
+        return '<h'.$level.' id="'.$id.'"'.$this->getClassesAndStyles($block['attrs']??[]).'>'.
                $content.
                '</h'.$level.'>';
     }
 
-	public function render_core_list(array $block):string
+	public function prerender_core_list(array $block, ?string $content, ?WP_Block $parent):?string
 	{
-		$tag = (array_key_exists('ordered', $block['attrs'])) ? 'ol' : 'ul';
-		return '<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.$this->innerBlocks($block).'</'.$tag.'>';
+//		jvbDump($block, 'list');
+//		jvbDump($parent, 'Parent');
+		$tag = (array_key_exists('ordered', $block['attrs']??[])) ? 'ol' : 'ul';
+		$output = '<'.$tag.$this->getClassesAndStyles($block['attrs']??[]).'>'.$this->innerBlocks($block).'</'.$tag.'>';
+		return $output;
 	}
 
-//	public function render_core_list_item(array $block):string
+//	public function prerender_core_list_item(array $block):string
 //	{
 //		return '<li'.$this->getClassesAndStyles($block['attrs']).'>'.$this->inside($block).'</li>';
 //	}
-    //render_core_missing
+    //prerender_core_missing
 
-    public function render_core_paragraph(array $block):string
+    public function prerender_core_paragraph(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        return '<p'.$this->getClassesAndStyles($block['attrs']).'>'.
-               $this->inside($block, 'p').
-               '</p>';
+//		jvbDump($block, 'paragraph');
+//		jvbDump($parent, 'Parent');
+		$inside = $this->inside($block);
+        return empty($inside) ? '' : sprintf(
+		'<p%s>%s</p>',
+			$this->getClassesAndStyles($block['attrs']??[]),
+		   $inside
+		);
     }
-	public function render_core_quote(array $block): string
+	public function prerender_core_quote(array $block, ?string $content, ?WP_Block $parent): ?string
 	{
+//		jvbDump($block, 'quote');
+//		jvbDump($parent, 'Parent');
 		$innerHTML = $block['innerHTML'];
 
 		// Extract cite content first
@@ -371,20 +509,22 @@
 		$citeHtml = ($cite === '') ? '' : '<cite>—&emsp;'.$cite.'</cite>';
 
 		// Get the blockquote content
-		$content = $this->inside($block, 'blockquote');
+		$content = $this->innerBlocks($block);
 
 		// Remove the cite element from content if it exists
 		if ($cite !== '') {
 			$content = $this->stripTagContents('cite', $content);
 		}
 
-		return '<blockquote'.$this->getClassesAndStyles($block['attrs']).'>
+		return '<blockquote'.$this->getClassesAndStyles($block['attrs']??[]).'>
         <div class="content">'.$content.'</div>'.
 			$citeHtml.
 			'</blockquote>';
 	}
-	public function render_core_pullquote(array $block): string
+	public function prerender_core_pullquote(array $block, ?string $content, ?WP_Block $parent):?string
 	{
+//		jvbDump($block, 'pullquote');
+//		jvbDump($parent, 'Parent');
 		$innerHTML = $block['innerHTML'];
 
 		// Extract cite content first
@@ -398,58 +538,106 @@
 		if ($cite !== '') {
 			$content = $this->stripTagContents('cite', $content);
 		}
-		$content = apply_filters('the_content', $content);
+		$content = jvb_filter_content( $content);
 
-		return '<blockquote'.$this->getClassesAndStyles($block['attrs'], ['pull']).'>'.
+		return '<blockquote'.$this->getClassesAndStyles($block['attrs']??[], ['pull']).'>'.
         	$content.
 			$citeHtml.
 			'</blockquote>';
 	}
-    //render_core_table
-    //render_core_verse
+    //prerender_core_table
+    //prerender_core_verse
 
     /**
      * Theme Blocks
      */
     //core_avatar
     //core_loginout
+	public function prerender_core_loginout(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		$action = is_user_logged_in() ? 'logout' : 'login';
+		$attrs = $block['attrs'];
+		$redirect = '';
+		if (array_key_exists('redirectToCurrent', $attrs) && $attrs['redirectToCurrent']) {
+			global $wp;
+			$redirect = get_home_url(null, $wp->request);
+		}
+
+		if (array_key_exists('displayLoginAsForm', $attrs) && $attrs['displayLoginAsForm']) {
+			LoginManager::getInstance()->setAction($action);
+			return LoginManager::getInstance()->renderLoginForm($action, $redirect, '<h2>Login</h2>');
+
+		}
+
+		return sprintf(
+			'<a href="%s"%s>%s</a>',
+			wp_login_url($redirect),
+			$this->getClassesAndStyles($attrs),
+			$action === 'login' ? jvbIcon('sign-in').'<span>Log in</span>' : jvbIcon('sign-out').'<span>Logout</span>'
+		);
+	}
     //core_pattern
 
-    public function render_core_site_logo(array $block, string $content):string
+    public function prerender_core_site_logo(array $block, ?string $content, ?WP_Block $parent = null):?string
     {
+//		jvbDump($block, 'site logo');
+//		jvbDump($parent, 'Parent');
+		$attrs = $block['attrs']??[];
         $open = $close = '';
 
-        if (!is_home() && !is_front_page()) {
-            $open = '<a href="'.get_home_url().'" rel="home">';
+        if ((!is_home() && !is_front_page()) && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) {
+            $open = '<a href="'.get_home_url().'" rel="home" class="logo">';
             $close = '</a>';
         }
         $img = get_theme_mod('custom_logo');
-        $img = $this->image($img, 'tiny', 'thumbnail');
-        $img = str_replace('<img', '<img'.$this->getClassesAndStyles($block['attrs']), $img);
+        $img = sprintf(
+			'<figure%s>%s</figure>',
+			$this->getClassesAndStyles($attrs, ['logo']),
+			$this->image($img, 'tiny', 'thumbnail')
+		);
         return $open.$img.$close;
     }
-    //core_site_title_tagline
+	public function prerender_core_site_tagline(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		$tagline = get_bloginfo('description');
 
-    public function render_core_site_title(array $block, string $content):string
+		return empty($tagline) ? '' : sprintf(
+			'<p%s>%s</p>',
+			$this->getClassesAndStyles($block['attrs']??[], ['tagline']),
+			$tagline
+		);
+	}
+
+    public function prerender_core_site_title(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        $tag = (array_key_exists('level', $block['attrs'])) ? $block['attrs']['level'] : 1;
+//		jvbDump($block, 'site title');
+//		jvbDump($parent, 'Parent');
+		$attrs = $block['attrs']??[];
+        $tag = (array_key_exists('level', $attrs)) ? $attrs['level'] : 1;
         $tag = ($tag == 0) ? 'p' : 'h'.$tag;
 
         $open = $close = '';
-        if (!is_front_page()) {
-            $open = '<a href="' . get_home_url() . '" rel="home">';
+        if (!is_front_page() && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) {
+            $open = sprintf(
+				'<a href="%s" rel="home">',
+				get_home_url()
+			);
             $close = '</a>';
         }
         $class = ($tag === 'p') ?
-            $this->getClassesAndStyles($block['attrs'], ['title']) :
-            $this->getClassesAndStyles($block['attrs']);
+            $this->getClassesAndStyles($block['attrs']??[], ['title']) :
+            $this->getClassesAndStyles($block['attrs']??[]);
 
 
-        return '<'.$tag.$class.'>'.
-               $open.
-               get_bloginfo('name').
-               $close.
-               '</'.$tag.'>';
+        return sprintf(
+			'<%s%s>%s%s%s</%s>',
+			$tag,
+			$class,
+			$open,
+			get_bloginfo('name'),
+			$close,
+			$tag
+		);
     }
 
     /**
@@ -472,93 +660,139 @@
     /**
      * Theme Navigation Blocks
      */
-    public function render_core_navigation(array $block, string $content):string
+    public function prerender_core_navigation(array $block, ?string $content, ?WP_Block $parent):?string
     {
-        $ID = (array_key_exists('ref', $block['attrs'])) ? $block['attrs']['ref'] : false;
+//		jvbDump($block, 'navigation');
+//		jvbDump($parent, 'Parent');
+//		jvbDump($block, 'navigation');
+        $ID = (array_key_exists('ref', $block['attrs']??[])) ? $block['attrs']['ref'] : false;
 
         if (empty($block['innerBlocks']) && $ID && get_post($ID)) {
             $block['innerBlocks'] = parse_blocks(get_post($ID)->post_content);
         }
+		$attrs = $block['attrs']??[];
 
-        $toggle = (array_key_exists('overlayMenu', $block['attrs'])
-                   && $block['attrs']['overlayMenu'] == 'never') ?
-            '':
-            '<button class="toggle main"
-            data-action="toggle-menu"
-            aria-label="Open Menu"
-            aria-controls="navigation-' .$ID. '"
-            aria-expanded="false">'.
-            jvbIcon('list', ['title'=>'Toggle Menu']).
-            jvbIcon('x', ['title'=>'Toggle Menu']).
-            '</button>';
-        $class = ($toggle === '') ?
-            $this->getClassesAndStyles($block['attrs'], ['mobile']) :
-            $this->getClassesAndStyles($block['attrs']);
-        $helpmenu = (get_the_title($ID) === 'Main') ?
-            '<nav><ul>'.jvbNotificationMenu().jvbHelpMenu().'</ul></nav>' :
-            '';
+		$toggle = '';
+		$classes = [];
+		if (!array_key_exists('overlayMenu', $attrs) || $attrs['overlayMenu'] !== 'never') {
+			$toggle = sprintf(
+				'<button class="toggle main"
+				data-action="toggle-menu"
+				aria-label="Open Menu"
+				aria-controls="navigation-%d"
+				aria-expanded="false">%s%s</button>',
+				$ID,
+				jvbIcon('list'),
+				jvbIcon('x')
+			);
+			$classes[] = 'mobile';
+			if (array_key_exists('overlayMenu', $attrs) && $attrs['overlayMenu'] === 'always') {
+				$classes[] = 'always';
+			}
+		}
+		if (!array_key_exists('layout', $attrs)) {
+			$classes[] = 'left';
+			$classes[] = 'row';
+		}
+		$class = $this->getClassesAndStyles($attrs, $classes);
+
+		$helpmenu = '';
+		$title = get_the_title($ID);
+		$isMain = false;
+		if ($title === 'Main') {
+			$isMain = true;
+			$helpmenu = sprintf(
+				'<nav><ul>%s%s</ul></nav>',
+				jvbNotificationMenu(),
+				jvbHelpMenu()
+			);
+		}
+
 
 		//Allows to add custom items to a menu, based on the menu name
-		$helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, get_the_title($ID));
-		$main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), get_the_title($ID), $block));
+		$helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, $title, $ID);
+		$main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), $title, $block));
 
-		$main = str_starts_with($main, '<ul') ? $main : '<ul>'.$main.'</ul>';
+		$main = str_starts_with($main, '<ul') ? $main : sprintf('<ul>%s</ul>',$main);
 
-        return '<nav'.$class.' id="navigation-' . $ID . '"aria-label="Navigation">
-            <span class="screen-reader-text">
+		$skipToContent = $isMain ? '<span class="screen-reader-text">
                 <a href="#content">Skip to Content</a>
-            </span>' .
-               $toggle .
-				$main.
-		   '</nav>'.$helpmenu;
+            </span>' : '';
+        return sprintf(
+			'<nav%s id="navigation-%d"aria-label="Navigation">
+            %s%s%s</nav>%s',
+			$class,
+			$ID,
+			$skipToContent,
+			$toggle,
+			$main,
+			$helpmenu
+		);
     }
 
-    public function render_core_navigation_link(array $block):string
+    public function prerender_core_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string
     {
+//		jvbDump($block, 'navigation link');
+//		jvbDump($parent, 'Parent');
         global $wp;
-        $url = (str_starts_with($block['attrs']['url'],'/')) ?
-            home_url($block['attrs']['url']) :
-            $block['attrs']['url'];
+		if (!array_key_exists('attrs', $block)) {
+			return '';
+		}
+		$attrs = $block['attrs']??[];
+        $url = (str_starts_with($attrs['url'],'/')) ?
+            home_url($attrs['url']) :
+            $attrs['url'];
         $current = (home_url($wp->request.'/') == $url);
-		$temp = $block['attrs'];
-		unset($temp['url']);
+		$attrs['url'] = $url;
         $classes = ($current) ?
-            $this->getClassesAndStyles($temp, ['current']):
-            $this->getClassesAndStyles($temp);
+            $this->getClassesAndStyles($attrs, ['current']):
+            $this->getClassesAndStyles($attrs);
         $aria = '';
         if ($current) {
             $aria = ' aria-current="page"';
         }
-        $linkOpen = $this->build_navigation_link($block['attrs'], $aria);
+        $linkOpen = $this->buildNavigationLink($attrs, $aria);
 
 
         return '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].'</a></li>';
     }
 
-    public function render_core_navigation_submenu(array $block):string
+    public function prerender_core_navigation_submenu(array $block, ?string $content, ?WP_Block $parent):?string
     {
+//		jvbDump($block, 'navigation submenu');
+//		jvbDump($parent, 'Parent');
         global $wp;
         $url = (str_starts_with($block['attrs']['url'],'/')) ?
             home_url($block['attrs']['url']) :
             $block['attrs']['url'];
         $current = (home_url($wp->request) == $url);
 
-		$temp = $block['attrs'];
-		unset($temp['url']);
+		$attrs = $block['attrs']??[];
+		$attrs['url'] = $url;
         $classes = ($current) ?
-            $this->getClassesAndStyles($temp, ['has-submenu', 'current']):
-            $this->getClassesAndStyles($temp, ['has-submenu']);
+            $this->getClassesAndStyles($attrs, ['has-submenu', 'current']):
+            $this->getClassesAndStyles($attrs, ['has-submenu']);
 
         $aria = '';
         if ($current) {
             $aria = ' aria-current="page"';
         }
         $id = sanitize_title($block['attrs']['label']);
-        $linkOpen = $this->build_navigation_link($block['attrs'], $aria);
-        $content = '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].
-                   '</a><button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open '.$block['attrs']['label'].' Submenu" aria-expanded="false" aria-controls="'.$id.'-submenu">'.
-                   jvbIcon('caret-down', ['title'=>'Toggle Submenu']).
-                   '</button><ul class="submenu" id='.$id.'-submenu">';
+        $linkOpen = $this->buildNavigationLink($attrs, $aria);
+        $content = sprintf(
+			'<li%s>%s%s</a>
+						<button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open %s Submenu" aria-expanded="false" aria-controls="%s-submenu">
+                   			%s
+						</button>
+						<ul class="submenu" id=%s-submenu">',
+			$classes,
+			$linkOpen,
+			$attrs['label'],
+			$attrs['label'],
+			$id,
+			jvbIcon('caret-down', ['title'=>'Toggle Submenu']),
+			$id
+		);
 
         $content .= $this->innerBlocks($block);
         $content .= '</ul></li>';
@@ -566,9 +800,8 @@
         return $content;
     }
 
-    protected function build_navigation_link(array $attrs, string $aria):string
+    protected function buildNavigationLink(array $attrs, string $aria):string
     {
-        global $wp;
         $url =(str_starts_with($attrs['url'],'/')) ?
             home_url($attrs['url']) :
             $attrs['url'];
@@ -603,263 +836,881 @@
      * Theme Query Blocks
      */
     //core_post_author
-    //core_post_author_biography
-    //core_post_author_name
-    public function render_core_post_content(array $block, string $content = ''):string
-    {
 
-        $tag = (array_key_exists('tagName', $block['attrs'])) ?
+	public function prerender_core_post_author(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		$attrs = $block['attrs'] ?? [];
+
+		$size = 96;
+		if (array_key_exists('avatarSize',$attrs) && is_int($attrs['avatarSize'])) {
+			$size = $attrs['avatarSize'];
+		}
+		$byline = $aOpen = $aClose = $avatar = $bio = '';
+		global $post;
+		$user = get_userdata($post->post_author);
+
+		if (!array_key_exists('showAvatar', $attrs) || $this->checkAttrs('showAvatar', $attrs)){
+			$avatar = get_avatar($post->post_author, $size);
+		}
+		if (!array_key_exists('showBio', $attrs) || $this->checkAttrs('showBio', $attrs)) {
+			$bio = wpautop($user->description);
+		}
+
+		$target = '';
+		if (array_key_exists('linkTarget', $attrs) && $attrs['linkTarget']=== '_blank') {
+			$target = ' target="_blank"';
+		}
+
+		if ($this->checkAttrs('isLink', $attrs)) {
+			$aOpen = sprintf(
+				'<a href="%s"%s>',
+				get_author_posts_url($post->post_author),
+				$target
+			);
+			$aClose = '</a>';
+		}
+
+		if (array_key_exists('byline', $attrs)) {
+			$byline = sprintf(
+				'<small>%s</small> — ',
+				$attrs['byline']
+			);
+		}
+
+		$name = $user->display_name;
+
+
+		return sprintf(
+			'<div%s>%s%s%s<p>%s%s%s%s</p>%s</div>',
+			$this->getClassesAndStyles($attrs, ['row','nowrap']),
+			$aOpen,
+			$avatar,
+			$aClose,
+			$aOpen,
+			$byline,
+			$name,
+			$aClose,
+			$bio
+		);
+	}
+    //core_post_author_biography
+
+	public function prerender_core_post_author_name(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		$attrs = $block['attrs']??[];
+		global $post;
+		$aOpen = $aClose = '';
+		if ($this->checkAttrs('isLink', $attrs)) {
+
+			$aOpen = sprintf(
+				'<a href="%s" rel="author">',
+				get_author_posts_url($post->post_author)
+			);
+			$aClose = '</a>';
+		}
+
+		$author = get_userdata($post->post_author);
+		return sprintf(
+			'<p%s>%s%s%s</p>',
+			$this->getClassesAndStyles($attrs, ['author']),
+			$aOpen,
+			$author->display_name,
+			$aClose
+		);
+	}
+    public function prerender_core_post_content(array $block, ?string $content, ?WP_Block $parent):?string
+    {
+//		jvbDump($block, 'post content');
+//		jvbDump($parent, 'Parent');
+
+        $tag = (array_key_exists('tagName', $block['attrs']??[])) ?
             $block['attrs']['tagName'] :
             'main';
 
         if ($content == '') {
-			global $post;
+			if(is_singular()) {
+				global $post, $page;
 
-			$block['innerBlocks'] = parse_blocks($post->post_content);
-			return $this->innerBlocks($block);
+				$pages = explode('<!--nextpage-->', $post->post_content);
+				$currentContent = $pages[max(0, $page - 1)] ?? $pages[0];
+
+
+				if ($page > 1 && !str_contains($currentContent, '<!--nextpage-->')) {
+					$currentContent = str_replace('<!-- /wp:nextpage -->','', $currentContent);
+					$currentContent .= '
+					<!-- wp:nextpage -->
+					<!--nextpage-->
+					<!-- /wp:nextpage -->';
+				}
+
+				$block['innerBlocks'] = parse_blocks($currentContent);
+				$result = $this->innerBlocks($block);
+			}else {
+				$result = '';
+			}
         } else {
-            return $this->inside($block, $tag, $content);
+            $result = $this->innerBlocks($block);
         }
+
+		return apply_filters('jvb_post_content_output', $result, $block);
     }
     //core_post_date
-	public function render_core_post_date(array $block):string
+	public function prerender_core_post_date(array $block, ?string $content, ?WP_Block $parent):?string
 	{
-		$postDate = get_the_date('c');
-		return '<time datetime="'.$postDate.'" itemprop="datePublished"'.$this->getClassesAndStyles($block['attrs']).'>'.get_the_date().'</time>';
+
+//		jvbDump($block, 'post date');
+//		return null;
+		$attrs = $block['attrs']??[];
+		$postDate = null;
+		$itemProp = 'datePublished';
+		$format = array_key_exists('format', $attrs) ? $attrs['format'] : 'M d, Y';
+		$dateFormat = null;
+		if (array_key_exists('displayType', $attrs)) {
+			switch ($attrs['displayType']) {
+				case 'displayType':
+					$postDate = get_post_modified_time('c');
+					$dateFormat = get_post_modified_time($format);
+					$itemProp = 'dateModified';
+					break;
+
+
+			}
+		}
+		$postDate = is_null($postDate) ? get_the_date('c') : $postDate;
+		$dateFormat = is_null($dateFormat) ? get_the_date($format) : $dateFormat;
+
+
+
+		$aOpen = $aClose = '';
+		if ($this->checkAttrs('isLink', $attrs) && !is_singular()) {
+			$aOpen = sprintf(
+				'<a href="%s">',
+				get_the_permalink()
+			);
+			$aClose = '</a>';
+		}
+//		jvbDump($parent, 'Parent');
+
+		return sprintf(
+			'<time datetime="%s" itemprop="%s"%s>%s%s%s</time>',
+			$postDate,
+			$itemProp,
+			$this->getClassesAndStyles($attrs),
+			$aOpen,
+			$dateFormat,
+			$aClose
+		);
 	}
     //core_post_excerpt
-    public function render_core_post_featured_image(array $block):string
+	public function prerender_core_post_excerpt(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		$attrs = $block['attrs']??[];
+
+		$moreText = array_key_exists('moreText', $attrs) ? $attrs['moreText'] : 'Read more '.jvbIcon('arrow-circle-right');
+		$showMoreOnNewLine = !array_key_exists('showMoreOnNewLine', $attrs) || $this->checkAttrs('showMoreOnNewLine', $attrs);
+//		jvbDump($block);
+//		jvbDump($showMoreOnNewLine);
+
+		$excerpt = array_filter(explode('<p>',wpautop(get_the_excerpt())));
+		$classes = $this->getClassesAndStyles($attrs);
+		$excerpt = array_map(function ($line) use ($classes) {
+			return sprintf(
+				'<p%s>%s',
+				$classes,
+				$line
+			);
+		}, $excerpt);
+
+		if (!empty($moreText)) {
+			if ($showMoreOnNewLine) {
+				$excerpt[] = sprintf(
+					'<p%s><a href="%s" class="read-more">%s</a></p>',
+					$classes,
+					get_the_permalink(),
+					$moreText
+				);
+			} else {
+				$last = array_key_last($excerpt);
+				$excerpt[$last] = str_replace('</p>', sprintf('<a href="%s" class="read-more">%s</a>',
+				get_the_permalink(),
+				$moreText), $excerpt[$last]);
+			}
+		}
+		return implode('',$excerpt);
+	}
+    public function prerender_core_post_featured_image(array $block, ?string $content, ?WP_Block $parent):?string
     {
+//		jvbDump($block, 'featured image');
+//		jvbDump($parent, 'Parent');
 		global $post;
+		$attrs = $block['attrs']??[];
 		$ID = get_post_thumbnail_id($post->ID);
-		$aOpen = $aClose = '';
-		if(!is_single($ID)) {
+		$aspectRatio = $aOpen = $aClose = '';
+		if(!is_single($post->ID) && $this->checkAttrs('isLink', $attrs)) {
 			$aOpen = '<a href="'.get_the_permalink($post->ID).'">';
 			$aClose = '</a>';
 		}
+		if (array_key_exists('aspectRatio', $attrs)) {
+			$aspectRatio = $attrs['aspectRatio'];
+		}
 
-        return $aOpen.'<figure'.$this->getClassesAndStyles($block['attrs']).'>'.
-               apply_filters('jvbCoreFeaturedImage', $this->image($ID), $post->post_type).
-               '</figure>'.$aClose;
+		$img = apply_filters('jvbCoreFeaturedImage', '', $post->post_type, $attrs);
+
+		if (empty($img)) {
+			$img = $this->image($ID);
+			$img = empty($aspectRatio) ? $img : str_replace('<img', '<img style="aspect-ratio:'.$aspectRatio.';"', $img);
+		}
+
+
+        return !empty($img) ? sprintf(
+			'<figure%s>%s%s%s</figure>',
+			$this->getClassesAndStyles($attrs),
+			$aOpen,
+			$img,
+			$aClose,
+		):'';
     }
     //core_post_navigation_link
-    //core_post_template
-    //core_post_terms
-	public function render_core_post_terms(array $block):string
+	public function prerender_core_post_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string
 	{
+		$attr = $block['attrs'];
+		$isPrevious = $attr['type']==='previous';
+		$title = array_key_exists('showTitle', $attr)&&$attr['showTitle'];
+		$linkLabel = array_key_exists('linkLabel', $attr)&&$attr['linkLabel'];
+		$label = array_key_exists('label', $attr) ? $attr['label'] : '';
+		$arrow = '';
+		if (array_key_exists('arrow', $attr)) {
+			$dir = $isPrevious ? 'left' : 'right';
+			$icon = match($attr['arrow']) {
+				'arrow'	=> 'arrow-square-',
+				'chevron' => 'caret-circle-'
+			};
+			if ($icon) {
+				$arrow = jvbIcon($icon.$dir);
+			}
+		}
+//		return $content;
+		$linkedLabel = $unlinkedLabel = '';
+		if (!empty($label)) {
+			$linkedLabel = $linkLabel ? $label : '';
+			$unlinkedLabel = $linkLabel ? '' : $label;
+		}
+		if ($title) {
+			$linkedLabel .=' %title';
+		} elseif (!empty($label)) {
+			$linkedLabel = $label;
+			$unlinkedLabel = '';
+		} else {
+			$linkedLabel = $isPrevious ? 'Previous' : 'Next';
+			$unlinkedLabel = '';
+		}
+
+		$result = $isPrevious ?
+			get_previous_post_link(
+				$arrow.$unlinkedLabel.' %link',
+				$linkedLabel
+			) :
+			get_next_post_link(
+				'%link '.$unlinkedLabel.$arrow,
+				$linkedLabel
+			);
+
+
+		return sprintf('<div%s>%s</div>',
+			$this->getClassesAndStyles($attr,['row', 'nowrap']),
+			$result
+		);
+	}
+    //core_post_template
+	public function prerender_core_post_template(array $block, ?string $content):?string
+	{
+		$inner = '';
+
+		if (!static::$currentLoop) {
+			jvbDump('No loop stored');
+			return $content;
+		}
+		if (static::$currentLoop->have_posts()) {
+			while (static::$currentLoop->have_posts()) {
+				static::$currentLoop->the_post();
+
+				$inner .= sprintf(
+					'<li>%s</li>',
+					$this->innerBlocks($block, '','',$block)
+				);
+			}
+		}
+		return sprintf(
+			'<ul%s>%s</ul>',
+			$this->getClassesAndStyles($block['attrs']??[], ['loop']),
+			$inner
+		);
+	}
+    //core_post_terms
+	public function prerender_core_post_terms(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		if (!array_key_exists('attrs', $block)) {
+			return '';
+		}
 		$terms = get_the_terms(get_the_ID(), $block['attrs']['term']);
+		$attrs = $block['attrs']??[];
 		$out = '';
 		if ($terms && !is_wp_error($terms)) {
-			$out = '<ul class="term-list">';
-				if (array_key_exists('prefix', $block['attrs'])) {
-					$out .= '<li>'.$block['attrs']['prefix'].'</li>';
+			$out = sprintf(
+				'<ul%s>',
+				$this->getClassesAndStyles($attrs, ['term-list', 'row', 'left'])
+			);
+				if (array_key_exists('prefix', $attrs)) {
+					$out .= sprintf(
+						'<li class="prefix">%s</li>',
+						$attrs['prefix']
+					);
 				}
 				foreach($terms as $term) {
-					$out .= '<li><a href="'.get_term_link($term).'" rel="tag">'.html_entity_decode($term->name).'</a></li>';
+					$out .= sprintf(
+						'<li><a href="%s" rel="tag">%s</a></li>',
+						get_term_link($term),
+						html_entity_decode($term->name)
+					);
 				}
-			if (array_key_exists('suffix', $block['attrs'])) {
-				$out .= '<li>'.$block['attrs']['suffix'].'</li>';
+			if (array_key_exists('suffix', $attrs)) {
+				$out .= sprintf(
+					'<li class="suffix">%s</li>',
+					$attrs['suffix']
+				);
 			}
 			$out .= '</ul>';
 		}
 		return $out;
 	}
     //core_post_time_to_read
-    public function render_core_post_title(array $block):string
+    public function prerender_core_post_title(array $block, ?string $content, ?WP_Block $parent):?string
     {
+//		jvbDump($parent, 'Parent');
         $open = $close = '';
-        if (array_key_exists('isLink', $block['attrs'])) {
-            $rel = (array_key_exists('rel', $block['attrs'])) ?
+		$attrs = $block['attrs']??[];
+        if ($this->checkAttrs('isLink', $attrs)) {
+            $rel = (array_key_exists('rel', $attrs)) ?
                 ' rel="'.$block['attrs']['rel'].'"' :
                 '';
-            $target = (array_key_exists('linkTarget', $block['attrs'])) ?
+            $target = (array_key_exists('linkTarget', $attrs)) ?
                 ' target="'.$block['attrs']['linkTarget'].'"' :
                 '';
             $open = '<a href="' . get_the_permalink() . '"' . $rel . $target . '>';
             $close = '</a>';
         }
-        if (is_singular(BASE.'partner')) {
-            $open .= '<small>edmonton.ink partner:</small> ';
-        }
-        $level = (array_key_exists('attrs', $block) &&
-                  array_key_exists('level', $block['attrs'])) ?
-            $block['attrs']['level'] :
-            2;
-        return '<h'.$level.$this->getClassesAndStyles($block['attrs']).'>'.
-               $open.get_the_title().$close.
-               '</h'.$level.'>';
-    }
 
-	public function render_core_query(array $block, string $content):string
+        $level = $attrs['level']??2;
+
+		$title = (!static::$currentLoop && !is_singular())
+			? get_the_title(get_queried_object_id())
+			: get_the_title();
+
+        return sprintf(
+			'<h%s%s>%s%s%s</h%s>',
+			$level,
+			$this->getClassesAndStyles($attrs),
+			$open,
+			$title,
+			$close,
+			$level
+		);
+    }
+	public function prerender_core_query(array $block, ?string $content):?string
 	{
-		$queryID = $block['attrs']['queryId'];
-		$args = [];
-		$inherit = $block['attrs']['inherit']??false;
+		global $wp_query;
+		$inherit = $block['attrs']['inherit'] ?? false;
+
 		if ($inherit) {
-			global $wp_query;
-			$loop = $wp_query;
+			static::$currentLoop = $wp_query;
 		} else {
-			foreach ($block['attrs']['query'] as $key => $value) {
-				if (empty($value)) {
-					continue;
-				}
+			static::$currentLoop = new WP_Query($this->buildQueryArgs($block['attrs']));
+		}
+		static::$currentQueryId = $block['attrs']['queryId'] ?? null;
+
+		static::$originalQuery = $wp_query;
+
+		$inside = $this->innerBlocks($block);
+
+		if (str_contains($inside, 'loop')) {
+			$classes = $this->getClassesAndStyles($block['attrs'] ?? [], ['loop']);
+			$classes = str_replace(' class="', '', $classes);
+			$classes = strtok($classes, '"');
+			$inside = str_replace('loop', $classes, $inside);
+		}
+
+
+		static::$currentQueryId = null;
+		static::$currentLoop = null;
+
+		wp_reset_postdata();
+		return $inside;
+	}
+//	public function render_core_query(array $block, string $content): string
+//	{
+//		$inside = $this->innerBlocks($block);
+//		if (str_contains($inside, 'loop')) {
+//			$classes = $this->getClassesAndStyles($block['attrs']??[], ['loop']);
+//			$classes = str_replace(' class="', '', $classes);
+//			$classes = strtok($classes, '"');
+//
+//			$inside = str_replace('loop', $classes, $inside);
+//		}
+//		return $inside;
+//	}
+		protected function buildQueryArgs(array $attrs): array
+		{
+			$queryID = $attrs['queryId'] ?? null;
+			$args = [];
+			foreach (($attrs['query'] ?? []) as $key => $value) {
+				if (empty($value)) continue;
 				switch ($key) {
-					case 'postType':
-						if ($value === BASE.'progress'){
-							$args['post_parent'] = 0;
-						}
-						$args['post_type'] = $value;
-						break;
-					case 'perPage':
-						$args['posts_per_page'] = $value;
-						break;
-					case 'orderBy':
-						$args['orderby'] = $value;
+					case 'postType':   $args['post_type']       = $value; break;
+					case 'perPage':    $args['posts_per_page']  = $value; break;
+					case 'orderBy':    $args['orderby']         = $value; break;
+					case 'sticky':
+						match ($value) {
+							'ignore'  => $args['ignore_sticky_posts'] = true,
+							'exclude' => $args['post__not_in'] = get_option('sticky_posts'),
+							'only'    => $args['post__in']     = get_option('sticky_posts'),
+							default   => null
+						};
 						break;
 					case 'taxQuery':
-						$taxQuery = [];
-						foreach ($value as $tax => $terms) {
-							$taxQuery[] = [
-								'taxonomy' 	=> $tax,
-								'terms'		=> $terms
-							];
-						}
-						if (!empty($taxQuery)) {
-							$args['tax_query'] = $taxQuery;
-							if (count($taxQuery) > 1) {
-								$args['tax_query']['relation'] = 'OR';
-							}
-						}
+						$taxQuery = array_map(fn($tax, $terms) => [
+							'taxonomy' => $tax, 'terms' => $terms
+						], array_keys($value), $value);
+						if (count($taxQuery) > 1) $taxQuery['relation'] = 'OR';
+						$args['tax_query'] = $taxQuery;
 						break;
-					case 'sticky':
-						if ($value === 'ignore') {
-							$args['ignore_sticky_posts'] = true;
-						} else if ($value === 'exclude'){
-							$args['post__not_in'] = get_option('sticky_posts');
-						} else if ($value === 'only') {
-							$args['include'] = get_option('sticky_posts');
-						}
-						break;
-					case 'search':
-						$args['s'] = $value;
-						break;
-					default:
-						$args[$key] = $value;
-						break;
-
+					case 'search':     $args['s']    = $value; break;
+					default:           $args[$key]   = $value; break;
 				}
 			}
-			//Add in any args from the query string
-			$search = 'query-'.$queryID;
+
+			// Handle pagination from query string
+			$search = 'q-' . $queryID.'-';
 			foreach ($_GET as $key => $value) {
-				if (str_contains($key, $search)) {
-					$key = str_replace($search, '', $key);
-					if ($key === 'page') {
-						$args['paged'] = (int)$value;
-					}
+				if (str_contains($key, $search) && str_replace($search, '', $key) === 'page') {
+					$args['paged'] = (int)$value;
 				}
 			}
-			$loop = new WP_Query($args);
+			return $args;
 		}
 
-		$inner = '';
-
-		foreach ($block['innerBlocks'] as $innerBlock) {
-			switch ($innerBlock['blockName']) {
-				case 'core/post-template':
-					$inner .= '<section class="item-grid">';
-					if ($loop->have_posts()) {
-						while($loop->have_posts()) {
-							$loop->the_post();
-							$postType = get_post_type();
-							$inner .= '<div class="item '.jvbNoBase($postType).'">'.$this->innerBlocks($innerBlock).'</div>';
-						}
-					}
-					$inner .= '</section>';
-					break;
-
-			}
+		protected function buildPaginationUrl(int $page): string
+		{
+			$param = 'q-' . static::$currentQueryId . '-page';
+			$url = remove_query_arg($param);
+			return $page > 1 ? add_query_arg($param, $page, $url) : $url;
 		}
 
-
-
-		$tagName = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
-		$out =  '<'.$tagName.' class="loop">'.$inner.'</'.$tagName.'>';
-		if ($inherit) {
-			wp_reset_postdata();
+		protected function getCurrentPage(): int
+		{
+			$param = 'q-' . static::$currentQueryId . '-page';
+			return isset($_GET[$param]) ? (int)$_GET[$param] : 1;
 		}
-		return $out;
-	}
+//	public function render_core_query(array $block, string $content): string
+//	{
+//
+////		$queryID = $block['attrs']['queryId'] ?? null;
+////		$inherit = $block['attrs']['inherit'] ?? false;
+////
+////		if ($inherit) {
+////			global $wp_query;
+////			$loop = $wp_query;
+////		} else {
+////			$args = [];
+////			foreach (($block['attrs']['query'] ?? []) as $key => $value) {
+////				if (empty($value)) {
+////					continue;
+////				}
+////				switch ($key) {
+////					case 'postType':
+////						if ($value === BASE.'progress'){
+////							$args['post_parent'] = 0;
+////						}
+////						$args['post_type'] = $value;
+////						break;
+////					case 'perPage':
+////						$args['posts_per_page'] = $value;
+////						break;
+////					case 'orderBy':
+////						$args['orderby'] = $value;
+////						break;
+////					case 'taxQuery':
+////						$taxQuery = [];
+////						foreach ($value as $tax => $terms) {
+////							$taxQuery[] = [
+////								'taxonomy' 	=> $tax,
+////								'terms'		=> $terms
+////							];
+////						}
+////						if (!empty($taxQuery)) {
+////							$args['tax_query'] = $taxQuery;
+////							if (count($taxQuery) > 1) {
+////								$args['tax_query']['relation'] = 'OR';
+////							}
+////						}
+////						break;
+////					case 'sticky':
+////						if ($value === 'ignore') {
+////							$args['ignore_sticky_posts'] = true;
+////						} else if ($value === 'exclude'){
+////							$args['post__not_in'] = get_option('sticky_posts');
+////						} else if ($value === 'only') {
+////							$args['include'] = get_option('sticky_posts');
+////						}
+////						break;
+////					case 'search':
+////						$args['s'] = $value;
+////						break;
+////					default:
+////						$args[$key] = $value;
+////						break;
+////
+////				}
+////			}
+////			$search = 'query-' . $queryID;
+////			foreach ($_GET as $key => $value) {
+////				if (str_contains($key, $search)) {
+////					$key = str_replace($search, '', $key);
+////					if ($key === 'page') {
+////						$args['paged'] = (int)$value;
+////					}
+////				}
+////			}
+////			$loop = new WP_Query($args);
+////		}
+////
+////		$inner = '';
+////		foreach ($block['innerBlocks'] as $innerBlock) {
+////			switch ($innerBlock['blockName']) {
+////				case 'core/post-template':
+////					$inner .= '<section class="item-grid">';
+////					if ($loop->have_posts()) {
+////						while ($loop->have_posts()) {
+////							$loop->the_post();
+////							$postType = get_post_type();
+////							$inner .= '<div class="item ' . jvbNoBase($postType) . '">' . $this->innerBlocks($innerBlock) . '</div>';
+////						}
+////					}
+////					$inner .= '</section>';
+////					break;
+////			}
+////		}
+////
+////		// Reset only after a custom query, not the main query
+////		if (!$inherit) {
+////			wp_reset_postdata();
+////		}
+//
+//		$tagName = $block['attrs']['tagName'] ?? 'div';
+////		return sprintf(
+////			'<%s class="loop">%s</%s>',
+////			$tagName,
+////			$this->innerBlocks($block),
+////			$tagName
+////		);
+//		return $this->innerBlocks($block);
+//	}
 
     //core_query_no_results
+	public function prerender_core_query_no_results(array $block, ?string $content):?string
+	{
+		if (!static::$currentLoop || static::$currentLoop->have_posts()) {
+			return '';
+		}
+
+		$inside = $this->innerBlocks($block);
+		return empty($inside) ? '' : sprintf(
+			'<div%s>%s</div>',
+			$this->getClassesAndStyles($block['attrs']??[], ['no-results']),
+			$inside
+		);
+	}
     //core_query_pagination
+	public function prerender_core_query_pagination(array $block, ?string $content):?string
+	{
+		return sprintf(
+			'<nav%s>%s</nav>',
+			$this->getClassesAndStyles($block['attrs']??[], ['pagination', 'condensed','btw']),
+			$this->innerBlocks($block)
+		);
+	}
+
     //core_query_pagination_next
-    //core_query_pagination_numbers
-    //core_query_pagination_previous
-    //core_query_title
-    //core_read_more
-    public function render_core_template_part(array $block, string $content):string
-    {
 
-		$isHeaderTemplate = (
-			(array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'header')) ||
-			(array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'header'))
-		) ? 'header' : false;
-		$isFooterTemplate = (
-			(array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'footer')) ||
-			(array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'footer'))
-		) ? 'footer' : false;
+	public function prerender_core_query_pagination_next(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		if (!static::$currentLoop) return '';
 
+		$currentPage = $this->getCurrentPage();
+		$maxPages = static::$currentLoop->max_num_pages;
 
-        if (($isHeaderTemplate || $isFooterTemplate)) {
+		if ($currentPage >= $maxPages) return '';
 
-			$tag = $isHeaderTemplate ?: $isFooterTemplate ?: 'div';
-
-            $breadcrumbs = $themeSwitch = $afterHeader = $beforeHeader = $footerText= '';
-            if ($isHeaderTemplate) {
-
-				$beforeHeader = apply_filters('jvbAboveHeader', $beforeHeader);
-				if ($beforeHeader !== '') {
-					$beforeHeader = '<aside class="pre-header">'.$beforeHeader.'</aside>';
-				}
-                $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
-                $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
-				$showThemeSwitch = (bool)apply_filters('jvb_show_theme_switch', true);
-                $themeSwitch = ($showThemeSwitch) ? '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
-                    <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'.
-					jvbIcon('sun-dim', ['title'=> 'Light Mode']).
-					jvbIcon('moon', ['title'=>'Dark Mode']).
-					'</span></label>' : '';
-                $breadcrumbs = jvbBuildBreadcrumbs();
-				$afterHeader = apply_filters('jvbBelowHeader', $afterHeader);
-
-				if ($afterHeader !== '') {
-					$afterHeader = '<aside class="sub-header">'.$afterHeader.'</aside>';
-				}
-				$footerText = '<div class="scroll-progress"><div class="bar"></div>
-</div>';
-            } elseif ($isFooterTemplate) {
-				$beforeHeader = apply_filters('jvbBeforeFooter', '');
-				if ($beforeHeader !== '') {
-					$beforeHeader = '<section class="pre-footer">'.$beforeHeader.'</section>';
-				}
-					$footerText = jvbRandomFooterText();
+		$nextLabel = $rArrow = '';
+		$type = get_post_type_object(get_post_type())->label;
+		if ($parent) {
+			$attrs = $parent->attributes;
+			if (array_key_exists('paginationArrow', $attrs)){
+				$rArrow = match($attrs['paginationArrow']) {
+					'chevron'	=> jvbIcon('caret-circle-right'),
+					default => jvbIcon('arrow-circle-right')
+				};
 			}
-//			jvbDump($beforeHeader,'beforeHeader');
-//			jvbDump('<'.$tag.$this->getClassesAndStyles($block['attrs']).'>','tag');
-//			jvbDump($themeSwitch,'themeSwitch');
-//			jvbDump($this->inside($block, $tag, $content),'inside');
-//			jvbDump($footerText,'footerText');
-//			jvbDump($afterHeader, 'afterheader');
-//			jvbDump($breadcrumbs, 'breadcrumbs');
+			if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) {
 
-            return $beforeHeader.'<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.
-                   $themeSwitch .
-				   $this->inside($block, $tag, $content) .
-                   $footerText.'</'.$tag.'>'.$afterHeader.$breadcrumbs;
-        }
+				$nextLabel = 'Next '.$type;
+			}
+		} else {
+			$rArrow = jvbIcon('caret-circle-right');
+		}
 
-        return $content;
+		$aOpen = sprintf(
+			'<a class="nav next" href="%s" title="Next %s">',
+			$this->buildPaginationUrl($currentPage + 1),
+			$type
+		);
+		$aClose = '</a>';
+		return sprintf(
+			'%s%s%s%s',
+			$aOpen,
+			$nextLabel,
+			$rArrow,
+			$aClose
+		);
+	}
+    //core_query_pagination_numbers
+	public function prerender_core_query_pagination_numbers(array $block, ?string $content):?string
+	{
+		if (!static::$currentLoop) return '';
+		$currentPage = $this->getCurrentPage();
+		$maxPages = (int)static::$currentLoop->max_num_pages;
+
+		$attrs = $block['attrs']??[];
+		if ($maxPages <= 1) return '';
+
+		$midSize = $attrs['midSize'] ?? 2;
+		$endSize = 1;
+
+
+		$items = '';
+		$gap = false;
+		for ($i = 1; $i <= $maxPages; $i++) {
+			if (($i <= min($endSize + 1, $maxPages)) ||
+				($i >= max(1, $currentPage - $midSize) && $i <= min($maxPages, $currentPage + $midSize)) ||
+				($i >= max(1, $maxPages - $endSize) && $i <= $maxPages)) {
+				$gap = true;
+				$items .= ($i === $currentPage)
+					? sprintf('<li aria-current="page" class="current">%d</li>', $i)
+					: sprintf('<li><a href="%s">%d</a></li>', $this->buildPaginationUrl($i), $i);
+			} elseif ($gap) {
+				$gap = false;
+				$items .= sprintf(
+					'<li class="dots"><span>%s</span></li>',
+					jvbIcon('dots-three')
+				);
+			}
+		}
+
+		return sprintf('<ul%s>%s</ul>',
+			$this->getClassesAndStyles($attrs, ['row', 'nowrap']),
+			$items
+		);
+	}
+    //core_query_pagination_previous
+
+	public function prerender_core_query_pagination_previous(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+
+		if (!static::$currentLoop) return '';
+
+		$currentPage = $this->getCurrentPage();
+		$maxPages = static::$currentLoop->max_num_pages;
+
+		if ($currentPage <= 1) return '';
+
+		$nextLabel = $rArrow = '';
+		$type = get_post_type_object(get_post_type())->label;
+		if ($parent) {
+			$attrs = $parent->attributes;
+			if (array_key_exists('paginationArrow', $attrs)){
+				$rArrow = match($attrs['paginationArrow']) {
+					'chevron'	=> jvbIcon('caret-circle-left'),
+					default => jvbIcon('arrow-circle-left')
+				};
+			}
+			if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) {
+
+				$nextLabel = 'Previous '.$type;
+			}
+		} else {
+			$rArrow = jvbIcon('caret-circle-left');
+		}
+
+		$aOpen = sprintf(
+			'<a class="nav prev" href="%s" title="Previous %s">',
+			$this->buildPaginationUrl($currentPage - 1),
+			$type
+		);
+		$aClose = '</a>';
+		return sprintf(
+			'%s%s%s%s',
+			$aOpen,
+			$nextLabel,
+			$rArrow,
+			$aClose
+		);
+	}
+
+	public function prerender_core_query_title(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		jvbDump($block, 'query title');
+		$attr = $block['attrs'];
+		$name = '';
+		$showPrefix = $attr['showPrefix']??false;
+		$obj = get_queried_object();
+		if (is_tax()) {
+			$name = $showPrefix ? $obj->label.':' : '';
+			$name .= $obj->name;
+		} else if (is_post_type_archive()) {
+			$name = $showPrefix ? 'Archive: ' : '';
+			$name .= $obj->label;
+		} elseif (is_search()) {
+			$name = '<small>Search results for:</small> '.get_search_query();
+		} elseif (is_singular()) {
+			$name = $obj->post_title;
+		}
+		$level = array_key_exists('level', $attr) ? 'h'.$attr['level'] : 'h1';
+
+		return sprintf(
+			'<%s id="%s">%s</%s>',
+			$level,
+			sanitize_title($name),
+			$name,
+			$level
+		);
+	}
+    //core_read_more
+    public function prerender_core_template_part(array $block, ?string $content, ?WP_Block $parent):?string
+    {
+//		jvbDump($block, 'template part');
+//		jvbDump($parent, 'Parent');
+
+
+		$slug  = $block['attrs']['slug'] ?? null;
+		$theme = $block['attrs']['theme'] ?? get_stylesheet();
+		$tag = $block['attrs']['tagName'] ?? 'div';
+		if (!$slug) {
+			return $content;
+		}
+
+		// Try to get the template part post (customized via FSE)
+		$template_part = get_block_template( "$theme//$slug", 'wp_template_part' );
+
+		if ( $template_part && ! empty( $template_part->content ) ) {
+			$block['innerBlocks'] = parse_blocks( $template_part->content );
+
+			$before = $themeSwitch = $after = $beforeClose = '';
+			switch ($tag) {
+				case 'header':
+					$before = apply_filters('jvbAboveHeader', '');
+					if (!empty($before)) {
+						$before = sprintf(
+							'<aside class="pre header row x-btw">%s</aside>',
+							$before
+						);
+					}
+					$themeSwitch = jvbDarkModeToggle();
+
+					$after = apply_filters('jvbBelowHeader', $after);
+					if (!empty($after)) {
+						$after = sprintf(
+							'<aside class="sub header row x-btw">%s</aside>',
+							$after
+						);
+					}
+					$after .= BreadcrumbManager::getInstance()->renderNavigation();
+					$beforeClose = '<div class="scroll-progress"><div class="bar"></div></div>';
+					break;
+				case 'footer':
+					$before = apply_filters('jvbBeforeFooter', $before);
+					if (!empty($before)) {
+						$before = sprintf(
+							'<aside class="pre footer">%s</aside>',
+							$before
+						);
+					}
+					$beforeClose = jvbRandomFooterText();
+					break;
+			}
+
+			return sprintf(
+				'%s<%s%s>%s%s%s</%s>%s',
+				$before,
+				$tag,
+				$this->getClassesAndStyles($block['attrs']??[]),
+				$themeSwitch,
+				$this->innerBlocks($block),
+				$beforeClose,
+				$tag,
+				$after
+			);
+
+		}
+		if (JVB_TESTING) {
+			jvbDump('Could not create template part block for '.$block['blockName']);
+		}
+		return $content;
+
+//		$isHeaderTemplate = (
+//			(array_key_exists('slug', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['slug']), 'header')) ||
+//			(array_key_exists('tagName', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['tagName']), 'header'))
+//		) ? 'header' : false;
+//		$isFooterTemplate = (
+//			(array_key_exists('slug', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['slug']), 'footer')) ||
+//			(array_key_exists('tagName', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['tagName']), 'footer'))
+//		) ? 'footer' : false;
+//        if (($isHeaderTemplate || $isFooterTemplate)) {
+//			$innerContent = $content;
+//
+//			$tag = $isHeaderTemplate ?: $isFooterTemplate ?: 'div';
+//
+//            $breadcrumbs = $themeSwitch = $afterHeader = $beforeHeader = $footerText= '';
+//            if ($isHeaderTemplate) {
+//
+//				$beforeHeader = apply_filters('jvbAboveHeader', $beforeHeader);
+//				if ($beforeHeader !== '') {
+//					$beforeHeader = '<aside class="pre header row x-btw">'.$beforeHeader.'</aside>';
+//				}
+//                $themeSwitch = jvbDarkModeToggle();
+//                $breadcrumbs = BreadcrumbManager::getInstance()->renderNavigation();
+//				$afterHeader = apply_filters('jvbBelowHeader', $afterHeader);
+//
+//				if ($afterHeader !== '') {
+//					$afterHeader = '<aside class="sub header row x-btw">'.$afterHeader.'</aside>';
+//				}
+//				$footerText = '<div class="scroll-progress"><div class="bar"></div>
+//</div>';
+//            } elseif ($isFooterTemplate) {
+//				$beforeHeader = apply_filters('jvbBeforeFooter', '');
+//				if ($beforeHeader !== '') {
+//					$beforeHeader = '<aside class="footer">'.$beforeHeader.'</aside>';
+//				}
+//					$footerText = jvbRandomFooterText();
+//			}
+//
+//            $content = $beforeHeader.'<'.$tag.$this->getClassesAndStyles($block['attrs']??[]).'>'.
+//                   $themeSwitch .
+//					$this->innerBlocks($block).
+////				   $this->innerBlocks($block).
+////					$innerContent.
+//                   $footerText.'</'.$tag.'>'.$afterHeader.$breadcrumbs;
+//        }
+//
+//        return $content;
     }
     //core_term_description
 
@@ -867,32 +1718,427 @@
      * Widgets Blocks
      */
     //core_archives
+	public function render_core_archives(array $block, string $content):string
+	{
+		jvbDump($block, 'archives');
+		$attrs = $block['attrs']??[];
+		$isDropdown = $this->checkAttrs('displayAsDropdown', $attrs);
+
+		$replace = strtok($content,'>').'>';
+		$content = str_replace($replace, '', $content);
+
+		if ($isDropdown) {
+			$content = sprintf(
+				'<div%s>%s',
+				$this->getClassesAndStyles($attrs, ['archive dropdown']),
+				$content
+			);
+		} else {
+			$content = sprintf(
+				'<ul%s>%s',
+				$this->getClassesAndStyles($attrs, ['archive-list']),
+				$content
+			);
+		}
+
+		return $content;
+	}
     //core_calendar
+	public function render_core_calendar(array $block, string $content):string
+	{
+		$content = $this->inside($block, false, $content);
+		$replace = strtok($content, '>').'>';
+		$content = str_replace($replace, '', $content);
+		return sprintf(
+			'<table%s>%s',
+			$this->getClassesAndStyles($block['attrs']??[], ['calendar']),
+			$content
+		);
+	}
     //core_categories
+	public function prerender_core_categories(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+		$attrs = $block['attrs']??[];
+		$args = [
+			'taxonomy'		=> 'category',
+			'hide_empty'	=> !$this->checkAttrs('showEmpty', $attrs)
+		];
+
+		$showHierarchy = $this->checkAttrs('showHierarchy', $attrs);
+		if ($this->checkAttrs('showOnlyTopLevel', $attrs) || $showHierarchy){
+			$args['parent'] = 0;
+		}
+
+		$terms = $this->getTerms($args, $showHierarchy);
+		if (!$terms){
+			return '';
+		}
+
+
+		$showPostCounts = $this->checkAttrs('showPostCounts', $attrs);
+		$isDropdown = $this->checkAttrs('displayAsDropdown', $attrs);
+
+		if ($isDropdown) {
+			$this->counter('core_categories');
+		}
+
+		$tax = get_taxonomy($args['taxonomy']);
+		$taxonomyName = $tax->label??'Categories';
+		$taxonomySingular = $tax->labels->singular_name??'Category';
+		$inner = $this->buildTermList($terms, $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts);
+		if ($isDropdown) {
+			return sprintf(
+				'<div%s>%s</div>',
+				$this->getClassesAndStyles($attrs, ['taxonomy-dropdown']),
+				$inner
+			);
+		}
+		return sprintf(
+			'<ul%s>%s</ul>',
+			$this->getClassesAndStyles($attrs, ['taxonomy-list', jvbNoBase($args['taxonomy'])]),
+			$inner
+		);
+	}
+		public function getTerms(array $args, bool $showHierarchy = false):array|false
+		{
+			$terms = get_terms($args);
+			if (!$terms || is_wp_error($terms)) {
+				return false;
+			}
+			$terms = array_map(function ($term) {
+				return (array) $term;
+			}, $terms);
+
+			if ($showHierarchy) {
+				$terms = array_map(function ($term) use ($args) {
+					$args['parent'] = $term['term_id'];
+					$children = $this->getTerms($args, true);
+					$term['children'] = $children?:[];
+					return $term;
+				}, $terms);
+			}
+
+			return $terms;
+		}
+		protected function buildTermList(array $terms, string $taxonomyName, string $taxonomySingular, bool $isDropdown, bool $showPostCounts, bool $isOpening = true, int $level = 0):string
+		{
+			$out = '';
+			if ($isOpening) {
+				$out = $isDropdown ?
+					sprintf(
+						'<label for="taxonomy-select-%s">%s</label>
+						<select name="%s_name" id="taxonomy-select-%s"><option value="">Select %s</option>',
+						static::$counters['core_categories'],
+						$taxonomyName,
+						str_replace('-', '_',sanitize_title(strtolower($taxonomyName))),
+						static::$counters['core_categories'],
+						$taxonomyName
+					) :
+					'';
+			} elseif (!$isDropdown) {
+				$out .= '<ul>';
+			}
+
+
+			$prefix = '';
+			if ($isDropdown) {
+				$base = '&emsp;';
+				for ($i = 1; $i <= $level; $i++) {
+					$prefix .= $base;
+				}
+				$prefix .= empty($prefix) ? '' : '- ';
+			}
+
+			$theTerms = array_map(function ($term) use ($taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, $prefix, $level) {
+				if ($isDropdown) {
+					return sprintf(
+						'<option value="%s">%s%s%s</option>%s',
+						$term['slug'],
+						$prefix,
+						$term['name'],
+						$showPostCounts ? ' ('.$term['count'].')' : '',
+						empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1)
+					);
+				}
+				return sprintf(
+					'<li><a href="%s">%s%s</a>%s</li>',
+					get_term_link($term['term_id']),
+					$term['name'],
+					$showPostCounts ? ' <span class="count">'.$term['count'].'</span>' : '',
+					empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1)
+				);
+			}, $terms);
+
+			$out .= implode('', $theTerms);
+
+			if ($isOpening) {
+				$out .= $isDropdown ?
+					'</select>' :
+					'';
+			} else if (!$isDropdown) {
+				$out .= '</ul>';
+			}
+
+
+			return $out;
+		}
     //core_html
     //core_latest_comments
     //core_latest_posts
+	public function prerender_core_latest_posts(array $block, ?string $content, ?WP_Block $parent):?string {
+		$attrs = $block['attrs']??[];
+//		jvbDump($block, 'latest posts');
+
+		$args = [];
+		$title = 'Latest Posts';
+		$args['order'] = array_key_exists('order', $attrs) ? strtoupper($attrs['order']) : 'DESC';
+		$args['orderby'] = array_key_exists('orderBy', $attrs) ? $attrs['orderBy'] : 'date';
+		$args['posts_per_page'] = array_key_exists('postsToShow', $attrs) ? $attrs['postsToShow'] : 5;
+
+		if (array_key_exists('categories', $attrs)) {
+			$list = jvbCommaList(array_column($attrs['categories'], 'name'));
+			$args['tax_query'] = [];
+			$args['tax_query'][] = [
+				'taxonomy'	=> 'category',
+				'terms'		=> array_column($attrs['categories'], 'id')
+			];
+			$title .= ' in '.$list;
+		}
+
+		$posts = new WP_Query($args);
+
+		if (!$posts->have_posts()) {
+			return '';
+		}
+		$posts = array_map(function ($post) use ($attrs) {
+			$img = $this->checkAttrs('displayFeaturedImage', $attrs)
+				? $this->image(get_post_thumbnail_id($post->ID), 'tiny', 'thumbnail')
+				: '';
+
+			$author = $this->checkAttrs('displayAuthor', $attrs)
+				? sprintf(
+					'<a href="%s">%s</a>',
+					get_author_posts_url($post->post_author),
+					get_userdata($post->post_author)->display_name
+				)
+				: '';
+
+			$date = $this->checkAttrs('displayPostDate', $attrs)
+				? sprintf(
+					'<time datetime="%s">%s</time>',
+					date('Y-m-d', strtotime($post->post_date)),
+					date_i18n('M j, Y', strtotime($post->post_date))
+				)
+				: '';
+			$authorDate = $author;
+			if (!empty($authorDate) && !empty($date)) {
+				$authorDate .= ' | '.$date;
+			} else if (!empty($date)) {
+				$authorDate = $date;
+			}
+
+			$excerpt = '';
+			if ($this->checkAttrs('displayPostContent', $attrs)) {
+				if (array_key_exists('excerptLength', $attrs)) {
+					$excerpt = wp_trim_words(get_the_content($post->ID), $attrs['excerptLength'], '...');
+				} else {
+					$excerpt = get_the_excerpt($post->ID);
+				}
+			}
+			if (!empty($excerpt)) {
+				$excerpt = wpautop($excerpt);
+			}
+
+			return sprintf(
+				'<li>%s<p><a href="%s">%s</a>%s</p>%s</li>',
+				$img,
+				get_the_permalink($post->ID),
+				$post->post_title,
+				!empty($authorDate) ? ' <small>— '.$authorDate.'</small>' : '',
+				$excerpt
+			);
+		}, $posts->posts);
+
+		wp_reset_postdata();
+		return sprintf(
+			'<ul%s>%s</ul>',
+//			$title,
+			$this->getClassesAndStyles($attrs, ['post-list']),
+			implode('', $posts)
+		);
+	}
     //core_page_list
-    //core_page_list_item
-    //core_rss
+	public function prerender_core_page_list(array $block, ?string $content, ?WP_Block $parent):?string{
+		$attrs = $block['attrs']??[];
+		$parent = array_key_exists('parentPageID', $attrs) ? $attrs['parentPageID'] : 0;
+		$pages = new WP_Query([
+			'post_type'			=> 'page',
+			'posts_per_page'	=> -1,
+			'parent'			=> $parent
+		]);
+
+		if (!$pages->have_posts()) {
+			return '';
+		}
+		$inside = [];
+		foreach($pages->posts as $page) {
+			jvbDump($page);
+			$inside[] = sprintf(
+				'<li><a href="%s">%s</a>',
+				get_the_permalink($page->ID),
+				$page->post_title
+			);
+		}
+		wp_reset_postdata();
+		return sprintf(
+			'<ul%s>%s</ul>',
+			$this->getClassesAndStyles($attrs, ['page-list']),
+			implode('',$inside)
+		);
+	}
+    //core_page_list_item (doesn't seem to be a thing)
+//	public function prerender_core_page_list_item(array $block, ?string $content, ?WP_Block $parent):?string{
+//		return $content;
+//	}
+    //core_
+//	public function prerender_core_rss(array $block, ?string $content, ?WP_Block $parent):?string
+//	{
+//		jvbDump($block, 'rss');
+//		return $content;
+//	}
     //core_search
-    //core_shortcode
-	public function render_core_social_link(array $block, string $content):string
+	public function prerender_core_search(array $block, ?string $content, ?WP_Block $parent):?string
 	{
-		$url = $block['attrs']['url'];
-		$service = $block['attrs']['service'];
+//		jvbDump($block, 'search');
+		$attrs = $block['attrs']??[];
+		$label = array_key_exists('label', $attrs) && !empty($attrs['label']) ? $attrs['label'] : '';
+		if (array_key_exists('showLabel', $attrs) && $attrs['showLabel'] === false) {
+			$label = '';
+		}
+		$placeholder = array_key_exists('placeholder', $attrs) ? $attrs['placeholder'] : 'Search...';
+
+		$buttonText = array_key_exists('buttonText', $attrs) && !empty($attrs['buttonText']) ? $attrs['buttonText'] : '';
+
+		$isInside = array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-inside';
+
+		$hideInput = $this->checkAttrs('isSearchFieldHidden', $attrs) || (array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-only');
+
+		return str_replace('<div class="search-container row left nowrap"', sprintf(
+			'<div%s',
+			$this->getClassesAndStyles($attrs, ['search-container', 'row', 'left', 'nowrap'])
+		), jvbSearch($placeholder, uniqid(), $label, $buttonText, $isInside, $hideInput));
+	}
+    //core_shortcode
+	public function prerender_core_social_link(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+//		jvbDump($block, 'social link');
+//		jvbDump($parent, 'Parent');
+		$parentAttrs = false;
+		if ($parent) {
+			$parentAttrs = $parent->attributes;
+		}
+		$attrs = $block['attrs']??[];
+		$url = $attrs['url']??'';
+		$service = $attrs['service']?:'';
 		$iconName = ($service === 'bluesky') ? 'butterfly' : $service.'-logo';
 		$icon = jvbIcon($iconName);
 		if (!$icon) {
 			$icon = jvbIcon('link');
 		}
-		return '<li><a href="'.$url.'" target="_blank" rel="nofollow" title="Find us on '.ucfirst($service).'">'.$icon.'<span class="screen-reader-text">Find us on '.ucfirst($service).'</span></a></li>';
+		$serviceName = $this->getServiceName($service);
+		$label = $parentAttrs && (!array_key_exists('className', $parentAttrs) || !str_contains($parentAttrs['className'], 'logos-only'))
+				? sprintf(
+					'<span>%s</span>',
+					$serviceName
+				)
+				: sprintf(
+					'<span class="screen-reader-text">Find us on %s</span>',
+				$serviceName
+			);
+		$pillShaped = $parentAttrs && (array_key_exists('className', $parentAttrs) && str_contains($parentAttrs['className'], 'pill-shape'))
+			? 'style="border-radius:var(--radius-outer);"'
+			: '';
+		return sprintf(
+			'<li><a href="%s" target="_blank" rel="nofollow" title="Find us on %s"%s>%s%s</a></li>',
+			$url,
+			$serviceName,
+			$pillShaped,
+			$icon,
+			$label
+		);
 	}
-	public function render_core_social_links(array $block, string $content):string
+		private function getServiceName(string $service) {
+			return match($service){
+				'wordpress'	=> 'WordPress',
+				default => ucfirst($service)
+			};
+		}
+	public function prerender_core_social_links(array $block, ?string $content, ?WP_Block $parent):?string
 	{
-		return '<ul class="socials">'.$this->innerBlocks($block).'</ul>';
+//		jvbDump($block['attrs']??[], 'social links');
+//		jvbDump($parent, 'Parent');
+
+		return sprintf(
+			'<ul%s>%s</ul>',
+			$this->getClassesAndStyles($block['attrs']??[], ['socials']),
+			$this->innerBlocks($block, '','',$block)
+		);
 	}
     //core_tag_cloud
+	public function prerender_core_tag_cloud(array $block, ?string $content, ?WP_Block $parent):?string
+	{
+//		jvbDump($block, 'tag cloud');
+		$attrs = $block['attrs']??[];
+		$taxonomy = (array_key_exists('taxonomy', $attrs) && !empty($attrs['taxonomy']))
+			? $attrs['taxonomy']
+			: 'post_tag';
+		$showCounts = $this->checkAttrs('showTagCounts', $attrs);
+
+		$terms = get_terms([
+			'taxonomy'		=> $taxonomy,
+			'hide_empty'	=> true,
+		]);
+
+		if (!$terms || is_wp_error($terms)) {
+			return '';
+		}
+
+		$inside = '';
+
+		foreach ($terms as $term) {
+			$url = get_term_link($term->term_id, $taxonomy);
+			$count = $showCounts ?
+				sprintf(
+					'<span class="count">%d</span>',
+					$term->count
+				) :
+				'';
+			$size = match(true) {
+				$term->count <= 2 => 'small',
+				$term->count <= 5 => 'x-small',
+				$term->count <= 10 => 'medium',
+				$term->count <= 15 => 'x-medium',
+				$term->count <= 20 => 'large',
+				$term->count <= 25 => 'x-large',
+				$term->count <= 30 => 'xx-large',
+				$term->count > 30 => 'xxx-large',
+			};
+			$fontSize = 'font-size: var(--txt-'.$size.');';
+			$inside .= sprintf(
+				'<li class="%s");"><a href="%s" rel="tag">%s%s</a></li>',
+				$size,
+//				$fontSize,
+				$url,
+				$term->name,
+				$count
+			);
+		}
+		return sprintf(
+			'<ul%s>%s</ul>',
+			$this->getClassesAndStyles($attrs, ['term-list','cloud', jvbNoBase($taxonomy)]),
+			$inside
+		);
+	}
 
 
     /**
@@ -912,62 +2158,51 @@
     /***********************************
      * Helpers
      **********************************/
-	public function stripTagContents(string $tag, string $content):string
+	public function stripTagContents(string $tag, ?string $content):string
 	{
 		$clean = preg_replace('/<'.$tag.'\b[^>]*>.*?<\/'.$tag.'>/is', '', $content);
 		$clean = preg_replace('/\s+/', ' ', $clean);
 		return trim($clean);
 	}
 
-    public function innerBlocks(array $block, string $before = '', string $after = ''):string
+    public function innerBlocks(array $block, string $before = '', string $after = '', ?array $parent = null):string
     {
+		if ($parent) {
+			$parent = new WP_Block($parent);
+		}
 		$content = '';
 		foreach ($block['innerBlocks'] as $b) {
-			$method = 'render_'.$this->sanitizeBlockName($b);
-			$function = BASE.$method;
 
-			$content .= $before;
-			if (function_exists($function)) {
-				$content .= $function($b, '');
-			} else if (method_exists($this, $method)) {
-				$content .= $this->$method($b, '');
-			} else {
-				$content .= render_block($b);
-			}
-			$content .= $after;
+			$rendered = $parent
+				? $this->checkMethods(null, $b, $parent, true)
+				: render_block($b);
+
+			$content .= sprintf('%s%s%s',
+				$before,
+				$rendered,
+				$after
+			);
 		}
 		return $content;
     }
 
-    public function inside(array $block, mixed $tag = false, mixed $o = false):string
-    {
-        if (!$o) {
-            $o = trim($block['innerHTML']);
-        }
-        if (!$tag) {
-            //check to see if there was one dynamically set first
-            $tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : '';
-            $tag = ($tag == '') ? str_replace('<', '', strtok($o, '>')) : '';
-            $tag = (str_contains($tag, ' class')) ? strtok($tag, ' class') : $tag;
-            $tag = trim($tag);
-        }
-		if (!str_starts_with($o, '<'.$tag)) {
-			return $o;
+	public function inside(array $block, mixed $tag = false, mixed $o = false): string
+	{
+		$html = $o ?: trim($block['innerHTML']);
+
+		if (empty($html)) {
+			return '';
 		}
 
-        $len = strlen('</'.$tag.'>');
+		if (preg_match('/^<(\w+)[^>]*>(.*)<\/\1>$/s', $html, $matches)) {
+			if ($tag && strtolower($matches[1]) !== strtolower($tag)) {
+				return $html;
+			}
+			return trim($matches[2]);
+		}
 
-        return substr_replace(
-            str_replace(
-                strtok($o, '>').'>',
-                ' ',
-                $o
-            ),
-            '',
-            -$len,
-            $len
-        );
-    }
+		return $html;
+	}
 
 	/**
 	 * Extract content from a specific nested element
@@ -1003,7 +2238,7 @@
                  (!array_key_exists('attrs', $block) && !array_key_exists('id', $block['attrs'])))) {
                 $ID = get_post_thumbnail_id();
             } else {
-                if (array_key_exists('id', $block['attrs'])) {
+                if (array_key_exists('id', $block['attrs']??[])) {
                     $ID = $block['attrs']['id'];
                 } elseif (array_key_exists('mediaId', $block['attrs'])) {
                     $ID = $block['attrs']['mediaId'];
@@ -1122,15 +2357,36 @@
 			$classes[] = 'col';
 		}
 
+
         // Merge with passed classes and styles
         $styles = array_merge($attr_styles, $styles);
         $classes = array_merge($attr_classes, $classes);
 
+		if (!empty(static::$pendingClass)) {
+			$classes = array_merge($classes, static::$pendingClass);
+			static::$pendingClass = [];
+		}
+		$classes = array_unique($classes);
+		$data = $this->getDataset($attrs);
+
         // Build attribute strings
         $class_string = !empty($classes) ? ' class="' . implode(' ', $classes) . '"' : '';
         $style_string = !empty($styles) ? ' style="' . implode(';', $styles) . '"' : '';
+		$data_string = '';
+		if (!empty($data)) {
+			foreach ($data as $d => $v) {
+				if ($d === 'bg-small') {
+					$data_string .= ' data-bg-img';
+				}
+				$data_string .= sprintf(
+					' data-%s="%s"',
+					$d,
+					$v
+				);
+			}
+		}
 
-        $return = trim($class_string . $style_string);
+        $return = trim($class_string . $style_string . $data_string);
         return ($return=='')? '' : ' '.$return;
     }
     /**
@@ -1160,18 +2416,18 @@
         $classes = [];
         foreach ($attrs as $key => $value) {
             $class = $this->getClass($key, $value, $attrs);
-            if (is_array($class)) {
-                $classes = array_merge($classes, $class);
-            } else {
-                $classes[] = $class;
-            }
+			if (is_string($class)) {
+				$class = explode(' ', $class);
+			}
+			$classes = array_merge($classes, $class);
         }
-		return array_filter($classes, function ($class) {
+		return array_unique(array_filter($classes, function ($class) {
 			return $class!=='' && !str_starts_with($class, 'wp');
-		});
+		}));
     }
     protected function getClass(string $key, string|bool|array|int $value, array $attrs):string|array
     {
+		//TODO: gradient
         switch ($key) {
             //Any additional classes the user adds
             case 'className':
@@ -1181,112 +2437,38 @@
                     default => str_replace('is-style-', '', $value),
                 };
 			case 'contentPosition':
-
-				$classes = [];
-				$pos = explode(' ', $value);
-				foreach($pos as $p) {
-					switch ($p) {
-						case 'top':
-							$classes[] = 'a-start';
-							break;
-						case 'right':
-							$classes[] = 'end';
-							break;
-						case 'bottom':
-							$classes[] = 'a-end';
-							break;
-						case 'left':
-							$classes[] = 'start';
-							break;
-					}
-				}
-				return implode(' ', $classes);
+				return $this->getContentPosition($value);
+			case 'term':
+			case 'taxonomy':
+				return jvbNoBase($value);
             //Layout attributes
             case 'layout':
-                $classes = [];
-				$type = 'row';
-                if (array_key_exists('type', $value)) {
-					$type = 'col';
-//                    if ($value['type'] === 'constrained') {
-//                        $classes[] = 'container col';
-//                    }
-                }
-				if (array_key_exists('orientation', $value)) {
-					$type = 'col';
-                    if ($value['orientation'] === 'vertical') {
-						$classes[] = 'col';
-						if (in_array('row', $classes)) {
-							$index = array_search('row', $classes);
-							unset($classes[$index]);
-						}
-					}
-                }else if (array_key_exists('type', $value) && $value['type'] === 'flex') {
-					$classes[] = 'row';
-					if (in_array('col', $classes)) {
-						$index = array_search('col', $classes);
-						unset($classes[$index]);
-					}
-				}
-//jvbDump($type);
-//jvbDump($value);
-//				$check = [$value, $attrs];
-//				foreach ($check as $ch) {
-//
-//				}
-				if (!array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) {
-					$classes[] = ($type === 'row') ? 'start' : 'a-start';
-				}
-				if (array_key_exists('justifyContent', $value)  && !array_key_exists('contentPosition', $attrs)) {
-					if (in_array($value['justifyContent'], ['left', 'right','space-between'])) {
-//						jvbDump($type);
-						switch ($value['justifyContent']) {
-							case 'right':
-								$classes[] = ($type === 'row') ? 'end' : 'a-end';
-								break;
-							case 'space-between':
-								$classes[] = 'btw';
-								break;
-						}
-					}
-				}
-
-
-                if (array_key_exists('flexWrap', $value)) {
-                    if ($value['flexWrap'] === 'nowrap') {
-                        $classes[] = 'nowrap';
-                    }
-                }
-                return implode(' ', $classes);
+                return $this->getLayout($value, $attrs);
             case 'align':
                 return !empty($value) ? 'align-'.$value : '';
             case 'verticalAlignment':
-                return !empty($value) ? 'v-align-'.$value : '';
-            case 'isStackedMobile':
+				switch ($value) {
+					case 'bottom':
+						$value = 'btm';
+						break;
+					case 'center':
+						$value = 'y-mid';
+					default:
+				}
+                return !empty($value) ? $value : '';
+            case 'isStackedOnMobile':
                 return ($value === true) ? 'stack-small' : '';
             case 'justifyContent':
                 return !empty($value) ? 'j-'.$value : '';
             case 'orientation':
                 return $value==='column' ? 'column' : '';
             case 'width':
+				return $this->getWidth($value);
             case 'dimRatio':
-                if (is_numeric($value)) {
-                    $width = match (true) {
-                        $value < 25 => '25',
-                        $value < 33 => '33',
-                        $value <= 50 => '50',
-                        $value < 66 => '66',
-                        $value < 75 => '75',
-                        default => 'full',
-                    };
-                    switch ($key) {
-                        case 'width':
-                            return 'width-'.$width;
-                        case 'dimRatio':
-                            return 'overlay-'.$width;
-                    }
-                }
-                return '';
-
+                return $this->getDimRatio($value);
+			case 'overlayColor':
+				return $value;
+				break;
             //Typography
             case 'textAlign':
                 return !empty($value) ? 'text-'.$value : '';
@@ -1301,171 +2483,285 @@
 
             //Style base:
             case 'style':
-                $classes = [];
-                //Margin and Padding
-				if (array_key_exists('spacing', $value)) {
-					foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) {
-						if (array_key_exists($search, $value['spacing'])) {
-							$directions = [];
-
-							// Collect ONLY preset spacing values for classes
-							foreach ($value['spacing'][$search] as $direction => $size) {
-								$presetSize = $this->getPresetSpacing($size);
-								if ($presetSize) {
-									$directions[$direction] = $presetSize;
-								}
-								// Non-preset values are skipped here and handled by inline styles below
-							}
-
-							if (empty($directions)) {
-								continue;
-							}
-
-							// Check what directions we have
-							$hasTop = isset($directions['top']);
-							$hasBottom = isset($directions['bottom']);
-							$hasLeft = isset($directions['left']);
-							$hasRight = isset($directions['right']);
-
-							// Check if axes match
-							$xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right'];
-							$yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom'];
-
-							// All 4 directions exist and match → p-3
-							if ($hasTop && $hasBottom && $hasLeft && $hasRight &&
-								count(array_unique($directions)) === 1) {
-								$classes[] = $c . '-' . reset($directions);
-							}
-							// Both axes match → px-3 py-2
-							elseif ($xMatch && $yMatch) {
-								$classes[] = $c . 'x-' . $directions['left'];
-								$classes[] = $c . 'y-' . $directions['top'];
-							}
-							// Only X axis matches → px-3 (+ individual for top/bottom)
-							elseif ($xMatch) {
-								$classes[] = $c . 'x-' . $directions['left'];
-								if ($hasTop) {
-									$classes[] = $c . 't-' . $directions['top'];
-								}
-								if ($hasBottom) {
-									$classes[] = $c . 'b-' . $directions['bottom'];
-								}
-							}
-							// Only Y axis matches → py-3 (+ individual for left/right)
-							elseif ($yMatch) {
-								$classes[] = $c . 'y-' . $directions['top'];
-								if ($hasLeft) {
-									$classes[] = $c . 'l-' . $directions['left'];
-								}
-								if ($hasRight) {
-									$classes[] = $c . 'r-' . $directions['right'];
-								}
-							}
-							// No matches - individual directions
-							else {
-								foreach ($directions as $direction => $size) {
-									$dir = match($direction) {
-										'top' => 't',
-										'bottom' => 'b',
-										'left' => 'l',
-										'right' => 'r',
-										default => $direction
-									};
-									$classes[] = $c . $dir . '-' . $size;
-								}
-							}
-						}
-					}
-				}
-
-                if (array_key_exists('fontSize', $value)) {
-                    if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) {
-                        $classes[] = 'font-'.$value['fontSize'];
-                    }
-                    if (in_array('fontWeight', $value)) {
-                        $classes[] = 'text-'.$value['fontWeight'];
-                    }
-                    if (in_array('textTransform', $value)) {
-                        if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) {
-                            $classes[] = $value['textTransform'];
-                        }
-                    }
-                }
-                return implode(' ', $classes);
+                return $this->getPresetStyles($value);
 			case 'fontSize':
 				$classes[] = 'font-'.$value;
 				return implode(' ', $classes);
-			case 'isStackedOnMobile':
-				return ($value === true) ? 'stack-small' : '';
-			case 'width':
-				if (is_numeric($value)) {
-					$width = match (true) {
-						$value < 25 => '25',
-						$value < 33 => '33',
-						$value <= 50 => '50',
-						$value < 66 => '66',
-						$value < 75 => '75',
-						default => 'full',
-					};
-					switch ($key) {
-						case 'width':
-							return 'width-'.$width;
-						case 'dimRatio':
-							return 'overlay-'.$width;
-					}
+			case 'postLayout':
+				$classes[] = 'item-grid';
+				if (isset($attrs['columns']) && $attrs['columns']!== 3){
+					$classes[] = sprintf(
+						'split-%d',
+						$attrs['columns']
+					);
 				}
-				return '';
+				return $classes;
             default:
-				$ignore = [
-					'useFeaturedImage',
-					'opacity',
-					'borderColor',
-					'backgroundColor',
-					'textColor',
-					'minHeight',
-					'minHeightUnit',
-					'isDark',
-					'sizeSlug',
-					'isUserOverlayColor',
-					'customOverlayColor',
-					'dimRatio',
-					'placeholder',
-					'alt',
-					'imageFill',
-					'mediaSizeSlug',
-					'isLink',
-					'kind',
-					'label',
-					'type',
-					'id',
-					'url',
-					'label',
-					'shouldSyncIcon',
-					'rel',
-					'opensInNewTab',
-					'title',
-					'ref',
-					'overlayMenu',
-					'slug',
-					'theme',
-					'tagName',
-					'level',
-					'ordered',
-					'area',
-					'mediaId',
-					'mediaLink',
-					'mediaType',
-					'height', //maybe still need?
-				];
-				if (!is_admin() &&!in_array($key, $ignore)) {
+				if (JVB_TESTING && !is_admin() &&!in_array($key, $this->ignore)) {
 //					TESTING
-//					jvbDump($key, 'getClass');
-//					jvbDump($attrs);
+					jvbDump($attrs, '[getClass] '.$key);
 				}
 
                 return '';
         }
     }
+		/*** CLASS HELPERS ***/
+		private function getContentPosition(string $value):string
+		{
+			$classes = [];
+			$pos = explode(' ', $value);
+			foreach($pos as $p) {
+				switch ($p) {
+					case 'top':
+						$classes[] = 'top';
+						break;
+					case 'right':
+						$classes[] = 'right';
+						break;
+					case 'bottom':
+						$classes[] = 'btm';
+						break;
+					case 'left':
+						$classes[] = 'left';
+						break;
+				}
+			}
+			return implode(' ', $classes);
+		}
+
+		private function getLayout(array $value, array $attrs):array
+		{
+//			jvbDump($value, 'getLayout');
+			$classes = [];
+
+			$type = 'row';
+			$isRow = true;
+			//Determine type
+			if ((array_key_exists('type', $value) && !in_array($value['type'], ['flex', 'grid'])) ||
+				(array_key_exists('orientation', $value) && $value['orientation'] === 'vertical')) {
+				$type = 'col';
+				$isRow = false;
+			} elseif (array_key_exists('type', $value) && $value['type'] === 'grid') {
+				$type = 'item-grid';
+				$isRow = false;
+				if (array_key_exists('columnCount', $value) && $value['columnCount']!== 3) {
+					$classes[] = sprintf(
+						'split-%s',
+						$value['columnCount']
+					);
+				}
+			}
+
+			if (array_key_exists('justifyContent', $value)  && !array_key_exists('contentPosition', $attrs)) {
+				switch ($value['justifyContent']) {
+					case 'right':
+						$classes[] = 'right';
+						break;
+					case 'center':
+						$classes[] = 'x-mid';
+						break;
+					case 'space-between':
+						$classes[] = 'x-btw';
+						break;
+					case 'left':
+						$classes[] = 'left';
+						break;
+					case 'space-evenly':
+						$classes[] = 'x-even';
+						break;
+					case 'space-around':
+						$classes[] = 'x-around';
+						break;
+					case 'stretch':
+						$classes[] = 'stretch';
+				}
+			} else {
+				$classes[] = 'left';
+			}
+
+			if (array_key_exists('verticalAlignment', $value)) {
+				switch ($value['verticalAlignment']) {
+					case 'bottom':
+						$classes[] = 'btm';
+						break;
+					case 'top':
+						$classes[] = 'top';
+						break;
+					case 'center':
+						$classes[] = 'y-mid';
+						break;
+					case 'space-between':
+						$classes[] = 'y-btw';
+						break;
+					case 'space-around':
+						$classes[] = 'y-around';
+						break;
+					case 'space-even':
+						$classes[] = 'y-even';
+				}
+			}
+
+
+
+			if (array_key_exists('flexWrap', $value)) {
+				if ($value['flexWrap'] === 'nowrap') {
+					$classes[] = 'nowrap';
+				}
+			}
+
+			$classes[] = $type;
+			return $classes;
+		}
+
+		private function getWidth(string $value):string
+		{
+			$value = (int)str_replace('%', '', $value);
+			return sprintf(
+				'width-%d',
+				match (true) {
+					$value <= 25 => '25',
+					$value <= 33 => '33',
+					$value <= 50 => '50',
+					$value <= 66 => '66',
+					$value <= 75 => '75',
+					default => 'full',
+				}
+			);
+		}
+		private function getDimRatio(string $value):string
+		{
+			if (is_numeric($value)) {
+				return sprintf(
+					'op-%d',
+					match (true) {
+						$value <= 14 => '1',
+						$value <= 28 => '2',
+						$value <= 42 => '3',
+						$value <= 56 => '45',
+						$value <= 70 => '4',
+						$value <= 84 => '5',
+						default => '6',
+					}
+				);
+			}
+			return '';
+		}
+
+		private function getPresetStyles(array $value):string
+		{
+			$classes = [];
+			//Margin and Padding
+			if (array_key_exists('spacing', $value)) {
+				$classes = array_merge($classes, $this->buildSpacingClasses($value));
+			}
+			if (array_key_exists('color', $value)) {
+				if (array_key_exists('duotone', $value['color'])) {
+					$preset = explode('|', $value['color']['duotone']);
+					$preset = $preset[array_key_last($preset)];
+					if (str_contains($preset, '-')) {
+						$preset = explode('-', $preset);
+					} else {
+						$preset = [$preset];
+					}
+					$classes[] = 'duotone';
+					foreach ($preset as $p) {
+						$classes[] = $p;
+					}
+				}
+			}
+
+			if (array_key_exists('fontSize', $value)) {
+				if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) {
+					$classes[] = 'font-'.$value['fontSize'];
+				}
+				if (in_array('fontWeight', $value)) {
+					$classes[] = 'text-'.$value['fontWeight'];
+				}
+				if (in_array('textTransform', $value)) {
+					if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) {
+						$classes[] = $value['textTransform'];
+					}
+				}
+			}
+			return implode(' ', $classes);
+		}
+			private function buildSpacingClasses(array $value):array
+			{
+				$classes = [];
+				foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) {
+					if (array_key_exists($search, $value['spacing'])) {
+						$directions = [];
+
+						// Collect ONLY preset spacing values for classes
+						foreach ($value['spacing'][$search] as $direction => $size) {
+							$presetSize = $this->getPresetSpacing($size);
+							if ($presetSize) {
+								$directions[$direction] = $presetSize;
+							}
+							// Non-preset values are skipped here and handled by inline styles below
+						}
+
+						if (empty($directions)) {
+							continue;
+						}
+
+						// Check what directions we have
+						$hasTop = isset($directions['top']);
+						$hasBottom = isset($directions['bottom']);
+						$hasLeft = isset($directions['left']);
+						$hasRight = isset($directions['right']);
+
+						// Check if axes match
+						$xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right'];
+						$yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom'];
+
+						// All 4 directions exist and match → p-3
+						if ($hasTop && $hasBottom && $hasLeft && $hasRight &&
+							count(array_unique($directions)) === 1) {
+							$classes[] = $c . '-' . reset($directions);
+						}
+						// Both axes match → px-3 py-2
+						elseif ($xMatch && $yMatch) {
+							$classes[] = $c . 'x-' . $directions['left'];
+							$classes[] = $c . 'y-' . $directions['top'];
+						}
+						// Only X axis matches → px-3 (+ individual for top/bottom)
+						elseif ($xMatch) {
+							$classes[] = $c . 'x-' . $directions['left'];
+							if ($hasTop) {
+								$classes[] = $c . 't-' . $directions['top'];
+							}
+							if ($hasBottom) {
+								$classes[] = $c . 'b-' . $directions['bottom'];
+							}
+						}
+						// Only Y axis matches → py-3 (+ individual for left/right)
+						elseif ($yMatch) {
+							$classes[] = $c . 'y-' . $directions['top'];
+							if ($hasLeft) {
+								$classes[] = $c . 'l-' . $directions['left'];
+							}
+							if ($hasRight) {
+								$classes[] = $c . 'r-' . $directions['right'];
+							}
+						}
+						// No matches - individual directions
+						else {
+							foreach ($directions as $direction => $size) {
+								$dir = match($direction) {
+									'top' => 't',
+									'bottom' => 'b',
+									'left' => 'l',
+									'right' => 'r',
+									default => $direction
+								};
+								$classes[] = $c . $dir . '-' . $size;
+							}
+						}
+					}
+				}
+				return $classes;
+			}
 
     protected function getInlineStyles(array $attrs):array
     {
@@ -1475,258 +2771,74 @@
         $styles = [];
         foreach ($attrs as $key => $value) {
             $style = $this->getStyle($key, $value, $attrs);
+			if (is_string($style)) {
+				$style = [$style];
+			}
             $styles = array_merge($styles, $style);
         }
         return $styles;
     }
-    protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array
+    protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array|string
     {
         $styles = [];
         switch ($key) {
             // Font family settings
-            case 'fontFamily':
-                if ($value === 'body') {
-                    $styles[] = 'font-family: "Open Sans", system-ui, -apple-system, sans-serif';
-                } elseif ($value === 'heading') {
-                    $styles[] = 'font-family: "Josefin Sans", system-ui, -apple-system, sans-serif';
-                } elseif (!empty($value)) {
-                    $styles[] = 'font-family: '.$value;
-                }
-                break;
+			case 'size':
+				return $this->getIconSizeStyle($value);
+
+			case 'fontFamily':
+                return $this->getFontFamilyStyle($value);
 
             // Icon color (for icon blocks)
             case 'iconColorValue':
-                if (!empty($value)) {
-                    $styles[] = 'color: '.$value;
-                }
-                break;
+				return $this->getColorStyle($value);
 
             // Minimum height settings
             case 'minHeight':
-                if (!empty($value) && isset($attrs['minHeightUnit'])) {
-                    $styles[] = 'min-height: '.$value.$attrs['minHeightUnit'];
-                } elseif (!empty($value)) {
-                    $styles[] = 'min-height: '.$value.'px'; // Default to px if no unit specified
-                }
-                break;
+                return $this->getMinHeightStyle($value, $attrs);
 
             // Background URL (for cover, media blocks)
             case 'url':
                 if (!empty($value) && str_starts_with($value, 'http')) {
-                    $styles[] = 'background-image: url('.$value.')';
+                    return 'background-image: url('.$value.')';
                 }
                 break;
 
             // Focal point for background images
             case 'focalPoint':
-				$x = (array_key_exists('x', $attrs['focalPoint'])) ? $attrs['focalPoint']['x'] * 100 : 'center';
-				$y = (array_key_exists('y', $attrs['focalPoint'])) ? $attrs['focalPoint']['y'] * 100 : 'center';
-				$styles[] = 'background-position:'.$x.' '.$y.';';
-
-                break;
+				return $this->getFocalPointStyle($value);
 
             // Complex style object
             case 'style':
-                // Border styles
-                if (isset($value['border'])) {
-                    $border = $value['border'];
+                return $this->extractStyles($value, $attrs);
 
-                    if (isset($border['radius'])) {
-                        $styles[] = 'border-radius: '.$border['radius'];
-                    }
-
-                    if (isset($border['width'])) {
-                        $styles[] = 'border-width: '.$border['width'];
-                    }
-
-                    if (isset($border['style']) && isset($border['width']) && !empty($border['style'])) {
-                        $styles[] = 'border-style: '.$border['style'];
-                    }
-
-                    if (isset($border['color'])) {
-                        $styles[] = 'border-color: '.$border['color'];
-                    }
-                }
-
-                // Color styles
-                if (isset($value['color'])) {
-                    $color = $value['color'];
-
-                    if (isset($color['background'])) {
-                        $styles[] = 'background-color: '.$color['background'];
-                    }
-
-                    if (isset($color['text'])) {
-                        $styles[] = 'color: '.$color['text'];
-                    }
-
-                    if (isset($color['gradient'])) {
-                        $styles[] = 'background: '.$color['gradient'];
-                    }
-                }
-
-                // Layout styles
-                if (isset($value['layout'])) {
-                    foreach ($value['layout'] as $layout => $option) {
-                        switch ($layout) {
-                            case 'selfStretch':
-                                if ($option === 'fixed' && isset($value['layout']['selfStretchValue'])) {
-                                    $styles[] = 'width: '.$value['layout']['selfStretchValue'];
-                                }
-                                break;
-                        }
-                    }
-                }
-
-                // Typography styles
-                if (isset($value['typography'])) {
-                    $typography = $value['typography'];
-
-                    if (isset($typography['fontSize'])) {
-                        $styles[] = 'font-size: '.$typography['fontSize'];
-                    }
-
-                    if (isset($typography['fontWeight'])) {
-                        $styles[] = 'font-weight: '.$typography['fontWeight'];
-                    }
-
-                    if (isset($typography['textDecoration'])) {
-                        $styles[] = 'text-decoration: '.$typography['textDecoration'];
-                    }
-
-                    if (isset($typography['textTransform'])) {
-                        $styles[] = 'text-transform: '.$typography['textTransform'];
-                    }
-
-                    if (isset($typography['letterSpacing'])) {
-                        $styles[] = 'letter-spacing: '.$typography['letterSpacing'];
-                    }
-
-                    if (isset($typography['lineHeight'])) {
-                        $styles[] = 'line-height: '.$typography['lineHeight'];
-                    }
-                }
-
-                // Spacing styles
-                if (isset($value['spacing'])) {
-                    $spacing = $value['spacing'];
-
-                    // Don't duplicate margin/padding that's handled by classes
-                    // Only add specific CSS values here that wouldn't work well as classes
-                    if (isset($spacing['margin'])) {
-                        foreach ($spacing['margin'] as $direction => $size) {
-                            // If not a preset value, add as inline style
-                            if (!str_contains($size, 'var:preset')) {
-                                $styles[] = 'margin-'.$direction.': '.$size;
-                            }
-                        }
-                    }
-
-                    if (isset($spacing['padding'])) {
-                        foreach ($spacing['padding'] as $direction => $size) {
-                            // If not a preset value, add as inline style
-                            if (!str_contains($size, 'var:preset')) {
-                                $styles[] = 'padding-'.$direction.': '.$size;
-                            }
-                        }
-                    }
-                }
-                break;
 			case 'dimRatio':
-				$ratio = (ceil($value /25) *25);
-				$s = 'background-color: rgba(var(--base-rgb), ';
-				switch ($ratio) {
-					case 0:
-						$s .= 'var(--rgb-subtle-hover));';
-						break;
-					case 25:
-						$s .= 'var(--rgb-light));';
-						break;
-					case 50:
-						$s .= 'var(--rgb-medium));';
-						break;
-					default:
-						$s .= 'var(--rgb-heavy));';
-						break;
-				}
-				$styles[] = $s;
-				break;
+				return $this->getDimRatioStyle($value, $attrs);
 
             // Custom styles (any other attributes that need inline styling)
             case 'backgroundType':
                 if ($value === 'video' && isset($attrs['backgroundUrl'])) {
                     // Don't set a background image for videos - it will be handled by the video element
                 } elseif (isset($attrs['backgroundUrl'])) {
-                    $styles[] = 'background-image: url('.$attrs['backgroundUrl'].')';
+                    return 'background-image: url('.$attrs['backgroundUrl'].')';
                 }
                 break;
 
 			case 'backgroundColor':
 			case 'borderColor':
 			case 'textColor':
-				$type = ($key === 'backgroundColor') ? 'background-color:' : (($key === 'borderColor') ? 'border-color:' : 'color:');
-				$defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']);
-				$continue = true;
-				foreach ($defaults as $default) {
-					if (str_starts_with($value, $default)) {
-						$continue = false;
-						$styles[] = $type.'var(--'.$value.')';
-					}
+				$type = str_replace('Color', '-color', $key);
+				$type = str_replace('text-', '', $type);
+				if (isset($attrs['border']['width']) && $key === 'borderColor') {
+					break;
 				}
-				if ($continue) {
-					$styles[] = $type.$value;
-				}
-				break;
+				return $this->getColorStyle($value, $type);
+
             // Any other attributes that need direct styling
             default:
-				$ignore = [
-					'useFeaturedImage',
-					'opacity',
-					'textAlign',
-					'minHeightUnit',
-					'isDark',
-					'isUserOverlayColor',
-					'contentPosition',
-					'sizeSlug',
-					'customOverlayColor',
-					'alt',
-					'placeholder',
-					'imageFill',
-					'mediaSizeSlug',
-					'isLink',
-					'kind',
-					'label',
-					'type',
-					'id',
-					'url',
-					'label',
-					'shouldSyncIcon',
-					'rel',
-					'opensInNewTab',
-					'title',
-					'ref',
-					'overlayMenu',
-					'slug',
-					'theme',
-					'tagName',
-					'level',
-					'ordered',
-					'area',
-					'className',
-					'fontSize',
-					'layout',
-					'align',
-					'mediaId',
-					'mediaLink',
-					'mediaType',
-					'isStackedOnMobile',
-					'width',
-					'height', // maybe still need?
-				];
-				if (!is_admin() && !in_array($key, $ignore)) {
+				if (JVB_TESTING && !is_admin() && !in_array($key, $this->ignore)) {
 					//TESTING
-//					jvbDump($key, 'getStyle');
-//					jvbDump($attrs);
+					jvbDump($attrs, '[getStyle] '.$key);
 				}
                 // No default inline styles
                 break;
@@ -1734,6 +2846,367 @@
 
         return $styles;
     }
+		private function getIconSizeStyle(string $value):array
+		{
+			$values = explode(' ', $value);
+			$styles = [];
+			foreach ($values as $v) {
+				switch ($value) {
+					case 'has-small-icon-size':
+						$styles[] = '--w:var(--txt-x-small)';
+						break;
+					case 'has-large-icon-size':
+						$styles[] = '--w:var(--txt-large)';
+						break;
+					case 'has-huge-icon-size':
+						$styles[] = '--w:var(--txt-xx-large)';
+						break;
+					default:
+						if (JVB_TESTING) {
+							jvbDump($value, 'No preset found for size: '.print_r($value, true));
+						}
+				}
+			}
+			return $styles;
+		}
+		private function getFontFamilyStyle(string $value):string
+		{
+			return match($value) {
+				'body'		=> 'font-family: var(--body)',
+				'heading'	=> 'font-family: var(--heading)',
+				default		=> sprintf('font-family: %s', $value)
+			};
+		}
+		private function getColorStyle(string $value, ?string $type = 'color'):string
+		{
+			if (!in_array($type, ['color', 'background','background-color','border-color'])) {
+				$type = null;
+			}
+			return sprintf(
+				'%s%s',
+				$type ? $type.': ' : '',
+				$this->getColor($value)
+			);
+		}
+		private function getMinHeightStyle(string $value, array $attrs):string
+		{
+			$out = '';
+			if (!empty($value)) {
+				if (isset($attrs['minHeightUnit'])) {
+					$out = sprintf(
+						'min-height: %s%s',
+						$value,
+						$attrs['minHeightUnit']
+					);
+				} else {
+					$out = sprintf(
+						'min-height: %spx',
+						$value
+					);
+				}
+			}
+			return $out;
+		}
+
+		private function getFocalPointStyle(array $value):string
+		{
+			jvbDump($value, 'Focal Point');
+			$x = array_key_exists('x', $value) ? $value['x'] * 100 : 'center';
+			$y = array_key_exists('y', $value) ? $value['y'] * 100 : 'center';
+
+			$y = $x === $y ? '' : ' '.$y;
+			return sprintf(
+				'background-position:%s%s',
+				$x,
+				$y
+			);
+		}
+
+		private function extractStyles(array $value, array $attrs):array
+		{
+			$styles = [];
+			foreach ($value as $k => $v) {
+				switch ($k) {
+					case 'border':
+						$styles = array_merge($styles, $this->getBorderStyle($v, $attrs));
+						break;
+
+					case 'color':
+						if (isset($v['background'])) {
+							$styles[] = $this->getColorStyle($v['background'], 'background-color');
+						}
+						if (isset($v['text'])) {
+							$styles[] = $this->getColorStyle($v['text']);
+						}
+						if (isset($v['gradient'])) {
+							jvbDump($v, 'Gradient');
+						}
+						break;
+
+					case 'layout':
+						$styles = array_merge($styles, $this->getLayoutStyle($v, $attrs));
+						break;
+
+					case 'typography':
+						$styles = array_merge($styles, $this->getTypographyStyle($v, $attrs));
+						break;
+
+					case 'spacing':
+						if (isset($v['blockGap'])) {
+							if (is_array($v['blockGap'])) {
+								$inner = [];
+								foreach ($v['blockGap'] as $gap) {
+									$inner[] = sprintf(
+										'var(--sp%s)',
+										$this->getPresetSpacing($gap)
+									);
+								}
+								if (!empty($inner)) {
+									$styles[] = sprintf(
+										'--gap: %s',
+										implode(' ', $inner)
+									);
+								}
+							} else {
+								$styles[] = '--gap: var(--sp'.$this->getPresetSpacing($v['blockGap']).')';
+							}
+						}
+
+						// Don't duplicate margin/padding that's handled by classes
+						// Only add specific CSS values here that wouldn't work well as classes
+						if (isset($v['margin'])) {
+							foreach ($v['margin'] as $direction => $size) {
+								if (!str_contains($size, 'var:preset')) {
+									$styles[] = 'margin-'.$direction.': '.$size;
+								}
+							}
+						}
+
+						if (isset($v['padding'])) {
+							foreach($v['padding'] as $dir => $size) {
+								if (!str_contains($size, 'var:preset')) {
+									$styles[] = 'padding-'.$dir.': '.$size;
+								}
+							}
+						}
+						break;
+
+					case 'background':
+						if (array_key_exists('backgroundImage', $v)) {
+							$data = Image::getData($v['backgroundImage']['id']);
+							if (!empty($data) && array_key_exists('tiny', $data)) {
+								$styles[] = sprintf(
+									'background-image: url(%s)',
+									$data['tiny']
+								);
+							}
+
+						}
+						break;
+
+					case 'dimensions':
+						foreach ($v as $sk => $sv) {
+							if ($sk === 'minHeight') {
+								$styles[] = sprintf(
+									'min-height: %s',
+									$sv
+								);
+							} else {
+								jvbDump('No config set for dimension '.$sk.': '.print_r($sv, true));
+							}
+						}
+
+
+						break;
+
+					case 'elements':
+						if (!empty($v)) {
+							// Generate a unique class tied to this block instance
+							$uid = 'b-'.substr(md5(serialize($attrs)), 0, 8);
+							$this->extractElementStyles($v, $uid);
+							// We need the uid added as a class — store it for getClassesAndStyles to pick up
+							static::$pendingClass[] = $uid;
+						}
+						break;
+
+					default:
+						jvbDump('No config set for '.$k.': '.print_r($v, true));
+				}
+			}
+
+			return $styles;
+		}
+			private function getBorderStyle(array $border, array $attrs):array
+			{
+				$styles = [];
+
+				if (isset($border['radius'])) {
+					$styles[] = 'border-radius: '.$border['radius'];
+				}
+
+				if (isset($border['width']) && isset($attrs['borderColor'])) {
+					$st = $border['style'] ?? 'solid';
+					$styles[] = sprintf(
+						'border: %s %s %s',
+						$border['width'],
+						$st,
+						$this->getColor($attrs['borderColor'])
+					);
+				} elseif (isset($border['width'])) {
+					$styles[] = 'border-width: '.$border['width'];
+				} elseif (isset($border['style'])) {
+					$styles[] = 'border-style: '.$border['style'];
+				}
+
+				return $styles;
+			}
+
+			private function getLayoutStyle(array $layout, array $attrs):array
+			{
+				$styles = [];
+//				jvbDump($layout);
+
+				foreach ($layout as $l => $option) {
+					switch ($l) {
+						case 'selfStretch':
+							if ($option === 'fixed' && isset($layout['selfStretchValue'])) {
+								$styles[] = 'width: '.$layout['selfStretchValue'];
+							} elseif ($option === 'fill') {
+								$styles[] = 'flex:1';
+							}
+							break;
+						default:
+							$ignore = [
+								'selfStretchValue',
+								'flexSize',
+							];
+							if (JVB_TESTING && !in_array($l, $ignore)) {
+								jvbDump($l, 'No layout style set for: ');
+							}
+//							case 'type':
+//								if ($option === 'grid' && $value['layout']['columnCount'] !== 3) {
+//									$styles[] = sprintf(
+//										'grid-template-columns: repeat(1fr, %s)',
+//										$value['layout']['columnCount']
+//									);
+//								}
+//								break;
+					}
+				}
+				return $styles;
+			}
+
+			private function getTypographyStyle(array $typography, array $attrs):array
+			{
+				$styles = [];
+
+				if (isset($typography['fontSize'])) {
+					$styles[] = 'font-size: '.$typography['fontSize'];
+				}
+
+				if (isset($typography['fontWeight'])) {
+					$styles[] = 'font-weight: '.$typography['fontWeight'];
+				}
+
+				if (isset($typography['textDecoration'])) {
+					$styles[] = 'text-decoration: '.$typography['textDecoration'];
+				}
+
+				if (isset($typography['textTransform'])) {
+					$styles[] = 'text-transform: '.$typography['textTransform'];
+				}
+
+				if (isset($typography['letterSpacing'])) {
+					$styles[] = 'letter-spacing: '.$typography['letterSpacing'];
+				}
+
+				if (isset($typography['lineHeight'])) {
+					$styles[] = 'line-height: '.$typography['lineHeight'];
+				}
+				return $styles;
+			}
+			private function extractElementStyles(array $elements, string $uid):void
+			{
+				foreach ($elements as $element => $states) {
+					$selector = match($element) {
+						'link'    => "a",
+						'heading' => "h1,h2,h3,h4,h5,h6",
+						'button'  => "button,.button",
+						default   => $element,
+					};
+
+					foreach ($states as $state => $rules) {
+						$css = [];
+						$fullSelector = str_starts_with($state, ':')
+							? ".{$uid} {$selector}{$state}"
+							: ".{$uid} {$selector}";
+
+						if (isset($rules['color']['text'])) {
+							$css[] = 'color: '.$this->getColor($rules['color']['text']);
+						}
+						if (isset($rules['color']['background'])) {
+							$css[] = 'background-color: '.$this->getColor($rules['color']['background']);
+						}
+						if (!empty($css)) {
+							static::$pendingStyles[] = $fullSelector.' { '.implode('; ', $css).' }';
+						}
+					}
+				}
+			}
+
+	public function maybeOutputCustomStyles(): string
+	{
+		if (empty(static::$pendingStyles)) return '';
+		$out = '<style>'.implode(' ', static::$pendingStyles).'</style>';
+		static::$pendingStyles = [];
+		return $out;
+	}
+
+		private function getDimRatioStyle(int $value, array $attrs):string
+		{
+			//TODO: This likely isn't working correctly
+			jvbDump($value, 'dimRatio');
+			jvbDump($attrs, 'dimRatio attrs');
+			$s = '';
+			if (!array_key_exists('overlayColor', $attrs)) {
+				$s = 'background-color: rgba(var(--base-rgb), ';
+				if ($value <= 14) {
+					$s .= 'var(--op-1));';
+				} elseif ($value <= 28) {
+					$s .= 'var(--op-2));';
+				} elseif ($value <= 42) {
+					$s .= 'var(--op-3));';
+				} elseif ($value <= 56) {
+					$s .= 'var(--op-45));';
+				} elseif ($value <= 70) {
+					$s .= 'var(--op-4));';
+				} elseif ($value <= 84) {
+					$s .= 'var(--op-5));';
+				} else {
+					$s .= 'var(--op-6));';
+				}
+			}
+			return $s;
+		}
+
+		protected function getDataset(array $attrs):array
+		{
+			$dataset = [];
+			if (array_key_exists('style', $attrs)) {
+				if (array_key_exists('background', $attrs['style'])){
+					if (array_key_exists('backgroundImage', $attrs['style']['background'])) {
+						$id = $attrs['style']['background']['backgroundImage']['id']??false;
+						if ($id) {
+							$data = Image::getData($id);
+							$dataset['bg-small'] = $data['small'];
+							$dataset['bg-med'] = $data['medium'];
+							$dataset['bg-large'] = $data['large'];
+						}
+					}
+				}
+			}
+			return $dataset;
+		}
 
     public function formatImage(int $ID = 0, string $start = 'tiny', string $replace = 'large'):string
     {
@@ -1746,4 +3219,28 @@
 		return jvbFormatImage($ID, $start, $replace);
     }
 
+	protected function checkAttrs(string $test, array $attrs):bool
+	{
+		return array_key_exists($test, $attrs) && $attrs[$test]===true;
+	}
+
+	protected function getColor(string $value):string
+	{
+		$defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']);
+		foreach ($defaults as $default) {
+			if (str_starts_with($value, $default)) {
+				return 'var(--'.$value.')';
+			}
+		}
+		return $value;
+	}
+
+	protected function counter(string $key):void
+	{
+		if (!array_key_exists($key, static::$counters)) {
+			static::$counters[$key] = 1;
+		} else {
+			static::$counters[$key]++;
+		}
+	}
 }

--
Gitblit v1.10.0