<?php
/**
 * Feature chip popout (modal) helpers.
 *
 * @package Rsmspark_Content_Widgets
 */

defined( 'ABSPATH' ) || exit;

/**
 * Popout map, CSS variables, and content resolution.
 */
final class Rsmspark_Content_Widgets_Features_Popouts {

	/**
	 * @param mixed $value Raw checkbox value.
	 * @return bool
	 */
	public static function is_enabled( $value ) {
		if ( is_bool( $value ) ) {
			return $value;
		}
		if ( is_string( $value ) ) {
			return in_array( strtolower( $value ), array( '1', 'true', 'yes', 'on' ), true );
		}
		return ! empty( $value );
	}

	/**
	 * Normalize a label for popout-to-chip matching.
	 *
	 * @param string $label Raw label.
	 * @return string
	 */
	public static function normalize_label( $label ) {
		$label = sanitize_title( (string) $label );
		return '' !== $label ? $label : '';
	}

	/**
	 * Build lookup maps from the popouts repeater.
	 *
	 * @param array<int, array<string, mixed>> $items Popout repeater rows.
	 * @return array{by_index: array<int, array<string, mixed>>, by_label: array<string, array<string, mixed>>}
	 */
	public static function build_popout_map( array $items ) {
		$by_index = array();
		$by_label = array();

		foreach ( $items as $i => $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$resolved = self::sanitize_popout_item( $item );
			if ( null === $resolved ) {
				continue;
			}
			$by_index[ (int) $i ] = $resolved;
			$key = self::normalize_label( $resolved['chip_label'] );
			if ( '' !== $key ) {
				$by_label[ $key ] = $resolved;
			}
		}

		return array(
			'by_index' => $by_index,
			'by_label' => $by_label,
		);
	}

	/**
	 * Resolve popout content for a chip.
	 *
	 * @param int                                $chip_index   Zero-based index in the features repeater.
	 * @param string                             $chip_title   Chip title.
	 * @param array<string, array<string, mixed>> $map         From build_popout_map().
	 * @return array<string, mixed>|null
	 */
	public static function resolve_for_chip( $chip_index, $chip_title, array $map ) {
		if ( isset( $map['by_index'][ (int) $chip_index ] ) ) {
			return $map['by_index'][ (int) $chip_index ];
		}
		$key = self::normalize_label( $chip_title );
		if ( '' !== $key && isset( $map['by_label'][ $key ] ) ) {
			return $map['by_label'][ $key ];
		}
		return null;
	}

	/**
	 * Media source options for popout header icons.
	 *
	 * @return array<string, string>
	 */
	public static function get_media_source_options() {
		return array(
			'inherit' => __( 'Inherit from feature chip', 'rsm-content-widgets' ),
			'custom'  => __( 'Custom icon or image', 'rsm-content-widgets' ),
		);
	}

	/**
	 * Convert hex color + opacity percent to rgba().
	 *
	 * @param string $hex     Hex color.
	 * @param int    $opacity Opacity 0–100.
	 * @return string
	 */
	public static function hex_to_rgba( $hex, $opacity ) {
		$hex = sanitize_hex_color( (string) $hex );
		if ( ! $hex ) {
			$hex = '#0f172a';
		}
		$opacity = max( 0, min( 100, (int) $opacity ) ) / 100;
		$hex     = ltrim( $hex, '#' );
		if ( 3 === strlen( $hex ) ) {
			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
		}
		$r = hexdec( substr( $hex, 0, 2 ) );
		$g = hexdec( substr( $hex, 2, 2 ) );
		$b = hexdec( substr( $hex, 4, 2 ) );
		return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . round( $opacity, 2 ) . ')';
	}

	/**
	 * Resolve popout header icon size.
	 *
	 * @param array<string, mixed> $popout        Popout repeater row.
	 * @param array<string, mixed> $chip          Chip field values.
	 * @param array<string, mixed> $icon_defaults Widget icon defaults.
	 * @return string CSS length for icon size.
	 */
	public static function resolve_popout_icon_size( array $popout, array $chip, array $icon_defaults ) {
		$preset = isset( $popout['popout_icon_size'] ) ? sanitize_key( (string) $popout['popout_icon_size'] ) : 'inherit';
		if ( 'inherit' !== $preset && in_array( $preset, Rsmspark_Content_Widgets_Features_Styles::icon_size_keys(), true ) ) {
			return Rsmspark_Content_Widgets_Features_Styles::resolve_icon_size(
				$preset,
				isset( $popout['popout_icon_size_custom'] ) ? (int) $popout['popout_icon_size_custom'] : 0
			);
		}
		return Rsmspark_Content_Widgets_Features_Styles::resolve_chip_icon_size(
			$chip['icon_size'] ?? 'inherit',
			$icon_defaults['icon_size_preset'],
			$icon_defaults['icon_size_custom'],
			isset( $chip['icon_size_custom'] ) ? (int) $chip['icon_size_custom'] : 0
		);
	}

	/**
	 * Resolve popout header media (icon/image) and inline CSS variables.
	 *
	 * @param array<string, mixed> $popout        Popout repeater row (may be empty).
	 * @param array<string, mixed> $chip          Chip values (title_icon, image_url, accent_color, icon_size, icon_size_custom).
	 * @param string               $custom_image  Resolved custom popout image URL.
	 * @param array<string, mixed> $icon_defaults Widget icon defaults.
	 * @return array{title_icon: string, image_url: string, inline_style: string}
	 */
	public static function resolve_media( array $popout, array $chip, $custom_image, array $icon_defaults ) {
		$mode = isset( $popout['popout_media'] ) ? sanitize_key( (string) $popout['popout_media'] ) : 'inherit';
		if ( ! in_array( $mode, array( 'inherit', 'custom' ), true ) ) {
			$mode = 'inherit';
		}

		$title_icon = '';
		$image_url  = '';

		if ( 'custom' === $mode ) {
			$title_icon = Rsmspark_Content_Widgets_Features_Styles::normalize_icon_classes( $popout['popout_icon'] ?? '' );
			$image_url  = is_string( $custom_image ) ? $custom_image : '';
			if ( '' === $title_icon && '' === $image_url ) {
				$title_icon = Rsmspark_Content_Widgets_Features_Styles::normalize_icon_classes( $chip['title_icon'] ?? '' );
				$image_url  = isset( $chip['image_url'] ) ? (string) $chip['image_url'] : '';
			}
		} else {
			$title_icon = Rsmspark_Content_Widgets_Features_Styles::normalize_icon_classes( $chip['title_icon'] ?? '' );
			$image_url  = isset( $chip['image_url'] ) ? (string) $chip['image_url'] : '';
		}

		$icon_size = self::resolve_popout_icon_size( $popout, $chip, $icon_defaults );
		$accent    = isset( $chip['accent_color'] ) ? sanitize_hex_color( (string) $chip['accent_color'] ) : '';

		$parts = array( '--rf-popout-item-icon-base:' . $icon_size );
		if ( $accent ) {
			$parts[] = '--rf-popout-item-accent:' . $accent;
		}

		return array(
			'title_icon'    => $title_icon,
			'image_url'     => $image_url,
			'inline_style'  => implode( ';', $parts ),
		);
	}

	public static function sanitize_popout_item( array $item ) {
		$content = isset( $item['popout_content'] ) ? Rsmspark_Rich_Text_Body::sanitize( $item['popout_content'] ) : '';
		$title   = isset( $item['popout_title'] ) ? Rsmspark_Content_Widgets_Headline_Styles::sanitize_line_html( $item['popout_title'] ) : '';
		$subtitle = isset( $item['popout_subtitle'] ) ? Rsmspark_Content_Widgets_Headline_Styles::sanitize_line_html( $item['popout_subtitle'] ) : '';
		$label   = isset( $item['chip_label'] ) ? sanitize_text_field( $item['chip_label'] ) : '';

		if (
			'' === trim( wp_strip_all_tags( $content ) )
			&& ! Rsmspark_Content_Widgets_Headline_Styles::line_has_text( $title )
			&& ! Rsmspark_Content_Widgets_Headline_Styles::line_has_text( $subtitle )
		) {
			return null;
		}

		return array(
			'chip_label'              => $label,
			'popout_title'            => $title,
			'popout_subtitle'         => $subtitle,
			'popout_content'          => $content,
			'popout_media'            => isset( $item['popout_media'] ) ? sanitize_key( (string) $item['popout_media'] ) : 'inherit',
			'popout_icon'                     => Rsmspark_Content_Widgets_Features_Styles::normalize_icon_classes( $item['popout_icon'] ?? '' ),
			'popout_icon_style'               => Rsmspark_Content_Widgets_Features_Styles::sanitize_icon_style( $item['popout_icon_style'] ?? 'glyph' ),
			'popout_icon_color'               => sanitize_hex_color( (string) ( $item['popout_icon_color'] ?? '' ) ) ?: '',
			'popout_icon_badge_circle_color'  => sanitize_hex_color( (string) ( $item['popout_icon_badge_circle_color'] ?? '' ) ) ?: '',
			'popout_icon_badge_text_color'    => sanitize_hex_color( (string) ( $item['popout_icon_badge_text_color'] ?? '' ) ) ?: '',
			'popout_icon_badge_glyph'         => sanitize_key( (string) ( $item['popout_icon_badge_glyph'] ?? '' ) ),
			'popout_image'            => $item['popout_image'] ?? '',
			'popout_icon_size'        => isset( $item['popout_icon_size'] ) ? sanitize_key( (string) $item['popout_icon_size'] ) : 'inherit',
			'popout_icon_size_custom' => isset( $item['popout_icon_size_custom'] ) ? (int) $item['popout_icon_size_custom'] : 0,
		);
	}

	/**
	 * Popout headline size. Zero uses a modal-scale clamp, not chip title size.
	 *
	 * @param int $size Slider value in px.
	 * @return string
	 */
	public static function popout_title_size_to_css( $size ) {
		$size = (int) $size;
		if ( $size <= 0 ) {
			return 'clamp(1.125rem, 0.9rem + 0.5vw, 1.5rem)';
		}
		return Rsmspark_Content_Widgets_Features_Styles::font_size_to_css( $size );
	}

	/**
	 * Popout sub-headline size. Zero scales proportionally below the title.
	 *
	 * @param int $size Slider value in px.
	 * @return string
	 */
	public static function popout_subtitle_size_to_css( $size ) {
		$size = (int) $size;
		if ( $size <= 0 ) {
			return 'clamp(0.8125rem, 0.75rem + 0.2vw, 0.875rem)';
		}
		return Rsmspark_Content_Widgets_Features_Styles::font_size_to_css( $size );
	}

	/**
	 * Whether the popout header divider is enabled.
	 *
	 * @param mixed $value Raw checkbox value.
	 * @return bool
	 */
	public static function is_header_divider_enabled( $value ) {
		return self::is_enabled( $value );
	}

	/**
	 * Header icon scale relative to the resolved chip/popout icon (1 = 100%).
	 *
	 * @param mixed $value Slider percent (100–200).
	 * @return string Unitless multiplier for CSS calc().
	 */
	public static function header_icon_scale( $value ) {
		$pct = isset( $value ) ? (int) $value : 125;
		$pct = max( 100, min( 200, $pct ) );
		return (string) round( $pct / 100, 2 );
	}

	/**
	 * CSS variables for the popout dialog panel.
	 *
	 * @param array<string, mixed> $popouts Popouts section values.
	 * @return array<string, string>
	 */
	public static function build_css_variables( array $popouts ) {
		return self::popout_css_variable_map( $popouts );
	}

	/**
	 * Inline style string for popout modal surfaces (dialog + panel).
	 *
	 * @param array<string, mixed> $popouts Popouts section values.
	 * @return string
	 */
	public static function build_inline_style( array $popouts ) {
		$pairs = array();
		foreach ( self::popout_css_variable_map( $popouts ) as $key => $value ) {
			$pairs[] = $key . ':' . $value;
		}
		return implode( ';', $pairs );
	}

	/**
	 * Header icon-to-text gap in pixels (0–48).
	 *
	 * @param array<string, mixed> $popouts Popouts section values.
	 * @return int
	 */
	public static function header_gap_px( array $popouts ) {
		return max( 0, min( 48, (int) ( $popouts['popout_header_gap'] ?? 12 ) ) );
	}

	/**
	 * Popout design tokens as CSS custom properties.
	 *
	 * @param array<string, mixed> $popouts Popouts section values.
	 * @return array<string, string>
	 */
	public static function popout_css_variable_map( array $popouts ) {
		$px = static function ( $v, $fallback = 0 ) {
			return max( 0, (int) $v ) . 'px';
		};

		$shape = isset( $popouts['popout_shape'] ) ? sanitize_key( (string) $popouts['popout_shape'] ) : 'medium';
		if ( ! in_array( $shape, Rsmspark_Content_Widgets_Features_Styles::shape_keys(), true ) || 'inherit' === $shape ) {
			$shape = 'medium';
		}

		$title_font = Rsmspark_Content_Widgets_Features_Fonts::get_stack( $popouts['popout_title_font'] ?? 'inherit' );
		$body_font  = Rsmspark_Content_Widgets_Features_Fonts::get_stack( $popouts['popout_body_font'] ?? 'inherit' );

		$overlay_hex = isset( $popouts['popout_overlay_color'] ) ? sanitize_hex_color( (string) $popouts['popout_overlay_color'] ) : '';
		if ( ! $overlay_hex ) {
			$overlay_hex = '#0f172a';
		}
		$overlay_opacity = isset( $popouts['popout_overlay_opacity'] ) ? (int) $popouts['popout_overlay_opacity'] : 70;
		$divider_enabled = self::is_header_divider_enabled( $popouts['popout_header_divider'] ?? false );
		$divider_width   = $divider_enabled ? max( 1, min( 8, (int) ( $popouts['popout_header_divider_width'] ?? 1 ) ) ) : 0;

		return array(
			'--rf-popout-overlay'              => self::hex_to_rgba( $overlay_hex, $overlay_opacity ),
			'--rf-popout-bg'                   => $popouts['popout_bg'] ?? '#ffffff',
			'--rf-popout-border'               => $popouts['popout_border_color'] ?? '#e2e8f0',
			'--rf-popout-border-width'         => $px( $popouts['popout_border_width'] ?? 1 ),
			'--rf-popout-radius'               => Rsmspark_Content_Widgets_Features_Styles::shape_to_radius( $shape ),
			'--rf-popout-title-color'          => $popouts['popout_title_color'] ?? '#0f172a',
			'--rf-popout-subtitle-color'       => $popouts['popout_subtitle_color'] ?? '#64748b',
			'--rf-popout-text-color'           => $popouts['popout_text_color'] ?? '#475569',
			'--rf-popout-pad'                  => $px( $popouts['popout_pad'] ?? 28 ),
			'--rf-popout-header-gap'           => $px( $popouts['popout_header_gap'] ?? 12 ),
			'--rf-popout-title-font'           => $title_font,
			'--rf-popout-title-size'           => self::popout_title_size_to_css( $popouts['popout_title_font_size'] ?? 0 ),
			'--rf-popout-subtitle-size'        => self::popout_subtitle_size_to_css( $popouts['popout_subtitle_font_size'] ?? 0 ),
			'--rf-popout-header-icon-scale'    => self::header_icon_scale( $popouts['popout_header_icon_scale'] ?? 125 ),
			'--rf-popout-header-divider-color' => $popouts['popout_header_divider_color'] ?? '#e2e8f0',
			'--rf-popout-header-divider-width' => $px( $divider_width ),
			'--rf-popout-body-font'            => $body_font,
			'--rf-popout-body-size'            => Rsmspark_Content_Widgets_Features_Styles::font_size_to_css( $popouts['popout_body_font_size'] ?? 0 ),
			'--rf-popout-max-width'            => max( 280, min( 1280, (int) ( $popouts['popout_max_width'] ?? 640 ) ) ) . 'px',
			'--rf-popout-max-height'           => max( 400, min( 960, (int) ( $popouts['popout_max_height'] ?? 720 ) ) ) . 'px',
		);
	}

	/**
	 * Register popout layout shortcodes for the TinyMCE content field.
	 *
	 * @return void
	 */
	public static function register_shortcodes() {
		add_shortcode( 'rsmspark_cols', array( __CLASS__, 'shortcode_cols' ) );
		add_shortcode( 'rsmspark_col', array( __CLASS__, 'shortcode_col' ) );
		add_shortcode( 'rsmspark_list', array( __CLASS__, 'shortcode_list' ) );
	}

	/**
	 * Render popout body HTML (shortcodes + safe tags).
	 *
	 * @param string $content Raw editor content.
	 * @return string
	 */
	public static function render_content( $content ) {
		$content = (string) $content;
		if ( '' === $content ) {
			return '';
		}

		$content = shortcode_unautop( $content );
		$content = do_shortcode( $content );

		return self::kses_popout_html( $content );
	}

	/**
	 * Shared data attributes for RSM embeds (Forms, Calendar, etc.) in popout HTML.
	 *
	 * @return array<string, bool>
	 */
	private static function embed_kses_attributes() {
		return array(
			'class'                          => true,
			'id'                             => true,
			'style'                          => true,
			'role'                           => true,
			'tabindex'                       => true,
			'hidden'                         => true,
			'aria-hidden'                    => true,
			'aria-label'                     => true,
			'aria-pressed'                   => true,
			'aria-live'                      => true,
			'aria-modal'                     => true,
			'aria-labelledby'                => true,
			'aria-required'                  => true,
			'placeholder'                    => true,
			'type'                           => true,
			'name'                           => true,
			'value'                          => true,
			'for'                            => true,
			'method'                         => true,
			'action'                         => true,
			'enctype'                        => true,
			'autocomplete'                   => true,
			'required'                       => true,
			'checked'                        => true,
			'selected'                       => true,
			'disabled'                       => true,
			'readonly'                       => true,
			'multiple'                       => true,
			'accept'                         => true,
			'rows'                           => true,
			'cols'                           => true,
			'min'                            => true,
			'max'                            => true,
			'step'                           => true,
			'maxlength'                      => true,
			'pattern'                        => true,
			'src'                            => true,
			'alt'                            => true,
			'loading'                        => true,
			'decoding'                       => true,
			'width'                          => true,
			'height'                         => true,
			'href'                           => true,
			'target'                         => true,
			'rel'                            => true,
			'data-rsmspark-ajax'                  => true,
			'data-rsmspark-form-id'               => true,
			'data-rsmspark-schema'                => true,
			'data-rsmspark-use-recaptcha'         => true,
			'data-rsmspark-recaptcha-v3'          => true,
			'data-rsmspark-recaptcha-site'        => true,
			'data-rsmspark-use-turnstile'         => true,
			'data-rsmspark-turnstile-site'        => true,
			'data-rsmspark-container-border-shape' => true,
			'data-rsmspark-field-id'              => true,
			'data-rsmspark-field-slug'            => true,
			'data-rsmspark-field-type'            => true,
			'data-rsmspark-row'                   => true,
			'data-rsmspark-page-index'            => true,
			'data-rsmspark-payments'              => true,
			'data-rsmspark-payment-config'        => true,
			'data-rsmspark-payment-status'        => true,
			'data-rsmspark-save'                  => true,
			'data-rsmspark-preview'               => true,
			'data-rsmspark-gateway'               => true,
			'data-rsmspark-calendar'              => true,
			'data-rsmspark-calendar-ajax'         => true,
			'data-atts-b64'                  => true,
			'data-atts'                      => true,
			'data-openrsm-month-key'         => true,
			'data-openrsm-month'             => true,
			'data-openrsm-month-select'      => true,
			'data-openrsm-calendar-id'       => true,
			'data-openrsm-tab'               => true,
			'data-openrsm-panel'             => true,
			'data-openrsm-source-filter'     => true,
			'data-openrsm-source-index'      => true,
			'data-openrsm-toggle-list'       => true,
			'data-openrsm-event'             => true,
			'data-openrsm-event-card'        => true,
			'data-openrsm-loaded'            => true,
			'data-date'                      => true,
			'data-sitekey'                   => true,
		);
	}

	/**
	 * Sanitize popout HTML while preserving RSM Forms, Calendar, and other embed markup.
	 *
	 * @param string $content HTML after shortcodes run.
	 * @return string
	 */
	public static function kses_popout_html( $content ) {
		$allowed = wp_kses_allowed_html( 'post' );
		$embed   = self::embed_kses_attributes();

		foreach ( array( 'div', 'section', 'nav', 'button', 'a', 'select', 'option', 'input', 'label', 'span', 'article', 'time', 'img', 'fieldset', 'legend', 'textarea', 'form' ) as $tag ) {
			if ( ! isset( $allowed[ $tag ] ) ) {
				$allowed[ $tag ] = array();
			}
			$allowed[ $tag ] = array_merge( $allowed[ $tag ], $embed );
		}

		return wp_kses( (string) $content, $allowed );
	}

	/**
	 * Sanitize a shortcode color (hex or common name → hex).
	 *
	 * @param string $color Raw color value.
	 * @return string Hex color or empty when invalid.
	 */
	public static function sanitize_shortcode_color( $color ) {
		$color = trim( (string) $color );
		if ( '' === $color ) {
			return '';
		}

		$hex = sanitize_hex_color( $color );
		if ( $hex ) {
			return $hex;
		}

		$names = array(
			'black'  => '#0f172a',
			'blue'   => '#2563eb',
			'gray'   => '#64748b',
			'grey'   => '#64748b',
			'green'  => '#16a34a',
			'orange' => '#ea580c',
			'purple' => '#7c3aed',
			'red'    => '#dc2626',
			'teal'   => '#0d9488',
			'white'  => '#ffffff',
			'yellow' => '#ca8a04',
		);
		$key = strtolower( $color );

		return isset( $names[ $key ] ) ? $names[ $key ] : '';
	}

	/**
	 * Normalize list marker style keyword.
	 *
	 * @param string $style Raw style (check, x, bullet, none).
	 * @return string Sanitized style or empty.
	 */
	public static function sanitize_list_style( $style ) {
		$style = sanitize_key( (string) $style );
		$aliases = array(
			'checkmark' => 'check',
			'cross'     => 'x',
			'dot'       => 'bullet',
			'favorite'  => 'star',
			'no'        => 'x',
			'yes'       => 'check',
		);
		if ( isset( $aliases[ $style ] ) ) {
			$style = $aliases[ $style ];
		}

		return in_array( $style, array( 'check', 'x', 'bullet', 'none', 'star' ), true ) ? $style : '';
	}

	/**
	 * Pick list style from shortcode attributes (supports several aliases).
	 *
	 * @param array<string, string> $atts    Shortcode attributes.
	 * @param string                $default Fallback when unset/invalid.
	 * @return string
	 */
	public static function resolve_list_style_from_atts( array $atts, $default = '' ) {
		foreach ( array( 'list_style', 'list', 'type', 'style' ) as $key ) {
			if ( ! empty( $atts[ $key ] ) ) {
				$style = self::sanitize_list_style( $atts[ $key ] );
				if ( '' !== $style ) {
					return $style;
				}
			}
		}

		return $default;
	}

	/**
	 *
	 * @param string $marker_color Raw marker color.
	 * @return string HTML style attribute or empty.
	 */
	public static function list_marker_style_attr( $marker_color ) {
		$marker_color = self::sanitize_shortcode_color( $marker_color );
		if ( '' === $marker_color ) {
			return '';
		}

		return ' style="--rf-list-marker-color:' . esc_attr( $marker_color ) . '"';
	}

	/**
	 * Two-column row wrapper.
	 *
	 * @param array<string, string>|string $atts    Attributes.
	 * @param string|null                  $content Enclosed shortcodes / HTML.
	 * @return string
	 */
	public static function shortcode_cols( $atts, $content = null ) {
		unset( $atts );
		$content = is_string( $content ) ? trim( $content ) : '';
		if ( '' === $content ) {
			return '';
		}

		return self::kses_popout_html(
			'<div class="rsmspark-features__popout-cols">' . do_shortcode( $content ) . '</div>'
		);
	}

	/**
	 * Single column inside rf_cols.
	 *
	 * @param array<string, string>|string $atts    Attributes.
	 * @param string|null                  $content Column body HTML.
	 * @return string
	 */
	public static function shortcode_col( $atts, $content = null ) {
		$atts = shortcode_atts(
			array(
				'title'        => '',
				'title_color'  => '',
				'color'        => '',
				'list_style'   => '',
				'list'         => '',
				'type'         => '',
				'style'        => '',
				'marker_color' => '',
				'dot_color'    => '',
			),
			is_array( $atts ) ? $atts : array(),
			'rsmspark_col'
		);

		$list_style   = self::resolve_list_style_from_atts( $atts );
		$marker_color = '' !== $atts['marker_color'] ? $atts['marker_color'] : $atts['dot_color'];

		$col_classes = array( 'rsmspark-features__popout-col' );
		if ( '' !== $list_style ) {
			$col_classes[] = 'rsmspark-features__popout-col--list-' . $list_style;
		}

		$content   = is_string( $content ) ? trim( $content ) : '';
		$col_attrs = self::list_marker_style_attr( $marker_color );
		$html      = '<div class="' . esc_attr( implode( ' ', $col_classes ) ) . '"' . $col_attrs . '>';

		if ( '' !== $atts['title'] ) {
			$title_color = self::sanitize_shortcode_color(
				'' !== $atts['title_color'] ? $atts['title_color'] : $atts['color']
			);
			$title_attrs = '';
			if ( '' !== $title_color ) {
				$title_attrs = ' style="color:' . esc_attr( $title_color ) . '"';
			}
			$html .= '<div class="rsmspark-features__popout-col-title"' . $title_attrs . '><strong>' . esc_html( $atts['title'] ) . '</strong></div>';
		}
		if ( '' !== $content ) {
			$html .= wp_kses_post( shortcode_unautop( $content ) );
		}
		$html .= '</div>';

		return self::kses_popout_html( $html );
	}

	/**
	 * Styled list (check, x, or bullet with optional marker color).
	 *
	 * @param array<string, string>|string $atts    Attributes.
	 * @param string|null                  $content List items (li HTML).
	 * @return string
	 */
	public static function shortcode_list( $atts, $content = null ) {
		$atts = shortcode_atts(
			array(
				'type'         => 'bullet',
				'style'        => '',
				'list_style'   => '',
				'marker_color' => '',
				'dot_color'    => '',
			),
			is_array( $atts ) ? $atts : array(),
			'rsmspark_list'
		);

		$list_style = self::resolve_list_style_from_atts( $atts, 'bullet' );
		if ( '' === $list_style ) {
			$list_style = 'bullet';
		}

		$marker_color = '' !== $atts['marker_color'] ? $atts['marker_color'] : $atts['dot_color'];
		$content      = is_string( $content ) ? trim( $content ) : '';
		if ( '' === $content ) {
			return '';
		}

		if ( preg_match( '#^\s*<ul[^>]*>(.*)</ul>\s*$#is', $content, $matches ) ) {
			$content = trim( $matches[1] );
		}

		return self::kses_popout_html(
			sprintf(
				'<ul class="rsmspark-features__popout-list rsmspark-features__popout-list--%1$s"%2$s>%3$s</ul>',
				esc_attr( $list_style ),
				self::list_marker_style_attr( $marker_color ),
				wp_kses_post( shortcode_unautop( $content ) )
			)
		);
	}
}
