Commit ea1631610ad for woocommerce

commit ea1631610ad5cbf695f95caad88621ecdc54da76
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date:   Fri Mar 20 13:01:02 2026 -0400

    Email Editor: Add rendering support for full-width blocks (#63752)

    * Email Editor: Add rendering support for full-width blocks breaking out of template group padding

    Extends the per-block root padding distribution pattern (from #63682) to
    also handle template group padding. When a template group wrapping
    post-content has its own horizontal padding, that padding is distributed
    per-block as container-padding alongside root-padding. Alignfull blocks
    skip both padding types and span the full contentSize.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Fix PHPStan error: loosen type annotation on remove_horizontal_padding

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Add changelog entry for email editor full-width block rendering support

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Address code review: resolve preset variables in container padding, add defensive guard, improve tests

    - Fix: resolve preset variable references (var:preset|spacing|20) in
      container padding values before they reach sum_padding_values() and
      parse_number_from_string_with_pixels(), which would otherwise parse
      them as 0.
    - Add defensive guard in remove_horizontal_padding() for malformed input.
    - Use Styles_Helper::extend_block_styles() for CSS rebuild (PHPStan
      level 9 safe).
    - Add asymmetric padding test (left != right).
    - Assert width in testItDistributesBothPaddingsInSecondPass.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Fix double container padding: root-level group subsumes root padding, accumulate nested containers

    Root-level groups with own padding (e.g. WooCommerce template's 20px group)
    represent the same visual inset as the theme's root padding. Previously both
    were applied additively (root 20px + container 20px = 40px). Now the root
    group's padding subsumes root padding, and nested container paddings are
    accumulated (root group 20px + content group 10px = 30px total).

    Changes:
    - Spacing_Preprocessor: pass empty root_padding to children when root-level
      container absorbs it
    - Content_Renderer: accumulate padding from all suppressed containers in
      ancestor chain instead of returning only the first
    - Content_Renderer: detect root-level suppress to remove root padding in
      second pass
    - Content_Renderer: resolve preset references in sum_padding_values

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Revert "Fix double container padding: root-level group subsumes root padding, accumulate nested containers"

    This reverts commit 2680cd337f5cd974d1f83abf61b528a6a061864d.

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/packages/php/email-editor/changelog/add-email-editor-fullwidth-block-rendering-support b/packages/php/email-editor/changelog/add-email-editor-fullwidth-block-rendering-support
new file mode 100644
index 00000000000..15e36292706
--- /dev/null
+++ b/packages/php/email-editor/changelog/add-email-editor-fullwidth-block-rendering-support
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add rendering support for full-width blocks to break out of template group padding in emails, distributing container padding per-block so alignfull blocks can span the full contentSize.
diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php
index 6db7325d72e..f0ba2130574 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-blocks-width-preprocessor.php
@@ -57,17 +57,24 @@ class Blocks_Width_Preprocessor implements Preprocessor {
 				$layout_width -= $this->parse_number_from_string_with_pixels( $styles['spacing']['padding']['right'] ?? '0px' );
 			}

-			// Subtract root padding for blocks that will receive it as CSS padding
-			// from Content_Renderer. This ensures block widths fit inside the
-			// root padding wrapper without overflow.
+			// Subtract root padding and container padding for blocks that will
+			// receive them as CSS padding from Content_Renderer. This ensures
+			// block widths fit inside the padding wrapper without overflow.
 			if ( 'full' !== $alignment ) {
 				$layout_width -= $this->parse_number_from_string_with_pixels( $block['email_attrs']['root-padding-left'] ?? '0px' );
 				$layout_width -= $this->parse_number_from_string_with_pixels( $block['email_attrs']['root-padding-right'] ?? '0px' );
+				// Container padding may be preset references (var:preset|spacing|20).
+				$layout_width -= $this->parse_number_from_string_with_pixels( $this->resolve_preset_value( $block['email_attrs']['container-padding-left'] ?? '0px', $variables_map ) );
+				$layout_width -= $this->parse_number_from_string_with_pixels( $this->resolve_preset_value( $block['email_attrs']['container-padding-right'] ?? '0px', $variables_map ) );
 			}

 			// Resolve block padding — may be preset references like var:preset|spacing|20.
-			$block_padding_left  = $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['left'] ?? '0px', $variables_map );
-			$block_padding_right = $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['right'] ?? '0px', $variables_map );
+			// When suppress-horizontal-padding is set, the block's horizontal padding
+			// has been distributed per-block by the Spacing_Preprocessor. Zero it out
+			// so children get the full available width.
+			$suppress_h_padding  = ! empty( $block['email_attrs']['suppress-horizontal-padding'] );
+			$block_padding_left  = $suppress_h_padding ? '0px' : $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['left'] ?? '0px', $variables_map );
+			$block_padding_right = $suppress_h_padding ? '0px' : $this->resolve_preset_value( $block['attrs']['style']['spacing']['padding']['right'] ?? '0px', $variables_map );

 			$width_input = $block['attrs']['width'] ?? '100%';
 			// Currently we support only % and px units in case only the number is provided we assume it's %
diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php
index 14ee33b0810..cf4134f0995 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/Preprocessors/class-spacing-preprocessor.php
@@ -29,11 +29,44 @@ class Spacing_Preprocessor implements Preprocessor {
 	 * @return array
 	 */
 	public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
-		$root_padding  = $this->get_root_padding( $styles );
-		$parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null, $root_padding );
+		$root_padding      = $this->get_root_padding( $styles );
+		$container_padding = $styles['__container_padding'] ?? array();
+		$parsed_blocks     = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null, $root_padding, false, $container_padding );
 		return $parsed_blocks;
 	}

+	/**
+	 * Extract and validate horizontal padding from a block's style attributes.
+	 *
+	 * @param array $block The block to extract padding from.
+	 * @return array Padding with 'left' and 'right' keys, or empty array if invalid/absent.
+	 */
+	private function get_block_horizontal_padding( array $block ): array {
+		$padding   = $block['attrs']['style']['spacing']['padding'] ?? array();
+		$has_left  = isset( $padding['left'] );
+		$has_right = isset( $padding['right'] );
+
+		if ( ! $has_left && ! $has_right ) {
+			return array();
+		}
+
+		$left  = $has_left ? $padding['left'] : '0px';
+		$right = $has_right ? $padding['right'] : '0px';
+
+		if ( ! is_string( $left ) || ! is_string( $right ) || preg_match( '/[<>"\']/', $left . $right ) ) {
+			return array();
+		}
+
+		if ( $this->is_zero_value( $left ) && $this->is_zero_value( $right ) ) {
+			return array();
+		}
+
+		return array(
+			'left'  => $left,
+			'right' => $right,
+		);
+	}
+
 	/**
 	 * Container block names that delegate root padding to their children
 	 * instead of receiving it themselves.
@@ -49,6 +82,12 @@ class Spacing_Preprocessor implements Preprocessor {
 	 * padding to their children instead of taking it themselves. This enables
 	 * alignfull blocks to skip root padding and span the full email width.
 	 *
+	 * Container padding works similarly: when a template group wrapping post-content
+	 * has its own horizontal padding, that padding is distributed per-block alongside
+	 * root padding. Alignfull blocks skip both padding types and span the full
+	 * contentSize. The template group gets a suppress-horizontal-padding flag so its
+	 * renderer omits horizontal padding from its own CSS output.
+	 *
 	 * Blocks fall into three categories for root padding:
 	 * - Zero padding (has_zero_padding): skip root padding entirely — edge-to-edge intent.
 	 * - Non-zero explicit padding (has_own_padding, !has_zero_padding): receive root padding
@@ -61,9 +100,10 @@ class Spacing_Preprocessor implements Preprocessor {
 	 * @param array|null $parent_block Parent block.
 	 * @param array      $root_padding Root horizontal padding with 'left' and 'right' keys.
 	 * @param bool       $apply_root_padding Whether this block should receive root padding (delegated by parent container).
+	 * @param array      $container_padding Container horizontal padding with 'left' and 'right' keys.
 	 * @return array
 	 */
-	private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null, array $root_padding = array(), bool $apply_root_padding = false ): array {
+	private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null, array $root_padding = array(), bool $apply_root_padding = false, array $container_padding = array() ): array {
 		foreach ( $parsed_blocks as $key => $block ) {
 			$block_name        = $block['blockName'] ?? '';
 			$parent_block_name = $parent_block['blockName'] ?? '';
@@ -102,7 +142,7 @@ class Spacing_Preprocessor implements Preprocessor {
 			$alignment          = $block['attrs']['align'] ?? null;
 			$has_zero_padding   = $this->has_zero_horizontal_padding( $block );
 			$has_own_padding    = $this->has_explicit_horizontal_padding( $block );
-			$wraps_post_content = $apply_root_padding && $is_container && ! $has_own_padding && $this->contains_post_content( $block );
+			$wraps_post_content = $apply_root_padding && $is_container && $this->contains_post_content( $block );
 			$should_apply       = $apply_root_padding || ( $is_root_level && ! $is_container ) || ( $is_root_level && $is_container && $has_own_padding );

 			$post_content_block_names = $this->get_post_content_block_names();
@@ -111,6 +151,13 @@ class Spacing_Preprocessor implements Preprocessor {
 				$block['email_attrs']['root-padding-right'] = $root_padding['right'];
 			}

+			// Apply container padding (from template group wrapping post-content).
+			// Alignfull blocks skip both root and container padding.
+			if ( $should_apply && ! $has_zero_padding && 'full' !== $alignment && ! in_array( $block_name, $post_content_block_names, true ) && ! $wraps_post_content && ! empty( $container_padding ) ) {
+				$block['email_attrs']['container-padding-left']  = $container_padding['left'];
+				$block['email_attrs']['container-padding-right'] = $container_padding['right'];
+			}
+
 			// Determine whether children should receive root padding delegation.
 			// Root-level containers delegate to their children.
 			// Post-content blocks that received delegation also pass it through.
@@ -118,16 +165,39 @@ class Spacing_Preprocessor implements Preprocessor {
 			// so that user blocks inside post-content get padding individually.
 			// Containers with explicit horizontal padding stop delegation — they
 			// manage their own layout.
-			$children_apply = false;
+			$children_apply         = false;
+			$children_container_pad = $container_padding;
 			if ( $is_root_level && $is_container && ! $has_own_padding ) {
 				$children_apply = true;
 			} elseif ( $apply_root_padding && in_array( $block_name, $post_content_block_names, true ) ) {
 				$children_apply = true;
 			} elseif ( $wraps_post_content ) {
 				$children_apply = true;
+
+				// When a container wrapping post-content has its own non-zero
+				// horizontal padding, distribute it as container-padding to
+				// descendant blocks and suppress the container's own CSS padding.
+				$block_padding = $this->get_block_horizontal_padding( $block );
+				if ( ! empty( $block_padding ) ) {
+					$children_container_pad                              = $block_padding;
+					$block['email_attrs']['suppress-horizontal-padding'] = true;
+				}
+			} elseif ( $is_root_level && $is_container && $has_own_padding && ! $has_zero_padding && $this->contains_post_content( $block ) ) {
+				// Root-level container with own padding that wraps post-content:
+				// distribute its padding as container-padding and suppress its own CSS.
+				$children_apply = true;
+				$block_padding  = $this->get_block_horizontal_padding( $block );
+				if ( ! empty( $block_padding ) ) {
+					$children_container_pad                              = $block_padding;
+					$block['email_attrs']['suppress-horizontal-padding'] = true;
+				}
+
+				// This container also should not receive root padding itself
+				// (it delegates everything to children).
+				unset( $block['email_attrs']['root-padding-left'], $block['email_attrs']['root-padding-right'] );
 			}

-			$block['innerBlocks']  = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block, $root_padding, $children_apply );
+			$block['innerBlocks']  = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block, $root_padding, $children_apply, $children_container_pad );
 			$parsed_blocks[ $key ] = $block;
 		}

diff --git a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
index 1858f51b740..43211c763e9 100644
--- a/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
+++ b/packages/php/email-editor/src/Engine/Renderer/ContentRenderer/class-content-renderer.php
@@ -108,6 +108,16 @@ class Content_Renderer {
 	 */
 	private ?string $post_content_width = null;

+	/**
+	 * Container padding from the template group wrapping post-content.
+	 *
+	 * Stored during the first preprocessing pass and passed to user blocks
+	 * in the second pass so they receive the container padding per-block.
+	 *
+	 * @var array{left?: string, right?: string}
+	 */
+	private array $container_padding = array();
+
 	/**
 	 * CSS inliner
 	 *
@@ -242,13 +252,20 @@ class Content_Renderer {
 			if ( $post_content_num < $content_size_num - 0.01 ) {
 				unset( $styles['spacing']['padding']['left'], $styles['spacing']['padding']['right'] );
 			}
+
+			// Pass container padding from the first pass so the
+			// Spacing_Preprocessor can distribute it to user blocks.
+			if ( ! empty( $this->container_padding ) ) {
+				$styles['__container_padding'] = $this->container_padding;
+			}
 		}

 		$result = $this->process_manager->preprocess( $parsed_blocks, $layout, $styles );

-		// After the first pass: find the post-content block's width.
+		// After the first pass: find the post-content block's width and container padding.
 		if ( null === $this->post_content_width ) {
 			$this->post_content_width = $this->find_post_content_width( $result );
+			$this->container_padding  = $this->find_container_padding( $result );
 		}

 		return $result;
@@ -284,6 +301,43 @@ class Content_Renderer {
 		return null;
 	}

+	/**
+	 * Find the container padding from blocks with suppress-horizontal-padding flag.
+	 *
+	 * Searches the preprocessed template blocks for a container that wraps
+	 * post-content and had its horizontal padding distributed per-block.
+	 *
+	 * @param array $blocks Preprocessed blocks.
+	 * @return array{left?: string, right?: string} Container padding values, or empty array.
+	 */
+	private function find_container_padding( array $blocks ): array {
+		$variables_map = $this->theme_controller->get_variables_values_map();
+
+		foreach ( $blocks as $block ) {
+			$email_attrs = $block['email_attrs'] ?? array();
+			if ( ! empty( $email_attrs['suppress-horizontal-padding'] ) ) {
+				$padding = $block['attrs']['style']['spacing']['padding'] ?? array();
+				$result  = array();
+				if ( isset( $padding['left'] ) && is_string( $padding['left'] ) ) {
+					$result['left'] = $this->resolve_preset_padding( $padding['left'], $variables_map );
+				}
+				if ( isset( $padding['right'] ) && is_string( $padding['right'] ) ) {
+					$result['right'] = $this->resolve_preset_padding( $padding['right'], $variables_map );
+				}
+				if ( ! empty( $result ) ) {
+					return $result;
+				}
+			}
+			if ( ! empty( $block['innerBlocks'] ) ) {
+				$found = $this->find_container_padding( $block['innerBlocks'] );
+				if ( ! empty( $found ) ) {
+					return $found;
+				}
+			}
+		}
+		return array();
+	}
+
 	/**
 	 * Renders block
 	 * Translates block's HTML to HTML suitable for email clients. The method is intended as a callback for 'render_block' filter.
@@ -345,24 +399,35 @@ class Content_Renderer {
 	}

 	/**
-	 * Wrap block output with root horizontal padding.
+	 * Wrap block output with horizontal padding (root + container).
 	 *
 	 * Root padding is distributed by the Spacing_Preprocessor from the outer
-	 * email container to individual blocks. This method applies it uniformly
-	 * to all blocks regardless of whether they use Abstract_Block_Renderer
-	 * or a custom render_email_callback.
+	 * email container to individual blocks. Container padding comes from
+	 * template groups wrapping post-content. Both are combined into a single
+	 * CSS padding wrapper. This method applies padding uniformly to all blocks
+	 * regardless of whether they use Abstract_Block_Renderer or a custom
+	 * render_email_callback.
 	 *
 	 * @param string $content The rendered block content.
 	 * @param array  $email_attrs The email attributes from the parsed block.
-	 * @return string The content wrapped with horizontal padding, or unchanged if no root padding.
+	 * @return string The content wrapped with horizontal padding, or unchanged if no padding.
 	 */
 	private function add_root_horizontal_padding( string $content, array $email_attrs ): string {
+		$padding_left  = $this->sum_padding_values(
+			$email_attrs['root-padding-left'] ?? null,
+			$email_attrs['container-padding-left'] ?? null
+		);
+		$padding_right = $this->sum_padding_values(
+			$email_attrs['root-padding-right'] ?? null,
+			$email_attrs['container-padding-right'] ?? null
+		);
+
 		$css_attrs = array();
-		if ( isset( $email_attrs['root-padding-left'] ) ) {
-			$css_attrs['padding-left'] = $email_attrs['root-padding-left'];
+		if ( $padding_left > 0 ) {
+			$css_attrs['padding-left'] = $padding_left . 'px';
 		}
-		if ( isset( $email_attrs['root-padding-right'] ) ) {
-			$css_attrs['padding-right'] = $email_attrs['root-padding-right'];
+		if ( $padding_right > 0 ) {
+			$css_attrs['padding-right'] = $padding_right . 'px';
 		}
 		if ( empty( $css_attrs ) ) {
 			return $content;
@@ -391,6 +456,43 @@ class Content_Renderer {
 		return Table_Wrapper_Helper::render_outlook_table_wrapper( $div_content, $table_attrs, $cell_attrs );
 	}

+	/**
+	 * Sum two CSS pixel padding values.
+	 *
+	 * @param string|null $value1 First padding value (e.g., '20px').
+	 * @param string|null $value2 Second padding value (e.g., '10px').
+	 * @return float The sum in pixels.
+	 */
+	private function sum_padding_values( ?string $value1, ?string $value2 ): float {
+		$sum = 0.0;
+		if ( null !== $value1 ) {
+			$sum += (float) str_replace( 'px', '', $value1 );
+		}
+		if ( null !== $value2 ) {
+			$sum += (float) str_replace( 'px', '', $value2 );
+		}
+		return $sum;
+	}
+
+	/**
+	 * Resolve a CSS value that may contain a preset variable reference.
+	 *
+	 * Block attributes store padding as preset references like
+	 * "var:preset|spacing|20" which resolve to actual pixel values.
+	 *
+	 * @param string $value The CSS value, possibly a preset reference.
+	 * @param array  $variables_map Map of CSS variable names to resolved values.
+	 * @return string The resolved value (e.g. "8px") or the original value.
+	 */
+	private function resolve_preset_padding( string $value, array $variables_map ): string {
+		if ( strpos( $value, 'var:preset|' ) !== 0 ) {
+			return $value;
+		}
+
+		$css_var_name = '--wp--' . str_replace( '|', '--', str_replace( 'var:', '', $value ) );
+		return $variables_map[ $css_var_name ] ?? $value;
+	}
+
 	/**
 	 * Set template globals
 	 *
@@ -424,6 +526,7 @@ class Content_Renderer {
 		remove_filter( 'woocommerce_email_blocks_renderer_parsed_blocks', array( $this, 'preprocess_parsed_blocks' ) );

 		$this->post_content_width = null;
+		$this->container_padding  = array();

 		// Restore the original core/post-content render callback.
 		// Note: We always restore it, even if it was null originally.
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php
index 83cc81211a8..10da2d431ea 100644
--- a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-group.php
@@ -65,7 +65,13 @@ class Group extends Abstract_Block_Renderer {
 		);

 		// Padding properties need to be added to the table cell.
+		// When suppress-horizontal-padding is set, the container's horizontal
+		// padding has been distributed per-block by the Spacing_Preprocessor.
+		// Only vertical padding is kept on the group's own CSS output.
 		$cell_styles = Styles_Helper::get_block_styles( $block_attributes, $rendering_context, array( 'padding' ) );
+		if ( ! empty( $parsed_block['email_attrs']['suppress-horizontal-padding'] ) ) {
+			$cell_styles = $this->remove_horizontal_padding( $cell_styles );
+		}

 		$table_attrs = array(
 			'class' => 'email-block-group ' . $original_classname,
@@ -81,4 +87,28 @@ class Group extends Abstract_Block_Renderer {

 		return Table_Wrapper_Helper::render_table_wrapper( '{group_content}', $table_attrs, $cell_attrs );
 	}
+
+	/**
+	 * Remove horizontal padding properties from compiled cell styles.
+	 *
+	 * Used when suppress-horizontal-padding is set to prevent the group
+	 * from applying its own horizontal padding (which has been distributed
+	 * per-block by the Spacing_Preprocessor).
+	 *
+	 * @param array $cell_styles The compiled cell styles from Styles_Helper::get_block_styles().
+	 * @return array Styles with horizontal padding removed.
+	 */
+	private function remove_horizontal_padding( array $cell_styles ): array {
+		if ( ! isset( $cell_styles['declarations'] ) || ! is_array( $cell_styles['declarations'] ) ) {
+			return $cell_styles;
+		}
+
+		unset(
+			$cell_styles['declarations']['padding-left'],
+			$cell_styles['declarations']['padding-right']
+		);
+
+		// Rebuild CSS string from remaining declarations.
+		return Styles_Helper::extend_block_styles( $cell_styles, array() );
+	}
 }
diff --git a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
index 60e6d995392..f5eee42d9f1 100644
--- a/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Renderer/ContentRenderer/Content_Renderer_Test.php
@@ -209,12 +209,14 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
 	}

 	/**
-	 * Test preprocess_parsed_blocks skips root padding in second pass when
-	 * a container above post-content absorbed it (WooCommerce template pattern).
+	 * Test preprocess_parsed_blocks distributes both root and container padding
+	 * in second pass when a container above post-content has own padding
+	 * (WooCommerce template pattern).
 	 */
-	public function testItSkipsRootPaddingInSecondPassWhenAbsorbed(): void {
+	public function testItDistributesBothPaddingsInSecondPassWhenContainerHasPadding(): void {
 		// First pass: template blocks with a group wrapping post-content.
-		// The group has own padding so it absorbs root padding and stops delegation.
+		// The group has own padding — it distributes both root padding and
+		// container padding per-block to user blocks.
 		$template_blocks = array(
 			array(
 				'blockName'   => 'core/group',
@@ -240,9 +242,13 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {

 		$first_result = $this->renderer->preprocess_parsed_blocks( $template_blocks );

-		// post-content should have a narrower width than contentSize
-		// (root padding + group padding subtracted).
-		$post_content = $first_result[0]['innerBlocks'][0];
+		// Root group should have suppress-horizontal-padding flag.
+		$root_group = $first_result[0];
+		$this->assertTrue( $root_group['email_attrs']['suppress-horizontal-padding'] );
+
+		// post-content should have full contentSize width (padding is distributed
+		// per-block, not subtracted from the group).
+		$post_content = $root_group['innerBlocks'][0];
 		$this->assertArrayHasKey( 'width', $post_content['email_attrs'] );

 		// Second pass: user blocks (simulating post-content rendering).
@@ -256,9 +262,18 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {

 		$second_result = $this->renderer->preprocess_parsed_blocks( $user_blocks );

-		// User blocks should NOT have root padding (it was absorbed upstream).
-		$this->assertArrayNotHasKey( 'root-padding-left', $second_result[0]['email_attrs'] );
-		$this->assertArrayNotHasKey( 'root-padding-right', $second_result[0]['email_attrs'] );
+		// User blocks should have root padding (delegated, not absorbed).
+		$this->assertArrayHasKey( 'root-padding-left', $second_result[0]['email_attrs'] );
+		$this->assertArrayHasKey( 'root-padding-right', $second_result[0]['email_attrs'] );
+
+		// User blocks should also have container padding from the template group.
+		$this->assertArrayHasKey( 'container-padding-left', $second_result[0]['email_attrs'] );
+		$this->assertArrayHasKey( 'container-padding-right', $second_result[0]['email_attrs'] );
+		$this->assertEquals( '20px', $second_result[0]['email_attrs']['container-padding-left'] );
+		$this->assertEquals( '20px', $second_result[0]['email_attrs']['container-padding-right'] );
+
+		// Width should account for both root and container padding.
+		$this->assertArrayHasKey( 'width', $second_result[0]['email_attrs'] );
 	}

 	/**
@@ -300,6 +315,98 @@ class Content_Renderer_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertArrayHasKey( 'root-padding-right', $second_result[0]['email_attrs'] );
 	}

+	/**
+	 * Test render_block applies combined root + container horizontal padding
+	 */
+	public function testItAppliesCombinedRootAndContainerPadding(): void {
+		register_block_type(
+			'test/combined-padding-block',
+			array(
+				'render_email_callback' => function () {
+					return '<p>combined padding</p>';
+				},
+			)
+		);
+
+		$result = $this->renderer->render_block(
+			'content',
+			array(
+				'blockName'   => 'test/combined-padding-block',
+				'email_attrs' => array(
+					'root-padding-left'       => '10px',
+					'root-padding-right'      => '10px',
+					'container-padding-left'  => '20px',
+					'container-padding-right' => '20px',
+				),
+			)
+		);
+
+		$this->assertStringContainsString( 'combined padding', $result );
+		$this->assertStringContainsString( 'email-root-padding', $result );
+		// Combined: 10 + 20 = 30px each side.
+		$this->assertStringContainsString( 'padding-left:30px', $result );
+		$this->assertStringContainsString( 'padding-right:30px', $result );
+		\WP_Block_Type_Registry::get_instance()->unregister( 'test/combined-padding-block' );
+	}
+
+	/**
+	 * Test render_block skips padding wrapper for alignfull blocks (no padding attrs)
+	 */
+	public function testItSkipsPaddingForAlignfullBlocks(): void {
+		register_block_type(
+			'test/alignfull-block',
+			array(
+				'render_email_callback' => function () {
+					return '<p>full width content</p>';
+				},
+			)
+		);
+
+		$result = $this->renderer->render_block(
+			'content',
+			array(
+				'blockName'   => 'test/alignfull-block',
+				'email_attrs' => array(),
+			)
+		);
+
+		$this->assertEquals( '<p>full width content</p>', $result );
+		$this->assertStringNotContainsString( 'email-root-padding', $result );
+		\WP_Block_Type_Registry::get_instance()->unregister( 'test/alignfull-block' );
+	}
+
+	/**
+	 * Test render_block applies asymmetric combined padding correctly
+	 */
+	public function testItAppliesAsymmetricCombinedPadding(): void {
+		register_block_type(
+			'test/asymmetric-padding-block',
+			array(
+				'render_email_callback' => function () {
+					return '<p>asymmetric</p>';
+				},
+			)
+		);
+
+		$result = $this->renderer->render_block(
+			'content',
+			array(
+				'blockName'   => 'test/asymmetric-padding-block',
+				'email_attrs' => array(
+					'root-padding-left'       => '10px',
+					'root-padding-right'      => '15px',
+					'container-padding-left'  => '20px',
+					'container-padding-right' => '25px',
+				),
+			)
+		);
+
+		// Combined: left = 10 + 20 = 30px, right = 15 + 25 = 40px.
+		$this->assertStringContainsString( 'padding-left:30px', $result );
+		$this->assertStringContainsString( 'padding-right:40px', $result );
+		\WP_Block_Type_Registry::get_instance()->unregister( 'test/asymmetric-padding-block' );
+	}
+
 	/**
 	 * Get the value of the style attribute for a given tag in the HTML.
 	 *
diff --git a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php
index d503859c6bc..d02a93d3405 100644
--- a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php
+++ b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Blocks_Width_Preprocessor_Test.php
@@ -582,6 +582,54 @@ class Blocks_Width_Preprocessor_Test extends \Email_Editor_Unit_Test {
 		$this->assertEquals( '225px', $image_block['email_attrs']['width'] );
 	}

+	/**
+	 * Test it subtracts container padding from block widths
+	 */
+	public function testItSubtractsContainerPaddingFromEmailAttrs(): void {
+		$blocks = array(
+			// Block with both root and container padding.
+			array(
+				'blockName'   => 'core/paragraph',
+				'attrs'       => array(),
+				'email_attrs' => array(
+					'root-padding-left'       => '10px',
+					'root-padding-right'      => '10px',
+					'container-padding-left'  => '20px',
+					'container-padding-right' => '20px',
+				),
+				'innerBlocks' => array(),
+			),
+			// Alignfull block: container padding should NOT be subtracted.
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(
+					'align' => 'full',
+				),
+				'email_attrs' => array(),
+				'innerBlocks' => array(),
+			),
+			// Block with only container padding.
+			array(
+				'blockName'   => 'core/paragraph',
+				'attrs'       => array(),
+				'email_attrs' => array(
+					'container-padding-left'  => '15px',
+					'container-padding-right' => '15px',
+				),
+				'innerBlocks' => array(),
+			),
+		);
+		$result = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+
+		$this->assertCount( 3, $result );
+		// 660 - 10 - 10 (root) - 20 - 20 (container) = 600.
+		$this->assertEquals( '600px', $result[0]['email_attrs']['width'] );
+		// Full-width: 660px (nothing subtracted).
+		$this->assertEquals( '660px', $result[1]['email_attrs']['width'] );
+		// 660 - 15 - 15 (container only) = 630.
+		$this->assertEquals( '630px', $result[2]['email_attrs']['width'] );
+	}
+
 	/**
 	 * Test it handles non-string width values
 	 */
diff --git a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php
index 267f1f9cdf8..f8c0c5c4bf7 100644
--- a/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php
+++ b/packages/php/email-editor/tests/unit/Engine/Renderer/ContentRenderer/Preprocessors/Spacing_Preprocessor_Test.php
@@ -676,6 +676,205 @@ class Spacing_Preprocessor_Test extends \Email_Editor_Unit_Test {
 		$this->assertEquals( '10px', $plain_paragraph['email_attrs']['root-padding-right'] );
 	}

+	/**
+	 * Test root-level group with own padding wrapping post-content distributes container padding
+	 */
+	public function testItDistributesContainerPaddingFromRootGroupWrappingPostContent(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(
+					'style' => array(
+						'spacing' => array(
+							'padding' => array(
+								'left'   => '20px',
+								'right'  => '20px',
+								'top'    => '15px',
+								'bottom' => '15px',
+							),
+						),
+					),
+				),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/post-content',
+						'attrs'       => array(),
+						'innerBlocks' => array(
+							array(
+								'blockName'   => 'core/paragraph',
+								'attrs'       => array(),
+								'innerBlocks' => array(),
+							),
+							array(
+								'blockName'   => 'core/group',
+								'attrs'       => array( 'align' => 'full' ),
+								'innerBlocks' => array(),
+							),
+						),
+					),
+				),
+			),
+		);
+
+		$result       = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$root_group   = $result[0];
+		$post_content = $root_group['innerBlocks'][0];
+		$paragraph    = $post_content['innerBlocks'][0];
+		$alignfull    = $post_content['innerBlocks'][1];
+
+		// Root group should have suppress-horizontal-padding flag.
+		$this->assertTrue( $root_group['email_attrs']['suppress-horizontal-padding'] );
+
+		// Root group should NOT have root padding (delegates everything).
+		$this->assertArrayNotHasKey( 'root-padding-left', $root_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'root-padding-right', $root_group['email_attrs'] );
+
+		// Post-content should not get container padding (it's a pass-through).
+		$this->assertArrayNotHasKey( 'container-padding-left', $post_content['email_attrs'] );
+		$this->assertArrayNotHasKey( 'container-padding-right', $post_content['email_attrs'] );
+
+		// Normal paragraph should get both root and container padding.
+		$this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-left'] );
+		$this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-right'] );
+		$this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-left'] );
+		$this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-right'] );
+
+		// Alignfull block should skip BOTH root and container padding.
+		$this->assertArrayNotHasKey( 'root-padding-left', $alignfull['email_attrs'] );
+		$this->assertArrayNotHasKey( 'root-padding-right', $alignfull['email_attrs'] );
+		$this->assertArrayNotHasKey( 'container-padding-left', $alignfull['email_attrs'] );
+		$this->assertArrayNotHasKey( 'container-padding-right', $alignfull['email_attrs'] );
+	}
+
+	/**
+	 * Test container padding is distributed from nested group wrapping post-content
+	 */
+	public function testItDistributesContainerPaddingFromNestedGroupWrappingPostContent(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/group',
+						'attrs'       => array(
+							'style' => array(
+								'spacing' => array(
+									'padding' => array(
+										'left'  => '25px',
+										'right' => '25px',
+									),
+								),
+							),
+						),
+						'innerBlocks' => array(
+							array(
+								'blockName'   => 'core/post-content',
+								'attrs'       => array(),
+								'innerBlocks' => array(
+									array(
+										'blockName'   => 'core/paragraph',
+										'attrs'       => array(),
+										'innerBlocks' => array(),
+									),
+								),
+							),
+						),
+					),
+				),
+			),
+		);
+
+		$result        = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$root_group    = $result[0];
+		$content_group = $root_group['innerBlocks'][0];
+		$paragraph     = $content_group['innerBlocks'][0]['innerBlocks'][0];
+
+		// Content group wrapping post-content should have suppress flag.
+		$this->assertTrue( $content_group['email_attrs']['suppress-horizontal-padding'] );
+
+		// Paragraph inside post-content should get container padding.
+		$this->assertEquals( '25px', $paragraph['email_attrs']['container-padding-left'] );
+		$this->assertEquals( '25px', $paragraph['email_attrs']['container-padding-right'] );
+	}
+
+	/**
+	 * Test container padding is passed from styles (second pass) to user blocks
+	 */
+	public function testItAppliesContainerPaddingFromStyles(): void {
+		$styles                        = $this->styles;
+		$styles['__container_padding'] = array(
+			'left'  => '20px',
+			'right' => '20px',
+		);
+
+		// Simulate second pass: user blocks at top level (as post-content renders them).
+		$blocks = array(
+			array(
+				'blockName'   => 'core/paragraph',
+				'attrs'       => array(),
+				'innerBlocks' => array(),
+			),
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array( 'align' => 'full' ),
+				'innerBlocks' => array(),
+			),
+		);
+
+		$result    = $this->preprocessor->preprocess( $blocks, $this->layout, $styles );
+		$paragraph = $result[0];
+		$alignfull = $result[1];
+
+		// Normal block gets container padding.
+		$this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-left'] );
+		$this->assertEquals( '20px', $paragraph['email_attrs']['container-padding-right'] );
+
+		// Alignfull block skips container padding.
+		$this->assertArrayNotHasKey( 'container-padding-left', $alignfull['email_attrs'] );
+		$this->assertArrayNotHasKey( 'container-padding-right', $alignfull['email_attrs'] );
+	}
+
+	/**
+	 * Test template group without own padding does NOT set container padding
+	 */
+	public function testItDoesNotSetContainerPaddingWhenGroupHasNoPadding(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/post-content',
+						'attrs'       => array(),
+						'innerBlocks' => array(
+							array(
+								'blockName'   => 'core/paragraph',
+								'attrs'       => array(),
+								'innerBlocks' => array(),
+							),
+						),
+					),
+				),
+			),
+		);
+
+		$result    = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$group     = $result[0];
+		$paragraph = $group['innerBlocks'][0]['innerBlocks'][0];
+
+		// Group should NOT have suppress flag.
+		$this->assertArrayNotHasKey( 'suppress-horizontal-padding', $group['email_attrs'] );
+
+		// Paragraph should NOT have container padding.
+		$this->assertArrayNotHasKey( 'container-padding-left', $paragraph['email_attrs'] );
+		$this->assertArrayNotHasKey( 'container-padding-right', $paragraph['email_attrs'] );
+
+		// Paragraph should still get root padding.
+		$this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-left'] );
+		$this->assertEquals( '10px', $paragraph['email_attrs']['root-padding-right'] );
+	}
+
 	/**
 	 * Test it rejects malicious values in blockGap
 	 */