Commit 60b7a2159da for woocommerce

commit 60b7a2159da2e591c7774e6c148bfffc41993195
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date:   Thu Mar 12 11:22:33 2026 -0400

    Email Editor: Distribute root horizontal padding to block wrappers (#63359)

    * Email Editor: Distribute root horizontal padding to individual block wrappers

    Move horizontal padding from the outer email_layout_wrapper to individual
    block spacer wrappers via the Spacing_Preprocessor. This is a foundational
    change for future alignfull block support — blocks marked as full-width can
    skip root padding to span the full email width.

    Container blocks (groups, post-content) at the root level delegate padding
    to their children instead of receiving it themselves. Groups that wrap
    post-content are transparent, delegating further so user blocks inside
    post-content get individual padding. The rendered email looks identical
    after this change.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Sanitize root padding values in Spacing_Preprocessor

    Extract root padding into a get_root_padding() helper that validates
    against injection characters (<, >, ", ') matching the existing guard
    in get_columns_block_gap() for defense-in-depth consistency.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Update integration test for distributed root padding

    The wrapper no longer has horizontal padding — it's distributed to
    individual block wrappers. Update testItInlinesWrappersStyles to
    assert the absence of padding-left/right on the wrapper.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Skip root padding when theme defines no horizontal padding

    Return empty array from get_root_padding() when neither left nor right
    key exists in theme styles, avoiding unnecessary 0px values on every
    block wrapper.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Make contains_post_content() recursive for deeply nested templates

    Search through container blocks recursively so that template structures
    like group → group → group → post-content are handled correctly. Without
    this, intermediate groups would absorb root padding and stop delegation,
    leaving user blocks inside post-content without padding.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Respect explicit block padding; make CONTAINER_BLOCKS private

    Blocks that explicitly define their own horizontal padding (e.g., a
    full-width banner with 0px padding or a group with 40px padding) now
    skip root padding. Containers with explicit padding also stop
    delegation so their children follow the container's own layout. This
    matches the editor behavior where edge-to-edge blocks have no margins.

    Also marks the CONTAINER_BLOCKS constant as private per review feedback.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

diff --git a/packages/php/email-editor/changelog/email-editor-distribute-root-padding b/packages/php/email-editor/changelog/email-editor-distribute-root-padding
new file mode 100644
index 00000000000..06b55a46d5c
--- /dev/null
+++ b/packages/php/email-editor/changelog/email-editor-distribute-root-padding
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Distribute root horizontal padding to individual block wrappers for future alignfull support.
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 f56d909c433..4ffa638fda3 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
@@ -22,20 +22,37 @@ class Spacing_Preprocessor implements Preprocessor {
 	 * @return array
 	 */
 	public function preprocess( array $parsed_blocks, array $layout, array $styles ): array {
-		$parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null );
+		$root_padding = $this->get_root_padding( $styles );
+
+		$parsed_blocks = $this->add_block_gaps( $parsed_blocks, $styles['spacing']['blockGap'] ?? '', null, $root_padding );
 		return $parsed_blocks;
 	}

 	/**
-	 * Adds margin-top to blocks that are not first or last in the columns block.
+	 * Container block names that delegate root padding to their children
+	 * instead of receiving it themselves.
+	 */
+	private const CONTAINER_BLOCKS = array( 'core/group', 'core/post-content' );
+
+	/**
+	 * Adds spacing to blocks: margin-top for vertical gaps, horizontal padding for
+	 * column gaps, and root padding for children of root-level containers.
+	 *
+	 * Root padding is distributed from the outer email wrapper to individual block
+	 * wrappers. Container blocks (groups, post-content) at the root level delegate
+	 * padding to their children instead of taking it themselves. This enables
+	 * alignfull blocks to skip root padding and span the full email width.
 	 *
 	 * @param array      $parsed_blocks Parsed blocks.
 	 * @param string     $gap Gap.
 	 * @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).
 	 * @return array
 	 */
-	private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null ): array {
+	private function add_block_gaps( array $parsed_blocks, string $gap = '', $parent_block = null, array $root_padding = array(), bool $apply_root_padding = false ): array {
 		foreach ( $parsed_blocks as $key => $block ) {
+			$block_name        = $block['blockName'] ?? '';
 			$parent_block_name = $parent_block['blockName'] ?? '';
 			// Ensure that email_attrs are set.
 			$block['email_attrs'] = $block['email_attrs'] ?? array();
@@ -57,13 +74,120 @@ class Spacing_Preprocessor implements Preprocessor {
 				}
 			}

-			$block['innerBlocks']  = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block );
+			// Distribute root horizontal padding.
+			// Container blocks (group, post-content) at root level do NOT get padding;
+			// they delegate it to their children. Non-container blocks at root level
+			// (e.g., columns, paragraph) get padding directly.
+			// Blocks flagged with $apply_root_padding (children of root containers)
+			// also get padding, unless they are post-content or a container wrapping
+			// post-content (both delegate further down the tree).
+			// Blocks that explicitly define their own horizontal padding are managing
+			// their own layout and skip root padding entirely. Containers with explicit
+			// padding also stop delegation so their children follow the container's padding.
+			$is_root_level      = null === $parent_block;
+			$is_container       = in_array( $block_name, self::CONTAINER_BLOCKS, true );
+			$alignment          = $block['attrs']['align'] ?? null;
+			$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 );
+			$should_apply       = $apply_root_padding || ( $is_root_level && ! $is_container );
+
+			if ( $should_apply && ! $has_own_padding && 'full' !== $alignment && 'core/post-content' !== $block_name && ! $wraps_post_content && ! empty( $root_padding ) ) {
+				$block['email_attrs']['padding-left']  = $root_padding['left'];
+				$block['email_attrs']['padding-right'] = $root_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.
+			// Containers wrapping post-content that received delegation also delegate,
+			// 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;
+			if ( $is_root_level && $is_container && ! $has_own_padding ) {
+				$children_apply = true;
+			} elseif ( $apply_root_padding && 'core/post-content' === $block_name ) {
+				$children_apply = true;
+			} elseif ( $wraps_post_content ) {
+				$children_apply = true;
+			}
+
+			$block['innerBlocks']  = $this->add_block_gaps( $block['innerBlocks'] ?? array(), $gap, $block, $root_padding, $children_apply );
 			$parsed_blocks[ $key ] = $block;
 		}

 		return $parsed_blocks;
 	}

+	/**
+	 * Checks whether a block contains a core/post-content descendant.
+	 *
+	 * Searches recursively through container blocks (groups) so that
+	 * deeply nested template structures like group → group → post-content
+	 * are handled correctly.
+	 *
+	 * @param array $block The block to check.
+	 * @return bool True if the block has a post-content descendant.
+	 */
+	private function contains_post_content( array $block ): bool {
+		foreach ( $block['innerBlocks'] ?? array() as $inner_block ) {
+			$name = $inner_block['blockName'] ?? '';
+			if ( 'core/post-content' === $name ) {
+				return true;
+			}
+			if ( in_array( $name, self::CONTAINER_BLOCKS, true ) && $this->contains_post_content( $inner_block ) ) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Checks whether a block explicitly defines its own horizontal padding.
+	 *
+	 * When a block has explicit padding-left or padding-right in its style
+	 * attributes, it is managing its own layout. Root padding should not
+	 * be added on top, and containers with explicit padding should not
+	 * delegate root padding to their children.
+	 *
+	 * @param array $block The block to check.
+	 * @return bool True if the block defines horizontal padding.
+	 */
+	private function has_explicit_horizontal_padding( array $block ): bool {
+		$padding = $block['attrs']['style']['spacing']['padding'] ?? array();
+		return isset( $padding['left'] ) || isset( $padding['right'] );
+	}
+
+	/**
+	 * Extracts and sanitizes root horizontal padding from theme styles.
+	 *
+	 * @param array $styles Theme styles.
+	 * @return array Root padding with 'left' and 'right' keys, or empty array if invalid.
+	 */
+	private function get_root_padding( array $styles ): array {
+		$padding   = $styles['spacing']['padding'] ?? array();
+		$has_left  = isset( $padding['left'] );
+		$has_right = isset( $padding['right'] );
+
+		// If neither horizontal padding key is defined, skip root padding entirely.
+		if ( ! $has_left && ! $has_right ) {
+			return array();
+		}
+
+		$left  = $has_left ? $padding['left'] : '0px';
+		$right = $has_right ? $padding['right'] : '0px';
+
+		// Validate against potentially malicious values.
+		if ( ! is_string( $left ) || ! is_string( $right ) || preg_match( '/[<>"\']/', $left . $right ) ) {
+			return array();
+		}
+
+		return array(
+			'left'  => $left,
+			'right' => $right,
+		);
+	}
+
 	/**
 	 * Extracts the horizontal blockGap from a columns 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 1c3b0f5dd3f..448fa0bdc2b 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
@@ -196,13 +196,20 @@ class Content_Renderer {
 	}

 	/**
-	 * Preprocess parsed blocks
+	 * Preprocess parsed blocks.
+	 *
+	 * Called for both template blocks and post-content user blocks. The
+	 * Spacing_Preprocessor handles root padding distribution: container
+	 * blocks (groups wrapping post-content) are transparent, delegating
+	 * padding to their children so user blocks get individual padding.
 	 *
 	 * @param array $parsed_blocks Parsed blocks.
 	 * @return array
 	 */
 	public function preprocess_parsed_blocks( array $parsed_blocks ): array {
-		return $this->process_manager->preprocess( $parsed_blocks, $this->theme_controller->get_layout_settings(), $this->theme_controller->get_styles() );
+		$styles = $this->theme_controller->get_styles();
+
+		return $this->process_manager->preprocess( $parsed_blocks, $this->theme_controller->get_layout_settings(), $styles );
 	}

 	/**
diff --git a/packages/php/email-editor/src/Engine/Renderer/class-renderer.php b/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
index 913653f98a6..d3baa51225b 100644
--- a/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
+++ b/packages/php/email-editor/src/Engine/Renderer/class-renderer.php
@@ -133,8 +133,6 @@ class Renderer {
 				'color'            => $email_styles['color']['text'] ?? 'inherit',
 				'padding-top'      => $email_styles['spacing']['padding']['top'] ?? '0px',
 				'padding-bottom'   => $email_styles['spacing']['padding']['bottom'] ?? '0px',
-				'padding-left'     => $email_styles['spacing']['padding']['left'] ?? '0px',
-				'padding-right'    => $email_styles['spacing']['padding']['right'] ?? '0px',
 				'font-family'      => $email_styles['typography']['fontFamily'] ?? 'inherit',
 				'line-height'      => $email_styles['typography']['lineHeight'] ?? '1.5',
 				'font-size'        => $email_styles['typography']['fontSize'] ?? 'inherit',
diff --git a/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php b/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php
index 495d4c944dc..8103e7b32d0 100644
--- a/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Renderer/Renderer_Test.php
@@ -158,8 +158,9 @@ class Renderer_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertStringContainsString( 'font-family: Test Font Family;', $style );
 		$this->assertStringContainsString( 'padding-top: 3px;', $style );
 		$this->assertStringContainsString( 'padding-bottom: 4px;', $style );
-		$this->assertStringContainsString( 'padding-left: 2px;', $style );
-		$this->assertStringContainsString( 'padding-right: 1px;', $style );
+		// Horizontal padding is now distributed to individual block wrappers via Spacing_Preprocessor.
+		$this->assertStringNotContainsString( 'padding-left:', $style );
+		$this->assertStringNotContainsString( 'padding-right:', $style );
 		$this->assertStringContainsString( 'max-width: 660px;', $style );
 	}

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 066bbe44ad3..3411648d097 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
@@ -111,6 +111,16 @@ class Spacing_Preprocessor_Test extends \Email_Editor_Unit_Test {
 		$this->assertArrayNotHasKey( 'margin-top', $nested_column_first_item['email_attrs'] );
 		$this->assertArrayHasKey( 'margin-top', $nested_column_second_item['email_attrs'] );
 		$this->assertEquals( '10px', $nested_column_second_item['email_attrs']['margin-top'] );
+
+		// Root-level blocks should have root padding.
+		$this->assertEquals( '10px', $first_columns['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $first_columns['email_attrs']['padding-right'] );
+		$this->assertEquals( '10px', $second_columns['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $second_columns['email_attrs']['padding-right'] );
+
+		// Nested blocks should not have root padding.
+		$this->assertArrayNotHasKey( 'padding-right', $nested_column_first_item['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $nested_column_second_item['email_attrs'] );
 	}

 	/**
@@ -244,6 +254,414 @@ class Spacing_Preprocessor_Test extends \Email_Editor_Unit_Test {
 		$this->assertEquals( '10px', $second_column['email_attrs']['padding-left'] );
 	}

+	/**
+	 * Test it skips root padding for core/post-content but applies it to its children
+	 */
+	public function testItDistributesRootPaddingThroughPostContent(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/post-content',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/paragraph',
+						'attrs'       => array(),
+						'innerBlocks' => array(),
+					),
+					array(
+						'blockName'   => 'core/image',
+						'attrs'       => array(),
+						'innerBlocks' => array(),
+					),
+				),
+			),
+		);
+
+		$result       = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$post_content = $result[0];
+		$first_child  = $post_content['innerBlocks'][0];
+		$second_child = $post_content['innerBlocks'][1];
+
+		// core/post-content itself should NOT get root padding (it's a pass-through).
+		$this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $post_content['email_attrs'] );
+
+		// Direct children of post-content should get root padding.
+		$this->assertEquals( '10px', $first_child['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $first_child['email_attrs']['padding-right'] );
+		$this->assertEquals( '10px', $second_child['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $second_child['email_attrs']['padding-right'] );
+	}
+
+	/**
+	 * Test it distributes root padding through post-content nested inside a root-level group
+	 */
+	public function testItDistributesRootPaddingThroughNestedPostContent(): 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(),
+							),
+							array(
+								'blockName'   => 'core/image',
+								'attrs'       => array(),
+								'innerBlocks' => array(),
+							),
+						),
+					),
+				),
+			),
+		);
+
+		$result       = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$group        = $result[0];
+		$post_content = $group['innerBlocks'][0];
+		$first_child  = $post_content['innerBlocks'][0];
+		$second_child = $post_content['innerBlocks'][1];
+
+		// Root-level group is a container — it does NOT get root padding itself.
+		$this->assertArrayNotHasKey( 'padding-left', $group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $group['email_attrs'] );
+
+		// Nested post-content should NOT get root padding (post-content never gets padding).
+		$this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $post_content['email_attrs'] );
+
+		// Children of post-content inside root group SHOULD get padding (delegation chain).
+		$this->assertEquals( '10px', $first_child['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $first_child['email_attrs']['padding-right'] );
+		$this->assertEquals( '10px', $second_child['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $second_child['email_attrs']['padding-right'] );
+	}
+
+	/**
+	 * Test content-group wrapping post-content is transparent in template-like structure
+	 */
+	public function testItMakesContentGroupTransparentWhenWrappingPostContent(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/site-title',
+						'attrs'       => array(),
+						'innerBlocks' => array(),
+					),
+					array(
+						'blockName'   => 'core/group',
+						'attrs'       => array(),
+						'innerBlocks' => array(
+							array(
+								'blockName'   => 'core/post-content',
+								'attrs'       => array(),
+								'innerBlocks' => array(),
+							),
+						),
+					),
+					array(
+						'blockName'   => 'core/group',
+						'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];
+		$site_title    = $root_group['innerBlocks'][0];
+		$content_group = $root_group['innerBlocks'][1];
+		$post_content  = $content_group['innerBlocks'][0];
+		$footer_group  = $root_group['innerBlocks'][2];
+
+		// Root group: no padding (root container delegates).
+		$this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $root_group['email_attrs'] );
+
+		// Site title: gets root padding (non-container, receives delegation).
+		$this->assertEquals( '10px', $site_title['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $site_title['email_attrs']['padding-right'] );
+
+		// Content group: transparent (wraps post-content, delegates further).
+		$this->assertArrayNotHasKey( 'padding-left', $content_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $content_group['email_attrs'] );
+
+		// Post-content: no padding (post-content never gets padding).
+		$this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $post_content['email_attrs'] );
+
+		// Footer group: gets root padding (doesn't wrap post-content).
+		$this->assertEquals( '10px', $footer_group['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $footer_group['email_attrs']['padding-right'] );
+	}
+
+	/**
+	 * Test deeply nested post-content (group → group → post-content) delegates correctly
+	 */
+	public function testItDelegatesThroughDeeplyNestedPostContent(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/group',
+						'attrs'       => array(),
+						'innerBlocks' => 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 );
+		$root_group   = $result[0];
+		$middle_group = $root_group['innerBlocks'][0];
+		$inner_group  = $middle_group['innerBlocks'][0];
+		$post_content = $inner_group['innerBlocks'][0];
+		$user_block   = $post_content['innerBlocks'][0];
+
+		// All container groups in the chain should be transparent (no padding).
+		$this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-left', $middle_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-left', $inner_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-left', $post_content['email_attrs'] );
+
+		// User block inside post-content should get root padding.
+		$this->assertEquals( '10px', $user_block['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $user_block['email_attrs']['padding-right'] );
+	}
+
+	/**
+	 * Test it skips root padding for alignfull children of root-level containers
+	 */
+	public function testItSkipsRootPaddingForAlignfullBlocks(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/group',
+						'attrs'       => array( 'align' => 'full' ),
+						'innerBlocks' => array(),
+					),
+					array(
+						'blockName'   => 'core/paragraph',
+						'attrs'       => array(),
+						'innerBlocks' => array(),
+					),
+				),
+			),
+		);
+
+		$result          = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$root_group      = $result[0];
+		$alignfull_child = $root_group['innerBlocks'][0];
+		$normal_child    = $root_group['innerBlocks'][1];
+
+		// Root-level group is a container — it does NOT get root padding itself.
+		$this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $root_group['email_attrs'] );
+
+		// Alignfull child should NOT get root padding (skipped for full-width).
+		$this->assertArrayNotHasKey( 'padding-left', $alignfull_child['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $alignfull_child['email_attrs'] );
+
+		// Normal child should get root padding.
+		$this->assertEquals( '10px', $normal_child['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $normal_child['email_attrs']['padding-right'] );
+	}
+
+	/**
+	 * Test root-level group delegates padding to direct children but not deeper
+	 */
+	public function testItDelegatesPaddingToDirectChildrenOnly(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					array(
+						'blockName'   => 'core/paragraph',
+						'attrs'       => array(),
+						'innerBlocks' => array(),
+					),
+					array(
+						'blockName'   => 'core/group',
+						'attrs'       => array(),
+						'innerBlocks' => array(
+							array(
+								'blockName'   => 'core/image',
+								'attrs'       => array(),
+								'innerBlocks' => array(),
+							),
+						),
+					),
+				),
+			),
+		);
+
+		$result           = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$root_group       = $result[0];
+		$nested_paragraph = $root_group['innerBlocks'][0];
+		$nested_group     = $root_group['innerBlocks'][1];
+		$deeply_nested    = $nested_group['innerBlocks'][0];
+
+		// Root-level group is a container — it does NOT get root padding itself.
+		$this->assertArrayNotHasKey( 'padding-left', $root_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $root_group['email_attrs'] );
+
+		// Direct children of root group SHOULD get root padding.
+		$this->assertEquals( '10px', $nested_paragraph['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $nested_paragraph['email_attrs']['padding-right'] );
+		$this->assertEquals( '10px', $nested_group['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $nested_group['email_attrs']['padding-right'] );
+
+		// Deeply nested blocks should NOT get root padding.
+		$this->assertArrayNotHasKey( 'padding-left', $deeply_nested['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $deeply_nested['email_attrs'] );
+	}
+
+	/**
+	 * Test blocks with explicit horizontal padding skip root padding
+	 */
+	public function testItSkipsRootPaddingForBlocksWithExplicitPadding(): void {
+		$blocks = array(
+			array(
+				'blockName'   => 'core/group',
+				'attrs'       => array(),
+				'innerBlocks' => array(
+					// Group with explicit 0px padding (edge-to-edge banner).
+					array(
+						'blockName'   => 'core/group',
+						'attrs'       => array(
+							'style' => array(
+								'spacing' => array(
+									'padding' => array(
+										'left'  => '0px',
+										'right' => '0px',
+									),
+								),
+							),
+						),
+						'innerBlocks' => array(
+							array(
+								'blockName'   => 'core/paragraph',
+								'attrs'       => array(),
+								'innerBlocks' => array(),
+							),
+						),
+					),
+					// Columns with explicit 0px padding.
+					array(
+						'blockName'   => 'core/columns',
+						'attrs'       => array(
+							'style' => array(
+								'spacing' => array(
+									'padding' => array(
+										'left'  => '0px',
+										'right' => '0px',
+									),
+								),
+							),
+						),
+						'innerBlocks' => array(),
+					),
+					// Group with explicit 40px padding.
+					array(
+						'blockName'   => 'core/group',
+						'attrs'       => array(
+							'style' => array(
+								'spacing' => array(
+									'padding' => array(
+										'left'  => '40px',
+										'right' => '40px',
+									),
+								),
+							),
+						),
+						'innerBlocks' => array(
+							array(
+								'blockName'   => 'core/paragraph',
+								'attrs'       => array(),
+								'innerBlocks' => array(),
+							),
+						),
+					),
+					// Paragraph with no explicit padding (should get root padding).
+					array(
+						'blockName'   => 'core/paragraph',
+						'attrs'       => array(),
+						'innerBlocks' => array(),
+					),
+				),
+			),
+		);
+
+		$result          = $this->preprocessor->preprocess( $blocks, $this->layout, $this->styles );
+		$root_group      = $result[0];
+		$banner_group    = $root_group['innerBlocks'][0];
+		$banner_child    = $banner_group['innerBlocks'][0];
+		$columns         = $root_group['innerBlocks'][1];
+		$padded_group    = $root_group['innerBlocks'][2];
+		$padded_child    = $padded_group['innerBlocks'][0];
+		$plain_paragraph = $root_group['innerBlocks'][3];
+
+		// Banner group (0px padding): skips root padding, children don't get delegation.
+		$this->assertArrayNotHasKey( 'padding-left', $banner_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $banner_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-left', $banner_child['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $banner_child['email_attrs'] );
+
+		// Columns (0px padding): skips root padding.
+		$this->assertArrayNotHasKey( 'padding-left', $columns['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $columns['email_attrs'] );
+
+		// Padded group (40px): skips root padding, children don't get delegation.
+		$this->assertArrayNotHasKey( 'padding-left', $padded_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $padded_group['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-left', $padded_child['email_attrs'] );
+		$this->assertArrayNotHasKey( 'padding-right', $padded_child['email_attrs'] );
+
+		// Plain paragraph (no explicit padding): gets root padding.
+		$this->assertEquals( '10px', $plain_paragraph['email_attrs']['padding-left'] );
+		$this->assertEquals( '10px', $plain_paragraph['email_attrs']['padding-right'] );
+	}
+
 	/**
 	 * Test it rejects malicious values in blockGap
 	 */