<?php
/**
 * Build methods and pattern helpers for AxcelersBlocksCss.
 *
 * @package AxcelersBlocks
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

trait AxcelersBlocksCssBuilders {

	// ─── Orchestration ────────────────────────────────────────────────────────

	private function buildMain(): void {
		$this->addSpacing( 'main', 'padding', 'padding' );
		$this->addSpacing( 'main', 'margin',  'margin' );
		$this->addMany( 'main', [
			// Layout
			[ 'display',               'display' ],
			[ 'align-items',           'alignItems' ],
			[ 'justify-content',       'justifyContent' ],
			[ 'justify-items',         'justifyItems' ],
			[ 'justify-self',          'justifySelf' ],
			[ 'align-self',            'alignSelf' ],
			// Grid
			[ 'grid-template-columns', 'gridTemplateColumns' ],
			[ 'grid-template-rows',    'gridTemplateRows' ],
			[ 'column-gap',            'columnGap' ],
			[ 'row-gap',               'rowGap' ],
			[ 'grid-auto-flow',        'gridAutoFlow' ],
			[ 'grid-column',           'gridColumn' ],
			[ 'grid-row',              'gridRow' ],
			[ 'order',                 'order' ],
			// Flex
			[ 'flex-direction',        'flexDirection' ],
			[ 'flex-wrap',             'flexWrap' ],
			[ 'flex-grow',             'flexGrow' ],
			[ 'flex-shrink',           'flexShrink' ],
			[ 'flex-basis',            'flexBasis' ],
			// Float
			[ 'float',                 'float' ],
			[ 'clear',                 'clear' ],
			// Sizing
			[ 'width',                 'width' ],
			[ 'height',                'height' ],
			[ 'min-width',             'minWidth' ],
			[ 'min-height',            'minHeight' ],
			[ 'max-width',             'maxWidth' ],
			[ 'max-height',            'maxHeight' ],
			[ 'aspect-ratio',          'aspectRatio' ],
			// Typography
			[ 'color',                 'textColor' ],
			[ 'font-size',             'fontSize' ],
			[ 'font-weight',           'fontWeight' ],
			[ 'font-style',            'fontStyle' ],
			[ 'text-align',            'textAlign' ],
			[ 'text-transform',        'textTransform' ],
			[ 'text-decoration',       'textDecoration' ],
			[ 'line-height',           'lineHeight' ],
			[ 'letter-spacing',        'letterSpacing' ],
			[ 'font-family',           'fontFamily' ],
			// Table semantics (only emitted when the attribute exists — table family).
			[ 'vertical-align',        'verticalAlign' ],
			[ 'border-spacing',        'borderSpacing' ],
		] );
		$this->addBackground( 'main', 'backgroundColor', 'backgroundLayers' );
		$this->addBorder( 'main', 'border', 'borderRadius' );
		$this->addPosition( 'main', 'position', 'inset', 'overflowX', 'overflowY', 'zIndex' );
		$this->addEffect( 'main', 'effect' );
		// Outline is only ever set on interactive states (e.g. button :focus / :active).
		// Emits nothing for blocks/states without an `outline` attribute.
		$this->addOutline( 'main', 'outline', 'outlineOffset' );
	}

	private function buildIcon(): void {
		$this->addMany( 'icon', [
			[ 'display',         'iconDisplay' ],
			[ 'align-items',     'iconAlignItems' ],
			[ 'justify-content', 'iconJustifyContent' ],
		] );
		$this->addSpacing( 'icon', 'padding', 'iconPadding' );
		$this->addSpacing( 'icon', 'margin',  'iconMargin' );
		$this->addBackground( 'icon', 'iconBackgroundColor', 'iconBackgroundLayers' );
		$this->addBorder( 'icon', 'iconBorder', 'iconBorderRadius' );
		$this->addPosition( 'icon', 'iconPosition', 'iconInset', 'iconOverflowX', 'iconOverflowY', 'iconZIndex' );
		$this->addEffect( 'icon', 'iconEffect' );
		$this->addIconSize();
		$this->addIconColor();
	}

	private function buildSvg(): void {
		$this->addMany( 'svg', [
			[ 'display',      'svgDisplay' ],
			[ 'width',        'svgWidth' ],
			[ 'height',       'svgHeight' ],
			[ 'min-width',    'svgMinWidth' ],
			[ 'min-height',   'svgMinHeight' ],
			[ 'max-width',    'svgMaxWidth' ],
			[ 'max-height',   'svgMaxHeight' ],
			[ 'aspect-ratio', 'svgAspectRatio' ],
		] );
		$this->addEffect( 'svg', 'svgEffect' );
		$this->addPosition( 'svg', 'svgPosition', 'svgInset', 'svgOverflowX', 'svgOverflowY', 'svgZIndex' );
	}

	private function buildLink(): void {
		// Each interactive state maps a bucket → attribute prefix. The base 'link'
		// state plus hover / focus / active, all sharing the same property set.
		foreach ( [
			'link'       => 'link',
			'linkHover'  => 'linkHover',
			'linkFocus'  => 'linkFocus',
			'linkActive' => 'linkActive',
		] as $bucket => $prefix ) {
			$this->buildLinkState( $bucket, $prefix );
		}
	}

	/** Build one link state bucket from its attribute prefix (link / linkHover / linkFocus / linkActive). */
	private function buildLinkState( string $bucket, string $prefix ): void {
		$this->addMany( $bucket, [
			[ 'display',         "{$prefix}Display" ],
			[ 'color',           "{$prefix}TextColor" ],
			[ 'font-size',       "{$prefix}FontSize" ],
			[ 'font-weight',     "{$prefix}FontWeight" ],
			[ 'font-style',      "{$prefix}FontStyle" ],
			[ 'text-decoration', "{$prefix}TextDecoration" ],
			[ 'text-transform',  "{$prefix}TextTransform" ],
			[ 'line-height',     "{$prefix}LineHeight" ],
			[ 'letter-spacing',  "{$prefix}LetterSpacing" ],
			[ 'font-family',     "{$prefix}FontFamily" ],
		] );
		$this->addBackground( $bucket, "{$prefix}BackgroundColor", "{$prefix}BackgroundLayers" );
		$this->addBorder( $bucket, "{$prefix}Border", "{$prefix}BorderRadius" );
		$this->addOutline( $bucket, "{$prefix}Outline", "{$prefix}OutlineOffset" );
		$this->addSpacing( $bucket, 'padding', "{$prefix}Padding" );
		$this->addSpacing( $bucket, 'margin',  "{$prefix}Margin" );
		$this->addEffect( $bucket, "{$prefix}Effect" );
	}

	// ─── Pattern helpers ──────────────────────────────────────────────────────

	/** Add a list of [css-property, attribute-name] pairs for every device. */
	private function addMany( string $target, array $map ): void {
		foreach ( $map as [ $prop, $attrName ] ) {
			foreach ( array_keys( $this->devices ) as $device ) {
				$this->add( $target, $device, $prop, $this->attributes[ $attrName ][ $device ] ?? '' );
			}
		}
	}

	/** Add padding or margin, handling linked/axial/individual side modes. */
	private function addSpacing( string $target, string $type, string $attrName ): void {
		$values = $this->attributes[ $attrName ] ?? [];

		foreach ( array_keys( $this->devices ) as $device ) {
			$t = $this->spacingSideValue( $values, $device, 'top' );
			$r = $this->spacingSideValue( $values, $device, 'right' );
			$b = $this->spacingSideValue( $values, $device, 'bottom' );
			$l = $this->spacingSideValue( $values, $device, 'left' );

			if ( '' === $t && '' === $r && '' === $b && '' === $l ) {
				continue;
			}

			if ( '' !== $t && '' !== $r && '' !== $b && '' !== $l ) {
				$this->add( $target, $device, $type, $this->spacingShorthand( $t, $r, $b, $l ) );
			} else {
				if ( '' !== $t ) { $this->add( $target, $device, "{$type}-top",    $t ); }
				if ( '' !== $r ) { $this->add( $target, $device, "{$type}-right",  $r ); }
				if ( '' !== $b ) { $this->add( $target, $device, "{$type}-bottom", $b ); }
				if ( '' !== $l ) { $this->add( $target, $device, "{$type}-left",   $l ); }
			}
		}
	}

	private function spacingSideValue( array $values, string $device, string $side ): string {
		$fallback_devices = $this->responsiveFallbackDevices( $device );
		$count            = count( $fallback_devices );

		for ( $index = 0; $index < $count; $index++ ) {
			$fallbackDevice = $fallback_devices[ $index ];
			$dv = $values[ $fallbackDevice ] ?? [];
			if ( ! is_array( $dv ) ) {
				continue;
			}

			$value = $this->spacingSideValueFromDevice( $dv, $side );
			if ( '' !== $value ) {
				$later_device_has_real_value = false;
				for ( $later_index = $index + 1; $later_index < $count; $later_index++ ) {
					$later_dv = $values[ $fallback_devices[ $later_index ] ] ?? [];
					if ( is_array( $later_dv ) && $this->hasNonZeroSpacingValue( $later_dv ) ) {
						$later_device_has_real_value = true;
						break;
					}
				}

				if ( $this->isDefaultZeroSpacingDevice( $dv ) && $later_device_has_real_value ) {
					continue;
				}

				return $value;
			}
		}

		return '';
	}

	private function spacingSideValueFromDevice( array $dv, string $side ): string {
		$first_set = static function ( ...$candidates ) {
			foreach ( $candidates as $candidate ) {
				$value = axcelersblocks_sanitize_css_value( $candidate );
				if ( '' !== $value ) {
					return $value;
				}
			}
			return '';
		};

		if ( ! empty( $dv['isVHActive'] ) ) {
			return $first_set(
				$dv['all'] ?? '',
				$dv['top'] ?? '',
				$dv['right'] ?? '',
				$dv['bottom'] ?? '',
				$dv['left'] ?? ''
			);
		}

		if ( ! empty( $dv['isSidesAllActive'] ) ) {
			if ( 'top' === $side || 'bottom' === $side ) {
				return $first_set( $dv['vertical'] ?? '', $dv['top'] ?? '', $dv['bottom'] ?? '' );
			}

			return $first_set( $dv['horizontal'] ?? '', $dv['right'] ?? '', $dv['left'] ?? '' );
		}

		return axcelersblocks_sanitize_css_value( $dv[ $side ] ?? '' );
	}

	private function isDefaultZeroSpacingDevice( array $dv ): bool {
		if ( ! empty( $dv['isVHActive'] ) || ! empty( $dv['isSidesAllActive'] ) ) {
			return false;
		}

		foreach ( [ 'all', 'vertical', 'horizontal' ] as $field ) {
			if ( '' !== axcelersblocks_sanitize_css_value( $dv[ $field ] ?? '' ) ) {
				return false;
			}
		}

		foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
			if ( ! $this->isZeroSpacingValue( $dv[ $side ] ?? '' ) ) {
				return false;
			}
		}

		return true;
	}

	private function hasNonZeroSpacingValue( array $dv ): bool {
		foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
			$value = $this->spacingSideValueFromDevice( $dv, $side );
			if ( '' !== $value && ! $this->isZeroSpacingValue( $value ) ) {
				return true;
			}
		}

		return false;
	}

	private function isZeroSpacingValue( $value ): bool {
		$value = axcelersblocks_sanitize_css_value( $value );
		if ( '' === $value ) {
			return false;
		}

		return 1 === preg_match( '/^0(?:\.0+)?(?:px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc)?$/i', $value );
	}

	/** Add background-color and background-image/position/size/repeat from layers. */
	private function addBackground( string $target, string $colorAttr, string $layersAttr ): void {
		foreach ( array_keys( $this->devices ) as $device ) {
			$this->add( $target, $device, 'background-color', $this->attributes[ $colorAttr ][ $device ] ?? '' );

			$layers = $this->attributes[ $layersAttr ][ $device ] ?? [];
			if ( empty( $layers ) ) {
				continue;
			}

			$img = []; $pos = []; $sz = []; $rep = [];
			foreach ( $layers as $layer ) {
				switch ( $layer['type'] ?? '' ) {
					case 'image':
						if ( ! empty( $layer['url'] ) ) {
							$img[] = 'url(' . esc_url( $layer['url'] ) . ')';
							$pos[] = $layer['position'] ?? 'center center';
							$sz[]  = $layer['size']     ?? 'cover';
							$rep[] = $layer['repeat']   ?? 'no-repeat';
						}
						break;
					case 'gradient':
						if ( ! empty( $layer['gradient'] ) ) {
							$img[] = $layer['gradient'];
						}
						break;
					case 'overlay':
						if ( ! empty( $layer['color'] ) ) {
							$opacity = isset( $layer['opacity'] ) ? floatval( $layer['opacity'] ) : 0.5;
							$rgb     = $this->hexToRgb( $layer['color'] );
							$img[]   = "linear-gradient(to left,rgba($rgb,$opacity) 0%,rgba($rgb,$opacity) 100%)";
						}
						break;
				}
			}

			$img = array_filter( $img );
			if ( ! empty( $img ) ) {
				$this->add( $target, $device, 'background-image',    implode( ',', $img ) );
				$this->add( $target, $device, 'background-position', implode( ',', array_filter( $pos ) ) );
				$this->add( $target, $device, 'background-size',     implode( ',', array_filter( $sz ) ) );
				$this->add( $target, $device, 'background-repeat',   implode( ',', array_filter( $rep ) ) );
			}
		}
	}

	/** Add border and border-radius, handling linked/side-split and corner modes. */
	private function addBorder( string $target, string $borderAttr, string $radiusAttr ): void {
		$borderValues = $this->attributes[ $borderAttr ] ?? [];

		foreach ( array_keys( $this->devices ) as $device ) {
			$sourceDevice = $this->effectiveBorderSourceDevice( $borderValues, $device );
			if ( null !== $sourceDevice ) {
				if ( $this->isLinkedBorderDevice( $borderValues, $sourceDevice ) ) {
					$w  = $this->linkedBorderFieldValue( $borderValues, $device, 'width' );
					$sv = $this->linkedBorderFieldValue( $borderValues, $device, 'style' );
					$c  = $this->linkedBorderFieldValue( $borderValues, $device, 'color' );
					if ( '' !== $w )  { $this->add( $target, $device, 'border-width', $w ); }
					if ( '' !== $sv ) { $this->add( $target, $device, 'border-style', $sv ); }
					if ( '' !== $c )  { $this->add( $target, $device, 'border-color', $c ); }
				} else {
					$activeSides = $this->activeBorderSides( $borderValues[ $sourceDevice ] ?? [] );
					foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
						if ( ! in_array( $side, $activeSides, true ) ) {
							$this->add( $target, $device, "border-{$side}-width", '0' );
							continue;
						}

						$w  = $this->sideBorderFieldValue( $borderValues, $device, $side, 'width' );
						$sv = $this->sideBorderFieldValue( $borderValues, $device, $side, 'style' );
						$c  = $this->sideBorderFieldValue( $borderValues, $device, $side, 'color' );
						if ( '' !== $w )  { $this->add( $target, $device, "border-{$side}-width", $w ); }
						if ( '' !== $sv ) { $this->add( $target, $device, "border-{$side}-style", $sv ); }
						if ( '' !== $c )  { $this->add( $target, $device, "border-{$side}-color", $c ); }
					}
				}
			}

			$rv = $this->attributes[ $radiusAttr ][ $device ] ?? [ 'isLinkActive' => true ];
			if ( ! empty( $rv['isLinkActive'] ) ) {
				if ( ( $rv['value'] ?? '' ) !== '' ) {
					$this->add( $target, $device, 'border-radius', $rv['value'] );
				}
			} else {
				$tl  = $rv['corners']['top-left']     ?? '';
				$tr  = $rv['corners']['top-right']    ?? '';
				$brv = $rv['corners']['bottom-right'] ?? '';
				$bl  = $rv['corners']['bottom-left']  ?? '';
				if ( $tl && $tr && $brv && $bl ) {
					$this->add( $target, $device, 'border-radius', $this->radiusShorthand( $tl, $tr, $brv, $bl ) );
				} else {
					if ( $tl )  { $this->add( $target, $device, 'border-top-left-radius',     $tl ); }
					if ( $tr )  { $this->add( $target, $device, 'border-top-right-radius',    $tr ); }
					if ( $brv ) { $this->add( $target, $device, 'border-bottom-right-radius', $brv ); }
					if ( $bl )  { $this->add( $target, $device, 'border-bottom-left-radius',  $bl ); }
				}
			}
		}
	}

	/** Device lookup order for desktop-first responsive controls emitted as mobile-first CSS. */
	private function responsiveFallbackDevices( string $device ): array {
		if ( 'mobile' === $device ) {
			return [ 'mobile', 'tablet', 'desktop' ];
		}

		if ( 'tablet' === $device ) {
			return [ 'tablet', 'desktop' ];
		}

		return [ 'desktop' ];
	}

	private function effectiveBorderSourceDevice( array $borderValues, string $device ): ?string {
		foreach ( $this->responsiveFallbackDevices( $device ) as $fallbackDevice ) {
			$border = $borderValues[ $fallbackDevice ] ?? null;
			if ( ! is_array( $border ) ) {
				continue;
			}
			if ( $this->borderDeviceHasValues( $border ) || ( array_key_exists( 'isLinkActive', $border ) && empty( $border['isLinkActive'] ) ) ) {
				return $fallbackDevice;
			}
		}

		return null;
	}

	private function borderDeviceHasValues( array $border ): bool {
		foreach ( [ 'width', 'style', 'color' ] as $field ) {
			if ( '' !== axcelersblocks_sanitize_css_value( $border[ $field ] ?? '' ) ) {
				return true;
			}
		}

		foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
			if ( $this->sideHasOwnBorderValue( $border, $side ) ) {
				return true;
			}
		}

		return false;
	}

	private function isLinkedBorderDevice( array $borderValues, string $device ): bool {
		$border = $borderValues[ $device ] ?? [];
		return ! is_array( $border ) || ! array_key_exists( 'isLinkActive', $border ) || ! empty( $border['isLinkActive'] );
	}

	private function linkedBorderFieldValue( array $borderValues, string $device, string $field ): string {
		foreach ( $this->responsiveFallbackDevices( $device ) as $fallbackDevice ) {
			$border = $borderValues[ $fallbackDevice ] ?? [];
			if ( ! is_array( $border ) ) {
				continue;
			}

			$value = $this->isLinkedBorderDevice( $borderValues, $fallbackDevice )
				? ( $border[ $field ] ?? '' )
				: $this->matchingSideBorderFieldValue( $border, $field );

			$value = axcelersblocks_sanitize_css_value( $value );
			if ( '' !== $value ) {
				return $value;
			}
		}

		return '';
	}

	private function sideBorderFieldValue( array $borderValues, string $device, string $side, string $field ): string {
		foreach ( $this->responsiveFallbackDevices( $device ) as $fallbackDevice ) {
			$border = $borderValues[ $fallbackDevice ] ?? [];
			if ( ! is_array( $border ) ) {
				continue;
			}

			$value = $this->isLinkedBorderDevice( $borderValues, $fallbackDevice )
				? ( $border[ $field ] ?? '' )
				: ( $border['sides'][ $side ][ $field ] ?? '' );
			if ( '' === axcelersblocks_sanitize_css_value( $value ) && ! $this->isLinkedBorderDevice( $borderValues, $fallbackDevice ) ) {
				$value = $border[ $field ] ?? '';
			}

			$value = axcelersblocks_sanitize_css_value( $value );
			if ( '' !== $value ) {
				return $value;
			}
		}

		return '';
	}

	private function activeBorderSides( array $border ): array {
		$activeSides = [];
		foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
			if ( $this->sideHasOwnBorderValue( $border, $side ) ) {
				$activeSides[] = $side;
			}
		}

		return $activeSides;
	}

	private function sideHasOwnBorderValue( array $border, string $side ): bool {
		foreach ( [ 'width', 'style', 'color' ] as $field ) {
			if ( '' !== axcelersblocks_sanitize_css_value( $border['sides'][ $side ][ $field ] ?? '' ) ) {
				return true;
			}
		}

		return false;
	}

	private function matchingSideBorderFieldValue( array $border, string $field ): string {
		$matched = null;
		foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
			$value = axcelersblocks_sanitize_css_value( $border['sides'][ $side ][ $field ] ?? '' );
			if ( '' === $value ) {
				return '';
			}
			if ( null === $matched ) {
				$matched = $value;
				continue;
			}
			if ( $matched !== $value ) {
				return '';
			}
		}

		return null === $matched ? '' : $matched;
	}

	/** Add outline and outline-offset for interactive link states. */
	private function addOutline( string $target, string $outlineAttr, string $offsetAttr ): void {
		foreach ( array_keys( $this->devices ) as $device ) {
			$outline = $this->attributes[ $outlineAttr ][ $device ] ?? [];
			if ( ! empty( $outline ) ) {
				$width = $outline['width'] ?? '';
				$style = $outline['style'] ?? '';
				$color = $outline['color'] ?? '';
				if ( $style === 'none' ) {
					$this->add( $target, $device, 'outline', 'none' );
				} elseif ( $width !== '' && $style !== '' && $color !== '' ) {
					$this->add( $target, $device, 'outline', "$width $style $color" );
				} else {
					if ( $width !== '' ) { $this->add( $target, $device, 'outline-width', $width ); }
					if ( $style !== '' ) { $this->add( $target, $device, 'outline-style', $style ); }
					if ( $color !== '' ) { $this->add( $target, $device, 'outline-color', $color ); }
				}
			}

			$offset = $this->attributes[ $offsetAttr ][ $device ] ?? '';
			if ( $offset !== '' ) {
				$this->add( $target, $device, 'outline-offset', $offset );
			}
		}
	}

	/** Add position, inset (top/right/bottom/left), overflow, and z-index. */
	private function addPosition(
		string $target,
		string $posAttr, string $insetAttr,
		string $oxAttr, string $oyAttr, string $ziAttr
	): void {
		$this->addMany( $target, [
			[ 'position',   $posAttr ],
			[ 'overflow-x', $oxAttr ],
			[ 'overflow-y', $oyAttr ],
			[ 'z-index',    $ziAttr ],
		] );

		$insetValues = $this->attributes[ $insetAttr ] ?? [];

		// Skip entirely when the block sets no inset on any side/device, so we
		// never emit offsets on elements that don't use positioning.
		$hasInset = false;
		foreach ( array_keys( $this->devices ) as $device ) {
			foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
				if ( '' !== axcelersblocks_sanitize_css_value( $insetValues[ $device ][ $side ] ?? '' ) ) {
					$hasInset = true;
					break 2;
				}
			}
		}
		if ( ! $hasInset ) {
			return;
		}

		// Inset sides are authored desktop-first: a breakpoint may set only some
		// sides and inherit the rest from a wider breakpoint. Emitted as mobile-first
		// CSS, a side left unset at (say) desktop would otherwise inherit the NARROWER
		// breakpoint's value (e.g. desktop picking up mobile's `right`), which
		// mispositions absolutely-positioned elements. Resolve every side per device
		// through the desktop-first fallback chain and emit all four (falling back to
		// `auto`) so each breakpoint is self-contained. add() de-dupes the cascade.
		foreach ( array_keys( $this->devices ) as $device ) {
			foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
				$v = $this->insetSideValue( $insetValues, $device, $side );
				$this->add( $target, $device, $side, '' !== $v ? $v : 'auto' );
			}
		}
	}

	/** Resolve an inset side via the desktop-first responsive fallback chain. */
	private function insetSideValue( array $insetValues, string $device, string $side ): string {
		foreach ( $this->responsiveFallbackDevices( $device ) as $fallbackDevice ) {
			$v = axcelersblocks_sanitize_css_value( $insetValues[ $fallbackDevice ][ $side ] ?? '' );
			if ( '' !== $v ) {
				return $v;
			}
		}
		return '';
	}

	/** Add all effect properties (box-shadow, transition, filter, etc.). */
	private function addEffect( string $target, string $attrName ): void {
		$effectAttr = $this->attributes[ $attrName ] ?? [];
		foreach ( array_keys( $this->devices ) as $device ) {
			$eff = $effectAttr[ $device ] ?? null;
			if ( empty( $eff ) ) {
				continue;
			}
			if ( $v = $this->effectBoxShadow( $eff ) )  { $this->add( $target, $device, 'box-shadow',  $v ); }
			if ( $v = $this->effectTransition( $eff ) )  { $this->add( $target, $device, 'transition',  $v ); }
			if ( $v = $this->effectFilter( $eff ) )      { $this->add( $target, $device, 'filter',      $v ); }
			if ( $v = $this->effectTextShadow( $eff ) )  { $this->add( $target, $device, 'text-shadow', $v ); }
			if ( $v = axcelersblocks_build_transform_style( $eff['transform'] ?? null ) ) {
				$this->add( $target, $device, 'transform', $v );
			}
			$op  = $eff['opacity']    ?? '';
			$vis = $eff['visibility'] ?? '';
			if ( $op  !== '' && $op  !== '1' )      { $this->add( $target, $device, 'opacity',    $op ); }
			if ( $vis !== '' && $vis !== 'visible' ) { $this->add( $target, $device, 'visibility', $vis ); }
		}
	}

	// ─── Icon-specific helpers ────────────────────────────────────────────────

	private function addIconSize(): void {
		$iconSize = $this->attributes['iconSize'] ?? null;
		if ( ! is_array( $iconSize ) ) {
			return;
		}
		$c       = sanitize_html_class( $this->uniqueClass );
		$sx      = $this->selectorSuffix;
		$prevKey = '';

		foreach ( $this->devices as $device => $config ) {
			$sides = $this->normalizeIconSize( $iconSize[ $device ] ?? '' );
			$w     = $sides['width'];
			$h     = $sides['height'];
			$key   = "$w|$h";

			if ( ( $w === '' && $h === '' ) || ( $device !== 'mobile' && $key === $prevKey ) ) {
				continue;
			}
			$prevKey = $key;

			$decls = [];
			if ( $w !== '' ) { $decls[] = "width:$w"; }
			if ( $h !== '' ) { $decls[] = "height:$h"; }
			if ( empty( $decls ) ) {
				continue;
			}

			$rule           = ".{$c}{$sx} .icon{" . implode( ';', $decls ) . ';}';
			$this->output[] = empty( $config['media'] ) ? $rule : "{$config['media']} { $rule }";
		}
	}

	private function addIconColor(): void {
		$color = axcelersblocks_sanitize_css_value( $this->attributes['iconColor'] ?? '' );
		if ( $color === '' ) {
			return;
		}
		$c                   = sanitize_html_class( $this->uniqueClass );
		$sx                  = $this->selectorSuffix;
		$this->globalRules[] = ".{$c}{$sx} .icon { color: $color; }";
		$this->globalRules[] = ".{$c}{$sx} .icon svg { fill: currentColor; }";
	}

	// ─── Backward-compat fill ─────────────────────────────────────────────────

	/**
	 * Fill missing mobile values from desktop (for min-width CSS cascade).
	 * Ensures mobile always has a value when desktop does, so the base rule is always emitted.
	 */
	private function backfillMobile( array $attributes ): array {
		$hasCssV = static function ( $v ) use ( &$hasCssV ): bool {
			if ( is_bool( $v ) || is_null( $v ) ) { return false; }
			if ( is_string( $v ) )                { return $v !== ''; }
			if ( is_array( $v ) ) {
				foreach ( $v as $i ) {
					if ( $hasCssV( $i ) ) { return true; }
				}
			}
			return false;
		};

		$skip_whole_object_backfill = [ 'border', 'iconBorder', 'linkBorder', 'linkHoverBorder', 'linkFocusBorder', 'linkActiveBorder' ];

		foreach ( $attributes as $attr_key => &$av ) {
			if ( in_array( $attr_key, $skip_whole_object_backfill, true ) ) {
				continue;
			}
			if ( ! is_array( $av ) ) { continue; }
			$hasD = isset( $av['desktop'] ) && $hasCssV( $av['desktop'] );
			$hasT = isset( $av['tablet'] )  && $hasCssV( $av['tablet'] );
			$hasM = isset( $av['mobile'] )  && $hasCssV( $av['mobile'] );
			if ( $hasD && ! $hasM ) {
				$av['mobile'] = $av['desktop'];
			}
			if ( $hasT && ! $hasD ) {
				$av['desktop'] = $av['tablet'];
				if ( ! $hasM ) { $av['mobile'] = $av['tablet']; }
			}
		}
		unset( $av );
		return $attributes;
	}
}
