<?php
defined( 'ABSPATH' ) || exit;

/**
 * WooCommerce Single Product.
 *
 * Renders a complete, composable single-product layout: gallery, title,
 * rating, price, excerpt, add-to-cart, meta, share, tabs, related and upsells
 * — each independently toggleable and ordered by the chosen layout.
 *
 * RENDERING STRATEGY (hybrid, mirroring Elementor)
 *  • Own markup for gallery / title / rating / price / excerpt / meta / share
 *    and the SIMPLE-product cart form, so every Style-tab control has a real
 *    selector to target.
 *  • WooCommerce template hooks for product tabs, related products and upsells
 *    (woocommerce_output_product_data_tabs() etc.) so plugin-injected tabs and
 *    WC's own settings keep working.
 *  • VARIABLE / grouped / external products delegate their cart form to
 *    woocommerce_template_single_add_to_cart(), which loads WC's variation
 *    script and handles price/stock/image updates — reimplementing that would
 *    break variation-swatch plugins.
 *
 * Those hooks require a global $product/$post, so render() sets up the loop
 * context around them and ALWAYS restores it (see with_product_context()).
 *
 * Contract, as with every element: ONE root wrapper, scoped
 * `.jalpi-el.jalpi-{id}` CSS via the renderer, everything escaped, and all
 * free-text CSS through self::css_value() (NOT bare sanitize_css_value).
 */
class Jalpi_Element_Single_Product extends Jalpi_Element {

	public static function type(): string { return 'single-product'; }

	/**
	 * Strict validator for free-text CSS values.
	 *
	 * Jalpi_Sanitizer::sanitize_css_value() strips script vectors but PERMITS
	 * braces and semicolons, letting a value terminate its declaration block
	 * and inject page-wide rules. Everything interpolated into a stylesheet
	 * here goes through this instead.
	 */
	private static function css_value( string $val, string $default = '' ): string {
		$val = Jalpi_Sanitizer::sanitize_css_value( $val );
		if ( '' === $val ) {
			return $default;
		}
		if ( preg_match( '/[{};@\\\\<>]/', $val ) ) {
			return $default;
		}
		if ( false !== strpos( $val, '/*' ) || false !== strpos( $val, '*/' ) ) {
			return $default;
		}
		if ( ! preg_match( '/^[A-Za-z0-9_\-\s.,%#()\/]+$/', $val ) ) {
			return $default;
		}
		return $val;
	}

	public function render( array $node, Jalpi_Renderer $renderer ): string {
		$id       = sanitize_html_class( $node['id'] ?? '' );
		$content  = (array) ( $node['content']  ?? [] );
		$styles   = (array) ( $node['styles']   ?? [] );
		$settings = (array) ( $node['settings'] ?? [] );
		$a        = $renderer->get_css_attrs( $node );
		$sel      = '.jalpi-el.jalpi-' . $id;

		// ── Helpers ──────────────────────────────────────────────────────────
		$raw = static fn( string $k, string $d = '' ): string =>
			isset( $settings[ $k ] ) && '' !== $settings[ $k ] ? (string) $settings[ $k ] : $d;

		$px = static function ( $v, string $d = '' ): string {
			$v = (string) $v;
			if ( '' === $v ) return $d;
			if ( preg_match( '/^-?[\d.]+$/', $v ) ) return '0' === $v ? '0' : $v . 'px';
			return self::css_value( $v, $d );
		};
		$enum = static fn( string $k, array $allowed, string $d ): string =>
			in_array( $raw( $k, $d ), $allowed, true ) ? $raw( $k, $d ) : $d;

		$cenum = static function ( string $k, array $allowed, string $d ) use ( $content ): string {
			$v = (string) ( $content[ $k ] ?? $d );
			return in_array( $v, $allowed, true ) ? $v : $d;
		};
		$on = static fn( string $k, bool $d ): bool =>
			array_key_exists( $k, $content ) ? ! empty( $content[ $k ] ) : $d;

		// ── Resolve the product ──────────────────────────────────────────────
		$product = $this->resolve_product( $content );

		// ── Layout / structure ───────────────────────────────────────────────
		$layout   = $cenum( 'layout',        [ 'default', 'modern', 'classic', 'custom' ], 'default' );
		$gal_pos  = $cenum( 'galleryPosition', [ 'left', 'right', 'top' ], 'left' );
		$thumb_p  = $cenum( 'thumbPosition',   [ 'vertical', 'horizontal' ], 'horizontal' );
		$img_link = $cenum( 'imageLink',       [ 'lightbox', 'product', 'none' ], 'lightbox' );
		$title_tag = $cenum( 'titleTag', [ 'h1', 'h2', 'h3', 'div' ], 'h1' );

		$show = [
			'gallery'  => $on( 'showGallery',   true ),
			'title'    => $on( 'showTitle',     true ),
			'rating'   => $on( 'showRating',    true ),
			'price'    => $on( 'showPrice',     true ),
			'excerpt'  => $on( 'showExcerpt',   true ),
			'cart'     => $on( 'showAddToCart', true ),
			'meta'     => $on( 'showMeta',      true ),
			'share'    => $on( 'showShare',     false ),
			'tabs'     => $on( 'showTabs',      true ),
			'related'  => $on( 'showRelated',   true ),
			'upsells'  => $on( 'showUpsells',   false ),
		];

		// ── CSS ──────────────────────────────────────────────────────────────
		$this->emit_css( $renderer, $sel, $settings, [
			'gal_pos' => $gal_pos, 'thumb_p' => $thumb_p, 'layout' => $layout,
		], $raw, $px, $enum );

		$renderer->add_css( $sel . '{box-sizing:border-box;'
			. $renderer->bg_data_to_css( (array) ( $settings['bgNormal'] ?? [] ) )
			. $renderer->border_data_to_css( (array) ( $settings['borderNormal'] ?? [] ) )
			. $renderer->styles_to_css( $styles ) . '}' );

		$hover_css = $renderer->bg_data_to_css( (array) ( $settings['bgHover'] ?? [] ) )
			. $renderer->border_data_to_css( (array) ( $settings['borderHover'] ?? [] ) );
		if ( $hover_css ) {
			$renderer->add_css( $sel . ':hover{' . $hover_css . '}' );
		}

		// Gallery thumbs, tabs and AJAX cart all need the frontend script.
		$renderer->set_needs_js( 'generic' );

		$classes = 'jalpi-el jalpi-' . esc_attr( $id ) . ' jalpi-sp jalpi-sp--' . esc_attr( $layout )
			. ' jalpi-sp--gal-' . esc_attr( $gal_pos ) . $a['extra_cls'];

		// ── No product resolvable → placeholder or notice ────────────────────
		if ( ! $product ) {
			return '<div class="' . $classes . '"' . $a['id_attr'] . '>'
				. $this->render_notice( $content ) . '</div>';
		}

		$is_placeholder = $product instanceof Jalpi_Woo_Product_Placeholder;

		// ── Body ─────────────────────────────────────────────────────────────
		$opts = compact( 'show', 'title_tag', 'img_link', 'thumb_p', 'is_placeholder' )
			+ [ 'content' => $content, 'settings' => $settings ];

		$main = '';
		if ( $show['gallery'] ) {
			$main .= '<div class="jalpi-sp__gallery-col">' . $this->render_gallery( $product, $opts ) . '</div>';
		}
		$main .= '<div class="jalpi-sp__summary">' . $this->render_summary( $product, $opts ) . '</div>';

		$out  = '<div class="' . $classes . '"' . $a['id_attr']
			. ' data-jalpi-sp="' . esc_attr( wp_json_encode( [
				'id'   => $is_placeholder ? 0 : (int) $product->get_id(),
				'ajax' => (bool) $on( 'ajaxAddToCart', true ),
			] ) ) . '">';
		$out .= '<div class="jalpi-sp__main">' . $main . '</div>';

		// ── Hook-driven sections (tabs / related / upsells) ──────────────────
		// These call WooCommerce template functions, which read the global
		// $product and $post — hence the context wrapper.
		if ( ! $is_placeholder && ( $show['tabs'] || $show['related'] || $show['upsells'] ) ) {
			$out .= $this->with_product_context( $product, function () use ( $show, $content ) {
				$html = '';

				if ( $show['tabs'] ) {
					$html .= '<div class="jalpi-sp__tabs">'
						. $this->capture( 'woocommerce_output_product_data_tabs' )
						. '</div>';
				}

				if ( $show['upsells'] ) {
					$limit = max( 1, min( 24, (int) ( $content['upsellsCount']   ?? 4 ) ) );
					$cols  = max( 1, min( 6,  (int) ( $content['upsellsColumns'] ?? 4 ) ) );
					$html .= '<div class="jalpi-sp__upsells">'
						. $this->capture( 'woocommerce_upsell_display', [ $limit, $cols ] )
						. '</div>';
				}

				if ( $show['related'] ) {
					$limit = max( 1, min( 24, (int) ( $content['relatedCount']   ?? 4 ) ) );
					$cols  = max( 1, min( 6,  (int) ( $content['relatedColumns'] ?? 4 ) ) );
					$orderby = (string) ( $content['relatedOrderBy'] ?? 'rand' );
					$orderby = in_array( $orderby, [ 'rand', 'date', 'price', 'title', 'popularity' ], true ) ? $orderby : 'rand';

					// woocommerce_related_products() takes its args via a filter.
					$filter = static function ( $args ) use ( $limit, $cols, $orderby ) {
						$args['posts_per_page'] = $limit;
						$args['columns']        = $cols;
						$args['orderby']        = $orderby;
						return $args;
					};
					add_filter( 'woocommerce_output_related_products_args', $filter, 20 );
					$html .= '<div class="jalpi-sp__related">'
						. $this->capture( 'woocommerce_output_related_products' )
						. '</div>';
					remove_filter( 'woocommerce_output_related_products_args', $filter, 20 );
				}

				return $html;
			} );
		} elseif ( $is_placeholder && ( $show['tabs'] || $show['related'] || $show['upsells'] ) ) {
			$out .= $this->render_placeholder_sections( $show );
		}

		$out .= '</div>';
		return $out;
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Product resolution + WooCommerce context
	// ─────────────────────────────────────────────────────────────────────────

	/**
	 * Resolve the product to render from the widget's source setting.
	 *
	 * @return WC_Product|Jalpi_Woo_Product_Placeholder|null
	 */
	private function resolve_product( array $content ) {
		if ( ! self::woo_active() ) {
			return new Jalpi_Woo_Product_Placeholder();
		}

		$source = (string) ( $content['source'] ?? 'current' );
		$source = in_array( $source, [ 'current', 'select', 'custom' ], true ) ? $source : 'current';

		$product_id = 0;

		if ( 'current' === $source ) {
			// Theme Builder / single-product view: the loop's post IS the product.
			$product_id = (int) get_the_ID();

			// In the editor or on a non-product page there is no current
			// product; fall back to the chosen preview product so the layout
			// stays visible instead of collapsing to a notice.
			if ( ! $product_id || 'product' !== get_post_type( $product_id ) ) {
				$product_id = max( 0, (int) ( $content['previewProduct'] ?? 0 ) );
			}
		} else {
			$key        = 'select' === $source ? 'productId' : 'customProductId';
			$product_id = max( 0, (int) ( $content[ $key ] ?? 0 ) );
		}

		if ( ! $product_id ) {
			return self::woo_active() ? null : new Jalpi_Woo_Product_Placeholder();
		}

		$product = wc_get_product( $product_id );
		if ( ! $product instanceof WC_Product ) {
			return null;
		}

		// Never surface a draft/private product to a visitor who cannot see it.
		$post_obj = get_post( $product_id );
		if ( $post_obj && 'publish' !== $post_obj->post_status && ! current_user_can( 'read_post', $product_id ) ) {
			return null;
		}

		return $product;
	}

	public static function woo_active(): bool {
		return class_exists( 'WooCommerce' ) && function_exists( 'wc_get_product' );
	}

	/**
	 * Run $fn with the WooCommerce loop globals pointed at $product, then
	 * restore them — WC template functions read global $product/$post rather
	 * than taking arguments.
	 *
	 * Restoration happens in a finally block so a fatal inside a third-party
	 * tab callback cannot leave the globals pointing at the wrong post.
	 */
	private function with_product_context( $product, callable $fn ): string {
		$prev_product = $GLOBALS['product'] ?? null;
		$prev_post    = $GLOBALS['post']    ?? null;

		// WooCommerce template functions read the global `$product` (and
		// `$post`) rather than taking arguments, so these names are fixed by
		// WooCommerce's contract — a jalpi_ prefix would simply be ignored by
		// every wc_get_template() call. Both are restored in the finally block.
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WooCommerce template global, name fixed by WC
		$GLOBALS['product'] = $product;
		$product_post       = get_post( $product->get_id() );
		if ( $product_post ) {
			$GLOBALS['post'] = $product_post;
			setup_postdata( $product_post );
		}

		try {
			return (string) $fn();
		} finally {
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- restoring the WooCommerce template global set above
			$GLOBALS['product'] = $prev_product;
			$GLOBALS['post']    = $prev_post;
			wp_reset_postdata();
		}
	}

	/** Buffer a WooCommerce template function's echoed output. */
	private function capture( string $callable, array $args = [] ): string {
		if ( ! function_exists( $callable ) ) {
			return '';
		}
		ob_start();
		try {
			call_user_func_array( $callable, $args );
		} catch ( \Throwable $e ) {
			// A broken third-party tab must not take down the whole page.
			ob_end_clean();
			return '';
		}
		return (string) ob_get_clean();
	}

	// ─────────────────────────────────────────────────────────────────────────
	// Sections
	// ─────────────────────────────────────────────────────────────────────────

	private function render_gallery( $product, array $opts ): string {
		$content = $opts['content'];
		$size    = sanitize_key( (string) ( $opts['settings']['sp_img_size'] ?? '' ) ) ?: 'woocommerce_single';
		$lazy    = ! array_key_exists( 'lazyLoad', $content ) || ! empty( $content['lazyLoad'] );
		$thumbs  = ! array_key_exists( 'showThumbs', $content ) || ! empty( $content['showThumbs'] );

		if ( $opts['is_placeholder'] ) {
			$main = '<img class="jalpi-sp__img" src="' . esc_url( $product->get_image_url() ) . '" alt="" />';
			$out  = '<div class="jalpi-sp__gallery jalpi-sp__gallery--' . esc_attr( $opts['thumb_p'] ) . '">';
			$out .= '<div class="jalpi-sp__stage">' . $main . '</div>';
			if ( $thumbs ) {
				$out .= '<div class="jalpi-sp__thumbs">';
				for ( $i = 0; $i < 3; $i++ ) {
					$out .= '<button type="button" class="jalpi-sp__thumb' . ( 0 === $i ? ' is-active' : '' ) . '">'
						. '<img src="' . esc_url( $product->get_image_url() ) . '" alt="" /></button>';
				}
				$out .= '</div>';
			}
			return $out . '</div>';
		}

		// Main image + gallery images, deduplicated.
		$ids = [];
		$featured = (int) $product->get_image_id();
		if ( $featured ) {
			$ids[] = $featured;
		}
		foreach ( $product->get_gallery_image_ids() as $gid ) {
			$gid = (int) $gid;
			if ( $gid && ! in_array( $gid, $ids, true ) ) {
				$ids[] = $gid;
			}
		}

		$out = '<div class="jalpi-sp__gallery jalpi-sp__gallery--' . esc_attr( $opts['thumb_p'] ) . '">';

		if ( ! $ids ) {
			$src  = function_exists( 'wc_placeholder_img_src' ) ? wc_placeholder_img_src( $size ) : '';
			$out .= '<div class="jalpi-sp__stage"><img class="jalpi-sp__img" src="' . esc_url( $src )
				. '" alt="' . esc_attr( $product->get_name() ) . '" /></div></div>';
			return $out;
		}

		// Stage: all images stacked; JS toggles the active one.
		$out .= '<div class="jalpi-sp__stage">';
		foreach ( $ids as $i => $img_id ) {
			$full = wp_get_attachment_image_url( $img_id, 'full' );
			$img  = wp_get_attachment_image( $img_id, $size, false, [
				'class'   => 'jalpi-sp__img',
				'loading' => ( $lazy && $i > 0 ) ? 'lazy' : 'eager',
			] );
			if ( ! $img ) {
				continue;
			}

			$slide = '<div class="jalpi-sp__slide' . ( 0 === $i ? ' is-active' : '' ) . '"'
				. ' data-index="' . esc_attr( (string) $i ) . '">';

			if ( 'lightbox' === $opts['img_link'] && $full ) {
				$slide .= '<a class="jalpi-sp__zoom" href="' . esc_url( $full ) . '"'
					. ' data-jalpi-lightbox="sp" aria-label="'
					. esc_attr__( 'View full size image', 'jalpi-page-builder' ) . '">' . $img . '</a>';
			} elseif ( 'product' === $opts['img_link'] ) {
				$slide .= '<a href="' . esc_url( $product->get_permalink() ) . '">' . $img . '</a>';
			} else {
				$slide .= $img;
			}

			$out .= $slide . '</div>';
		}
		$out .= '</div>';

		// Thumbnails
		if ( $thumbs && count( $ids ) > 1 ) {
			$out .= '<div class="jalpi-sp__thumbs" role="tablist">';
			foreach ( $ids as $i => $img_id ) {
				$thumb = wp_get_attachment_image( $img_id, 'woocommerce_gallery_thumbnail', false, [
					'loading' => 'lazy',
					'alt'     => '',
				] );
				if ( ! $thumb ) {
					continue;
				}
				$out .= '<button type="button" class="jalpi-sp__thumb' . ( 0 === $i ? ' is-active' : '' ) . '"'
					. ' data-index="' . esc_attr( (string) $i ) . '" role="tab"'
					. ' aria-selected="' . ( 0 === $i ? 'true' : 'false' ) . '"'
					. ' aria-label="' . esc_attr( sprintf(
						/* translators: %d: image number */
						__( 'Show image %d', 'jalpi-page-builder' ), $i + 1
					) ) . '">' . $thumb . '</button>';
			}
			$out .= '</div>';
		}

		return $out . '</div>';
	}

	private function render_summary( $product, array $opts ): string {
		$show    = $opts['show'];
		$content = $opts['content'];
		$out     = '';

		if ( $show['title'] ) {
			$tag  = $opts['title_tag'];
			$out .= '<' . $tag . ' class="jalpi-sp__title">' . esc_html( $product->get_name() ) . '</' . $tag . '>';
		}

		if ( $show['rating'] ) {
			$out .= $this->render_rating( $product, $content );
		}

		if ( $show['price'] ) {
			$out .= $this->render_price( $product, $content );
		}

		if ( $show['excerpt'] ) {
			$short = $product->get_short_description();
			if ( $short ) {
				$out .= '<div class="jalpi-sp__excerpt">' . wp_kses_post( wpautop( $short ) ) . '</div>';
			}
		}

		if ( $show['cart'] ) {
			$out .= $this->render_cart( $product, $opts );
		}

		if ( $show['meta'] ) {
			$out .= $this->render_meta( $product, $content, $opts['is_placeholder'] );
		}

		if ( $show['share'] ) {
			$out .= $this->render_share( $product, $content, $opts['is_placeholder'] );
		}

		return $out;
	}

	private function render_rating( $product, array $content ): string {
		$avg   = (float) $product->get_average_rating();
		$count = (int) $product->get_review_count();

		if ( ! $count && empty( $content['showEmptyRating'] ) ) {
			return '';
		}

		$pct   = max( 0, min( 100, ( $avg / 5 ) * 100 ) );
		$label = sprintf(
			/* translators: 1: average rating, 2: review count */
			__( 'Rated %1$s out of 5 based on %2$s reviews', 'jalpi-page-builder' ),
			number_format_i18n( $avg, 2 ),
			number_format_i18n( $count )
		);

		$stars = '<span class="jalpi-sp__stars" role="img" aria-label="' . esc_attr( $label ) . '">'
			. '<span class="jalpi-sp__stars-fill" style="width:' . esc_attr( (string) round( $pct, 2 ) ) . '%"></span>'
			. '</span>';

		$out = '<div class="jalpi-sp__rating">' . $stars;

		$show_count = ! array_key_exists( 'showReviewCount', $content ) || ! empty( $content['showReviewCount'] );
		if ( $show_count && $count ) {
			$text = esc_html( sprintf(
				/* translators: %s: review count */
				_n( '%s review', '%s reviews', $count, 'jalpi-page-builder' ),
				number_format_i18n( $count )
			) );

			// Link to the Reviews tab when asked (and when it exists).
			if ( ! empty( $content['linkToReviews'] ) ) {
				$out .= '<a class="jalpi-sp__review-link" href="#reviews">' . $text . '</a>';
			} else {
				$out .= '<span class="jalpi-sp__review-count">' . $text . '</span>';
			}
		}

		return $out . '</div>';
	}

	private function render_price( $product, array $content ): string {
		$show_regular = ! array_key_exists( 'showRegularPrice', $content ) || ! empty( $content['showRegularPrice'] );
		$show_sale    = ! array_key_exists( 'showSalePrice', $content ) || ! empty( $content['showSalePrice'] );

		$html = $product->get_price_html();
		if ( ! $html ) {
			return '';
		}

		// Toggling either off means showing only the other figure; strip the
		// tag WooCommerce uses for the one we're hiding.
		if ( ! $show_regular ) {
			$html = preg_replace( '#<del\b[^>]*>.*?</del>#is', '', $html );
		}
		if ( ! $show_sale ) {
			$html = preg_replace( '#<ins\b[^>]*>(.*?)</ins>#is', '', $html );
		}

		$out = '<div class="jalpi-sp__price">' . wp_kses_post( $html );

		// Discount badge — percentage off, computed from the real prices.
		// Defaults ON when the key is absent, matching every other toggle here
		// and the registry's seed content.
		$show_badge = ! array_key_exists( 'showSaleBadge', $content ) || ! empty( $content['showSaleBadge'] );
		if ( $show_badge && $product->is_on_sale() ) {
			$regular = (float) $product->get_regular_price();
			$sale    = (float) $product->get_sale_price();
			if ( $regular > 0 && $sale > 0 && $sale < $regular ) {
				$pct  = (int) round( ( ( $regular - $sale ) / $regular ) * 100 );
				$out .= '<span class="jalpi-sp__badge">-' . esc_html( number_format_i18n( $pct ) ) . '%</span>';
			} else {
				$out .= '<span class="jalpi-sp__badge">' . esc_html__( 'Sale', 'jalpi-page-builder' ) . '</span>';
			}
		}

		return $out . '</div>';
	}

	private function render_cart( $product, array $opts ): string {
		$content = $opts['content'];

		if ( $opts['is_placeholder'] ) {
			return '<div class="jalpi-sp__cart">'
				. '<span class="jalpi-sp__qty-box"><span class="jalpi-sp__qty">1</span></span>'
				. '<span class="jalpi-sp__btn">' . esc_html__( 'Add to cart', 'jalpi-page-builder' ) . '</span>'
				. '</div>';
		}

		// Variable / grouped / external products: hand off to WooCommerce so
		// variation matching, swatch plugins and stock sync keep working.
		if ( 'simple' !== $product->get_type() ) {
			$html = $this->with_product_context( $product, function () {
				return $this->capture( 'woocommerce_template_single_add_to_cart' );
			} );
			return $html ? '<div class="jalpi-sp__cart jalpi-sp__cart--wc">' . $html . '</div>' : '';
		}

		if ( ! $product->is_purchasable() || ! $product->is_in_stock() ) {
			return '<div class="jalpi-sp__cart"><p class="jalpi-sp__stock jalpi-sp__stock--out">'
				. esc_html__( 'Out of stock', 'jalpi-page-builder' ) . '</p></div>';
		}

		$btn_text = sanitize_text_field( (string) ( $content['buttonText'] ?? '' ) )
			?: $product->single_add_to_cart_text();

		$icon_key = (string) ( $opts['settings']['sp_btn_icon'] ?? 'none' );
		$icon_key = in_array( $icon_key, [ 'none', 'cart', 'bag', 'plus', 'arrow' ], true ) ? $icon_key : 'none';
		$icon_pos = (string) ( $opts['settings']['sp_btn_icon_pos'] ?? 'before' );
		$icon_pos = in_array( $icon_pos, [ 'before', 'after' ], true ) ? $icon_pos : 'before';

		$glyphs = [ 'cart' => '🛒', 'bag' => '🛍', 'plus' => '＋', 'arrow' => '→' ];
		$icon   = ( 'none' !== $icon_key && isset( $glyphs[ $icon_key ] ) )
			? '<span class="jalpi-sp__btn-icon" aria-hidden="true">' . esc_html( $glyphs[ $icon_key ] ) . '</span>'
			: '';

		$label = '<span class="jalpi-sp__btn-text">' . esc_html( $btn_text ) . '</span>';
		$inner = 'after' === $icon_pos ? $label . $icon : $icon . $label;

		$show_qty = ! array_key_exists( 'showQuantity', $content ) || ! empty( $content['showQuantity'] );
		$qty_type = (string) ( $content['quantityType'] ?? 'input' );
		$qty_type = in_array( $qty_type, [ 'input', 'plusminus' ], true ) ? $qty_type : 'input';

		$max = $product->get_max_purchase_quantity();
		$min = $product->get_min_purchase_quantity();

		$out = '<form class="jalpi-sp__cart" method="post" enctype="multipart/form-data"'
			. ' action="' . esc_url( $product->get_permalink() ) . '">';

		if ( $show_qty && ! $product->is_sold_individually() ) {
			$out .= '<div class="jalpi-sp__qty-box jalpi-sp__qty-box--' . esc_attr( $qty_type ) . '">';
			if ( 'plusminus' === $qty_type ) {
				$out .= '<button type="button" class="jalpi-sp__qty-btn" data-step="-1"'
					. ' aria-label="' . esc_attr__( 'Decrease quantity', 'jalpi-page-builder' ) . '">−</button>';
			}
			$out .= '<input type="number" class="jalpi-sp__qty" name="quantity"'
				. ' value="' . esc_attr( (string) $min ) . '"'
				. ' min="' . esc_attr( (string) $min ) . '"'
				. ( $max > 0 ? ' max="' . esc_attr( (string) $max ) . '"' : '' )
				. ' step="1" aria-label="' . esc_attr__( 'Quantity', 'jalpi-page-builder' ) . '" />';
			if ( 'plusminus' === $qty_type ) {
				$out .= '<button type="button" class="jalpi-sp__qty-btn" data-step="1"'
					. ' aria-label="' . esc_attr__( 'Increase quantity', 'jalpi-page-builder' ) . '">+</button>';
			}
			$out .= '</div>';
		}

		$ajax = ! array_key_exists( 'ajaxAddToCart', $content ) || ! empty( $content['ajaxAddToCart'] );

		$out .= '<input type="hidden" name="add-to-cart" value="' . esc_attr( (string) $product->get_id() ) . '" />';
		$out .= '<button type="submit" class="jalpi-sp__btn' . ( $ajax ? ' jalpi-sp__btn--ajax' : '' ) . '"'
			. ' data-product-id="' . esc_attr( (string) $product->get_id() ) . '">' . $inner . '</button>';
		$out .= '</form>';

		return $out;
	}

	private function render_meta( $product, array $content, bool $is_placeholder ): string {
		$sep    = sanitize_text_field( (string) ( $content['metaSeparator'] ?? '' ) ) ?: ', ';
		$labels = ! array_key_exists( 'metaShowLabels', $content ) || ! empty( $content['metaShowLabels'] );

		$show_sku  = ! array_key_exists( 'metaSku',        $content ) || ! empty( $content['metaSku'] );
		$show_cats = ! array_key_exists( 'metaCategories', $content ) || ! empty( $content['metaCategories'] );
		$show_tags = ! array_key_exists( 'metaTags',       $content ) || ! empty( $content['metaTags'] );

		$rows = '';

		if ( $show_sku ) {
			$sku = $is_placeholder ? 'DEMO-001' : (string) $product->get_sku();
			if ( $sku ) {
				$rows .= '<span class="jalpi-sp__meta-row">'
					. ( $labels ? '<span class="jalpi-sp__meta-label">' . esc_html__( 'SKU:', 'jalpi-page-builder' ) . '</span> ' : '' )
					. '<span class="jalpi-sp__meta-val">' . esc_html( $sku ) . '</span></span>';
			}
		}

		if ( ! $is_placeholder ) {
			foreach ( [
				[ $show_cats, 'product_cat', __( 'Category:', 'jalpi-page-builder' ) ],
				[ $show_tags, 'product_tag', __( 'Tags:', 'jalpi-page-builder' ) ],
			] as [ $enabled, $taxonomy, $label ] ) {
				if ( ! $enabled ) {
					continue;
				}
				// get_the_term_list() returns linked terms, or WP_Error/false.
				$list = get_the_term_list( $product->get_id(), $taxonomy, '', esc_html( $sep ), '' );
				if ( ! $list || is_wp_error( $list ) ) {
					continue;
				}
				$rows .= '<span class="jalpi-sp__meta-row">'
					. ( $labels ? '<span class="jalpi-sp__meta-label">' . esc_html( $label ) . '</span> ' : '' )
					. '<span class="jalpi-sp__meta-val">' . wp_kses_post( $list ) . '</span></span>';
			}
		} elseif ( $show_cats ) {
			$rows .= '<span class="jalpi-sp__meta-row">'
				. ( $labels ? '<span class="jalpi-sp__meta-label">' . esc_html__( 'Category:', 'jalpi-page-builder' ) . '</span> ' : '' )
				. '<span class="jalpi-sp__meta-val">' . esc_html__( 'Sample', 'jalpi-page-builder' ) . '</span></span>';
		}

		return $rows ? '<div class="jalpi-sp__meta">' . $rows . '</div>' : '';
	}

	private function render_share( $product, array $content, bool $is_placeholder ): string {
		$nets = $content['shareNetworks'] ?? 'facebook,twitter,linkedin,pinterest,whatsapp';
		$nets = array_filter( array_map( 'trim', explode( ',', (string) $nets ) ) );

		$allowed = [ 'facebook', 'twitter', 'linkedin', 'pinterest', 'whatsapp' ];
		$nets    = array_values( array_intersect( $nets, $allowed ) );
		if ( ! $nets ) {
			return '';
		}

		$url   = $is_placeholder ? home_url( '/' ) : $product->get_permalink();
		$title = $is_placeholder ? __( 'Sample Product', 'jalpi-page-builder' ) : $product->get_name();
		$enc_u = rawurlencode( $url );
		$enc_t = rawurlencode( $title );

		$targets = [
			'facebook'  => [ 'https://www.facebook.com/sharer/sharer.php?u=' . $enc_u,               'f',  'Facebook' ],
			'twitter'   => [ 'https://twitter.com/intent/tweet?url=' . $enc_u . '&text=' . $enc_t,   '𝕏',  'X' ],
			'linkedin'  => [ 'https://www.linkedin.com/sharing/share-offsite/?url=' . $enc_u,        'in', 'LinkedIn' ],
			'pinterest' => [ 'https://pinterest.com/pin/create/button/?url=' . $enc_u . '&description=' . $enc_t, 'P', 'Pinterest' ],
			'whatsapp'  => [ 'https://api.whatsapp.com/send?text=' . $enc_t . '%20' . $enc_u,        '⌾',  'WhatsApp' ],
		];

		$out = '<div class="jalpi-sp__share">';
		foreach ( $nets as $net ) {
			if ( ! isset( $targets[ $net ] ) ) {
				continue;
			}
			[ $href, $glyph, $label ] = $targets[ $net ];
			$out .= '<a class="jalpi-sp__share-btn jalpi-sp__share-btn--' . esc_attr( $net ) . '"'
				. ' href="' . esc_url( $href ) . '" target="_blank" rel="noopener noreferrer nofollow"'
				. ' aria-label="' . esc_attr( sprintf(
					/* translators: %s: social network name */
					__( 'Share on %s', 'jalpi-page-builder' ), $label
				) ) . '"><span aria-hidden="true">' . esc_html( $glyph ) . '</span></a>';
		}
		return $out . '</div>';
	}

	/** Static stand-ins for the hook-driven sections when WC is unavailable. */
	private function render_placeholder_sections( array $show ): string {
		$out = '';

		if ( $show['tabs'] ) {
			$out .= '<div class="jalpi-sp__tabs jalpi-sp__tabs--placeholder">'
				. '<div class="jalpi-sp__tabnav">'
				. '<span class="jalpi-sp__tabnav-item is-active">' . esc_html__( 'Description', 'jalpi-page-builder' ) . '</span>'
				. '<span class="jalpi-sp__tabnav-item">' . esc_html__( 'Additional information', 'jalpi-page-builder' ) . '</span>'
				. '<span class="jalpi-sp__tabnav-item">' . esc_html__( 'Reviews', 'jalpi-page-builder' ) . '</span>'
				. '</div>'
				. '<div class="jalpi-sp__tabpanel"><p>'
				. esc_html__( 'Product tabs render here from WooCommerce on the live site.', 'jalpi-page-builder' )
				. '</p></div></div>';
		}

		foreach ( [ 'upsells' => __( 'You may also like', 'jalpi-page-builder' ),
		            'related' => __( 'Related products', 'jalpi-page-builder' ) ] as $key => $heading ) {
			if ( empty( $show[ $key ] ) ) {
				continue;
			}
			$out .= '<div class="jalpi-sp__' . esc_attr( $key ) . '">'
				. '<h2 class="jalpi-sp__section-title">' . esc_html( $heading ) . '</h2>'
				. '<p class="jalpi-sp__placeholder-note">'
				. esc_html__( 'Renders from WooCommerce on the live site.', 'jalpi-page-builder' )
				. '</p></div>';
		}

		return $out;
	}

	private function render_notice( array $content ): string {
		$msg = sanitize_text_field( (string) ( $content['emptyMessage'] ?? '' ) )
			?: __( 'No product selected — choose a product in the widget settings.', 'jalpi-page-builder' );

		return '<div class="jalpi-sp__empty">'
			. '<div class="jalpi-sp__empty-icon" aria-hidden="true">🛍</div>'
			. '<p class="jalpi-sp__empty-text">' . esc_html( $msg ) . '</p>'
			. '</div>';
	}

	// ─────────────────────────────────────────────────────────────────────────
	// CSS
	// ─────────────────────────────────────────────────────────────────────────

	private function emit_css(
		Jalpi_Renderer $renderer, string $sel, array $settings,
		array $l, callable $raw, callable $px, callable $enum
	): void {
		$col = static fn( string $k, string $d = '' ): string =>
			Jalpi_Sanitizer::sanitize_color( (string) ( $settings[ $k ] ?? '' ) ) ?: $d;

		// ── Main two-column layout ───────────────────────────────────────────
		$gap      = $px( $raw( 'sp_col_gap', '40' ), '40px' );
		$gal_w    = max( 20, min( 80, (int) $raw( 'sp_gallery_width', '50' ) ) );
		$stacked  = 'top' === $l['gal_pos'];
		$reversed = 'right' === $l['gal_pos'];

		if ( $stacked ) {
			$renderer->add_css( $sel . ' .jalpi-sp__main{display:block;}' );
			$renderer->add_css( $sel . ' .jalpi-sp__gallery-col{margin-bottom:' . $gap . ';}' );
		} else {
			$renderer->add_css( $sel . ' .jalpi-sp__main{display:grid;gap:' . $gap
				. ';grid-template-columns:' . $gal_w . '% minmax(0,1fr);align-items:flex-start;}' );
			if ( $reversed ) {
				// Swap visual order without reordering the DOM (keeps the
				// gallery first for screen readers and tab order).
				$renderer->add_css( $sel . ' .jalpi-sp__gallery-col{order:2;}' );
				$renderer->add_css( $sel . ' .jalpi-sp__summary{order:1;}' );
				$renderer->add_css( $sel . ' .jalpi-sp__main{grid-template-columns:minmax(0,1fr) ' . $gal_w . '%;}' );
			}
			// Single column on small screens regardless of gallery position.
			$renderer->add_mobile_css( $sel . ' .jalpi-sp__main{grid-template-columns:1fr;}' );
		}
		$renderer->add_tablet_css( $sel . ' .jalpi-sp__main{gap:'
			. $px( $raw( 'sp_col_gap_tablet', '' ), $gap ) . ';}' );

		// ── Container ────────────────────────────────────────────────────────
		$cont  = '';
		if ( $col( 'sp_bg' ) )   $cont .= 'background:' . $col( 'sp_bg' ) . ';';
		$pad = self::css_value( $raw( 'sp_pad', '' ) );
		if ( $pad )              $cont .= 'padding:' . $pad . ';';
		$mar = self::css_value( $raw( 'sp_margin', '' ) );
		if ( $mar )              $cont .= 'margin:' . $mar . ';';
		$radius = $px( $raw( 'sp_radius', '' ) );
		if ( '' !== $radius )    $cont .= 'border-radius:' . $radius . ';overflow:hidden;';
		$shadow = self::css_value( $raw( 'sp_shadow', '' ) );
		if ( $shadow )           $cont .= 'box-shadow:' . $shadow . ';';
		$bd = $enum( 'sp_bd_type', [ 'none', 'solid', 'dashed', 'dotted', 'double' ], 'none' );
		if ( 'none' !== $bd ) {
			$cont .= 'border-style:' . $bd . ';';
			$cont .= 'border-width:' . ( $px( $raw( 'sp_bd_width', '' ) ) ?: '1px' ) . ';';
			$cont .= 'border-color:' . ( $col( 'sp_bd_color' ) ?: '#e5e7eb' ) . ';';
		}
		if ( $cont ) {
			$renderer->add_css( $sel . ' .jalpi-sp__main{' . $cont . '}' );
		}

		// ── Gallery ──────────────────────────────────────────────────────────
		$img_radius = $px( $raw( 'sp_img_radius', '8' ), '8px' );
		$renderer->add_css( $sel . ' .jalpi-sp__stage{position:relative;overflow:hidden;border-radius:'
			. $img_radius . ';}' );
		// Slides stack; only the active one is shown.
		$renderer->add_css( $sel . ' .jalpi-sp__slide{display:none;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__slide.is-active{display:block;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__img{width:100%;height:auto;display:block;'
			. 'transition:transform 400ms ease;}' );

		$hover = $enum( 'sp_img_hover', [ 'none', 'zoom', 'lift' ], 'none' );
		if ( 'zoom' === $hover ) {
			$renderer->add_css( $sel . ' .jalpi-sp__stage:hover .jalpi-sp__img{transform:scale(1.06);}' );
		} elseif ( 'lift' === $hover ) {
			$renderer->add_css( $sel . ' .jalpi-sp__stage:hover{transform:translateY(-4px);}' );
		}

		$t_gap  = $px( $raw( 'sp_thumb_gap', '10' ), '10px' );
		$t_size = $px( $raw( 'sp_thumb_size', '72' ), '72px' );

		if ( 'vertical' === $l['thumb_p'] ) {
			$renderer->add_css( $sel . ' .jalpi-sp__gallery--vertical{display:flex;flex-direction:row-reverse;'
				. 'gap:' . $t_gap . ';align-items:flex-start;}' );
			$renderer->add_css( $sel . ' .jalpi-sp__gallery--vertical .jalpi-sp__stage{flex:1 1 auto;min-width:0;}' );
			$renderer->add_css( $sel . ' .jalpi-sp__gallery--vertical .jalpi-sp__thumbs{display:flex;'
				. 'flex-direction:column;gap:' . $t_gap . ';flex:0 0 ' . $t_size . ';}' );
			// Thumbs move below the image on phones — a vertical rail is too narrow.
			$renderer->add_mobile_css( $sel . ' .jalpi-sp__gallery--vertical{flex-direction:column;}' );
			$renderer->add_mobile_css( $sel . ' .jalpi-sp__gallery--vertical .jalpi-sp__thumbs{'
				. 'flex-direction:row;flex-wrap:wrap;flex-basis:auto;}' );
		} else {
			$renderer->add_css( $sel . ' .jalpi-sp__thumbs{display:flex;flex-wrap:wrap;gap:' . $t_gap
				. ';margin-top:' . $t_gap . ';}' );
			$renderer->add_css( $sel . ' .jalpi-sp__thumbs .jalpi-sp__thumb{width:' . $t_size . ';}' );
		}

		$renderer->add_css( $sel . ' .jalpi-sp__thumb{padding:0;border:2px solid transparent;background:none;'
			. 'cursor:pointer;border-radius:' . $px( $raw( 'sp_thumb_radius', '6' ), '6px' )
			. ';overflow:hidden;line-height:0;transition:border-color 200ms ease,opacity 200ms ease;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__thumb img{width:100%;height:100%;display:block;object-fit:cover;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__thumb.is-active{border-color:'
			. ( $col( 'sp_thumb_active_color' ) ?: '#111827' ) . ';}' );

		// ── Title ────────────────────────────────────────────────────────────
		$title  = 'margin:' . ( self::css_value( $raw( 'sp_title_margin', '' ) ) ?: '0 0 10px' ) . ';';
		$title .= 'font-size:' . $px( $raw( 'sp_title_size', '30' ), '30px' ) . ';';
		$title .= 'font-weight:' . ( (int) $raw( 'sp_title_weight', '700' ) ?: 700 ) . ';';
		$title .= 'line-height:' . ( is_numeric( $raw( 'sp_title_lh', '1.25' ) ) ? $raw( 'sp_title_lh', '1.25' ) : '1.25' ) . ';';
		$tt = $enum( 'sp_title_transform', [ 'none', 'uppercase', 'lowercase', 'capitalize' ], 'none' );
		if ( 'none' !== $tt ) $title .= 'text-transform:' . $tt . ';';
		$ls = $px( $raw( 'sp_title_ls', '' ) );
		if ( '' !== $ls )     $title .= 'letter-spacing:' . $ls . ';';
		$ff = self::css_value( $raw( 'sp_title_font', '' ) );
		if ( $ff )            $title .= 'font-family:' . $ff . ';';
		if ( $col( 'sp_title_color' ) ) $title .= 'color:' . $col( 'sp_title_color' ) . ';';
		$ta = $enum( 'sp_title_align', [ 'inherit', 'left', 'center', 'right' ], 'inherit' );
		if ( 'inherit' !== $ta ) $title .= 'text-align:' . $ta . ';';
		$renderer->add_css( $sel . ' .jalpi-sp__title{' . $title . '}' );
		if ( $col( 'sp_title_hover_color' ) ) {
			$renderer->add_css( $sel . ' .jalpi-sp__title:hover{color:' . $col( 'sp_title_hover_color' ) . ';}' );
		}

		// ── Rating ───────────────────────────────────────────────────────────
		$star = $px( $raw( 'sp_star_size', '16' ), '16px' );
		$renderer->add_css( $sel . ' .jalpi-sp__rating{display:flex;align-items:center;gap:8px;margin-bottom:'
			. $px( $raw( 'sp_rating_spacing', '10' ), '10px' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__stars{position:relative;display:inline-block;width:5.2em;'
			. 'height:1em;line-height:1;font-size:' . $star . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__stars::before{content:"★★★★★";letter-spacing:.05em;color:'
			. ( $col( 'sp_star_empty_color' ) ?: '#d1d5db' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__stars-fill{position:absolute;inset:0;overflow:hidden;white-space:nowrap;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__stars-fill::before{content:"★★★★★";letter-spacing:.05em;color:'
			. ( $col( 'sp_star_color' ) ?: '#f59e0b' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__review-count,' . $sel . ' .jalpi-sp__review-link{font-size:'
			. $px( $raw( 'sp_review_size', '13' ), '13px' ) . ';color:'
			. ( $col( 'sp_review_color' ) ?: 'inherit' ) . ';}' );

		// ── Price ────────────────────────────────────────────────────────────
		$price  = 'display:flex;align-items:center;flex-wrap:wrap;gap:10px;';
		$price .= 'margin-bottom:' . $px( $raw( 'sp_price_spacing', '14' ), '14px' ) . ';';
		$price .= 'font-size:' . $px( $raw( 'sp_price_size', '24' ), '24px' ) . ';';
		$price .= 'font-weight:' . ( (int) $raw( 'sp_price_weight', '700' ) ?: 700 ) . ';';
		if ( $col( 'sp_price_color' ) ) $price .= 'color:' . $col( 'sp_price_color' ) . ';';
		$renderer->add_css( $sel . ' .jalpi-sp__price{' . $price . '}' );
		// WooCommerce marks the struck-through original with <del>, the active
		// figure with <ins>.
		$renderer->add_css( $sel . ' .jalpi-sp__price del{opacity:.5;font-weight:400;font-size:.75em;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__price ins{text-decoration:none;'
			. ( $col( 'sp_sale_price_color' ) ? 'color:' . $col( 'sp_sale_price_color' ) . ';' : '' ) . '}' );

		$renderer->add_css( $sel . ' .jalpi-sp__badge{display:inline-block;font-size:'
			. $px( $raw( 'sp_badge_size', '12' ), '12px' ) . ';font-weight:600;line-height:1.4;'
			. 'padding:' . ( self::css_value( $raw( 'sp_badge_pad', '' ) ) ?: '3px 9px' ) . ';'
			. 'border-radius:' . $px( $raw( 'sp_badge_radius', '4' ), '4px' ) . ';'
			. 'background:' . ( $col( 'sp_badge_bg' ) ?: '#dc2626' ) . ';'
			. 'color:' . ( $col( 'sp_badge_color' ) ?: '#ffffff' ) . ';}' );

		// ── Excerpt ──────────────────────────────────────────────────────────
		$exc  = 'font-size:' . $px( $raw( 'sp_desc_size', '15' ), '15px' ) . ';';
		$exc .= 'line-height:' . ( is_numeric( $raw( 'sp_desc_lh', '1.7' ) ) ? $raw( 'sp_desc_lh', '1.7' ) : '1.7' ) . ';';
		$exc .= 'margin-bottom:' . $px( $raw( 'sp_desc_spacing', '18' ), '18px' ) . ';';
		if ( $col( 'sp_desc_color' ) ) $exc .= 'color:' . $col( 'sp_desc_color' ) . ';';
		$da = $enum( 'sp_desc_align', [ 'inherit', 'left', 'center', 'right', 'justify' ], 'inherit' );
		if ( 'inherit' !== $da ) $exc .= 'text-align:' . $da . ';';
		$renderer->add_css( $sel . ' .jalpi-sp__excerpt{' . $exc . '}' );
		$renderer->add_css( $sel . ' .jalpi-sp__excerpt p:last-child{margin-bottom:0;}' );

		// ── Cart / quantity / button ─────────────────────────────────────────
		$renderer->add_css( $sel . ' .jalpi-sp__cart{display:flex;align-items:stretch;flex-wrap:wrap;gap:12px;'
			. 'margin-bottom:' . $px( $raw( 'sp_cart_spacing', '20' ), '20px' ) . ';}' );

		$qty_w = $px( $raw( 'sp_qty_width', '110' ), '110px' );
		$qty_h = $px( $raw( 'sp_qty_height', '46' ), '46px' );
		$renderer->add_css( $sel . ' .jalpi-sp__qty-box{display:inline-flex;align-items:center;'
			. 'width:' . $qty_w . ';height:' . $qty_h . ';box-sizing:border-box;overflow:hidden;'
			. 'background:' . ( $col( 'sp_qty_bg' ) ?: 'transparent' ) . ';'
			. 'border:1px solid ' . ( $col( 'sp_qty_bd_color' ) ?: '#e5e7eb' ) . ';'
			. 'border-radius:' . $px( $raw( 'sp_qty_radius', '8' ), '8px' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__qty{flex:1 1 auto;width:100%;min-width:0;height:100%;'
			. 'border:none;background:none;text-align:center;box-sizing:border-box;'
			. 'font-size:' . $px( $raw( 'sp_qty_size', '15' ), '15px' ) . ';'
			. 'color:' . ( $col( 'sp_qty_color' ) ?: 'inherit' ) . ';}' );
		// Native spinners duplicate the +/- affordance.
		$renderer->add_css( $sel . ' .jalpi-sp__qty-box--plusminus .jalpi-sp__qty::-webkit-outer-spin-button,'
			. $sel . ' .jalpi-sp__qty-box--plusminus .jalpi-sp__qty::-webkit-inner-spin-button{'
			. '-webkit-appearance:none;margin:0;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__qty-box--plusminus .jalpi-sp__qty{-moz-appearance:textfield;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__qty-btn{flex:0 0 auto;width:2.2em;height:100%;border:none;'
			. 'background:none;cursor:pointer;font-size:15px;line-height:1;'
			. 'color:' . ( $col( 'sp_qty_btn_color' ) ?: 'inherit' ) . ';}' );

		$btn  = 'display:inline-flex;align-items:center;justify-content:center;gap:8px;cursor:pointer;';
		$btn .= 'box-sizing:border-box;text-decoration:none;';
		$btn .= 'padding:' . ( self::css_value( $raw( 'sp_btn_pad', '' ) ) ?: '12px 28px' ) . ';';
		$btn .= 'font-size:' . $px( $raw( 'sp_btn_size', '15' ), '15px' ) . ';';
		$btn .= 'font-weight:' . ( (int) $raw( 'sp_btn_weight', '600' ) ?: 600 ) . ';';
		$btn .= 'color:' . ( $col( 'sp_btn_color' ) ?: '#ffffff' ) . ';';
		$btn .= 'background:' . ( $col( 'sp_btn_bg' ) ?: '#111827' ) . ';';
		$btn .= 'border-radius:' . $px( $raw( 'sp_btn_radius', '8' ), '8px' ) . ';';
		$btn .= 'transition:background 200ms ease,color 200ms ease,border-color 200ms ease,transform 200ms ease;';
		$btn_bd = $enum( 'sp_btn_bd_type', [ 'none', 'solid', 'dashed', 'dotted' ], 'none' );
		if ( 'none' !== $btn_bd ) {
			$btn .= 'border-style:' . $btn_bd . ';';
			$btn .= 'border-width:' . ( $px( $raw( 'sp_btn_bd_width', '' ) ) ?: '1px' ) . ';';
			$btn .= 'border-color:' . ( $col( 'sp_btn_bd_color' ) ?: 'currentColor' ) . ';';
		} else {
			$btn .= 'border:none;';
		}
		// Also style WooCommerce's own button so the variable-product form matches.
		$renderer->add_css( $sel . ' .jalpi-sp__btn,' . $sel . ' .jalpi-sp__cart--wc button.single_add_to_cart_button{'
			. $btn . '}' );

		$bh = '';
		if ( $col( 'sp_btn_hover_color' ) ) $bh .= 'color:' . $col( 'sp_btn_hover_color' ) . ';';
		if ( $col( 'sp_btn_hover_bg' ) )    $bh .= 'background:' . $col( 'sp_btn_hover_bg' ) . ';';
		if ( $col( 'sp_btn_hover_bd' ) )    $bh .= 'border-color:' . $col( 'sp_btn_hover_bd' ) . ';';
		$anim = $enum( 'sp_btn_hover_anim', [ 'none', 'grow', 'shrink', 'float', 'sink' ], 'none' );
		$tfm  = [ 'grow' => 'scale(1.04)', 'shrink' => 'scale(.96)', 'float' => 'translateY(-3px)', 'sink' => 'translateY(3px)' ];
		if ( isset( $tfm[ $anim ] ) ) $bh .= 'transform:' . $tfm[ $anim ] . ';';
		if ( $bh ) {
			$renderer->add_css( $sel . ' .jalpi-sp__btn:hover,' . $sel . ' .jalpi-sp__btn:focus-visible,'
				. $sel . ' .jalpi-sp__cart--wc button.single_add_to_cart_button:hover{' . $bh . '}' );
		}
		$renderer->add_css( $sel . ' .jalpi-sp__btn.is-busy{opacity:.7;pointer-events:none;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__stock--out{margin:0;font-weight:600;opacity:.75;}' );

		// ── Meta ─────────────────────────────────────────────────────────────
		$renderer->add_css( $sel . ' .jalpi-sp__meta{display:flex;flex-direction:column;gap:'
			. $px( $raw( 'sp_meta_spacing', '6' ), '6px' ) . ';font-size:'
			. $px( $raw( 'sp_meta_size', '13' ), '13px' ) . ';'
			. 'color:' . ( $col( 'sp_meta_color' ) ?: 'inherit' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__meta-label{font-weight:600;color:'
			. ( $col( 'sp_meta_label_color' ) ?: 'inherit' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__meta a{color:inherit;}' );

		// ── Share ────────────────────────────────────────────────────────────
		$sh_size = $px( $raw( 'sp_share_size', '15' ), '15px' );
		$shape   = $enum( 'sp_share_style', [ 'circle', 'rounded', 'square' ], 'circle' );
		$sh_rad  = 'circle' === $shape ? '50%' : ( 'rounded' === $shape ? '8px' : '0' );
		$renderer->add_css( $sel . ' .jalpi-sp__share{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__share-btn{display:inline-flex;align-items:center;'
			. 'justify-content:center;width:2.4em;height:2.4em;font-size:' . $sh_size . ';line-height:1;'
			. 'text-decoration:none;border-radius:' . $sh_rad . ';'
			. 'background:' . ( $col( 'sp_share_bg' ) ?: '#f3f4f6' ) . ';'
			. 'color:' . ( $col( 'sp_share_color' ) ?: '#111827' ) . ';'
			. 'transition:background 200ms ease,color 200ms ease;}' );
		if ( $col( 'sp_share_hover_bg' ) ) {
			$renderer->add_css( $sel . ' .jalpi-sp__share-btn:hover{background:'
				. $col( 'sp_share_hover_bg' ) . ';}' );
		}

		// ── Tabs (styles target WooCommerce's own markup too) ────────────────
		$tab_sp = $px( $raw( 'sp_tabs_spacing', '48' ), '48px' );
		$renderer->add_css( $sel . ' .jalpi-sp__tabs{margin-top:' . $tab_sp . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__tabs .wc-tabs,' . $sel . ' .jalpi-sp__tabnav{'
			. 'display:flex;flex-wrap:wrap;gap:' . $px( $raw( 'sp_tab_gap', '24' ), '24px' ) . ';'
			. 'list-style:none;margin:0 0 20px;padding:0 0 10px;'
			. 'border-bottom:1px solid ' . ( $col( 'sp_tab_bd_color' ) ?: '#e5e7eb' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__tabs .wc-tabs li a,' . $sel . ' .jalpi-sp__tabnav-item{'
			. 'display:inline-block;text-decoration:none;cursor:pointer;'
			. 'font-size:' . $px( $raw( 'sp_tab_size', '15' ), '15px' ) . ';'
			. 'font-weight:' . ( (int) $raw( 'sp_tab_weight', '600' ) ?: 600 ) . ';'
			. 'color:' . ( $col( 'sp_tab_color' ) ?: 'inherit' ) . ';'
			. 'transition:color 200ms ease;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__tabs .wc-tabs li.active a,'
			. $sel . ' .jalpi-sp__tabnav-item.is-active{color:'
			. ( $col( 'sp_tab_active_color' ) ?: '#111827' ) . ';}' );
		if ( $col( 'sp_tab_hover_color' ) ) {
			$renderer->add_css( $sel . ' .jalpi-sp__tabs .wc-tabs li a:hover{color:'
				. $col( 'sp_tab_hover_color' ) . ';}' );
		}
		$renderer->add_css( $sel . ' .jalpi-sp__tabs .woocommerce-Tabs-panel,' . $sel . ' .jalpi-sp__tabpanel{'
			. 'font-size:' . $px( $raw( 'sp_tab_content_size', '15' ), '15px' ) . ';'
			. 'line-height:' . ( is_numeric( $raw( 'sp_tab_content_lh', '1.7' ) ) ? $raw( 'sp_tab_content_lh', '1.7' ) : '1.7' ) . ';'
			. 'color:' . ( $col( 'sp_tab_content_color' ) ?: 'inherit' ) . ';}' );

		// ── Related / upsells section headings ───────────────────────────────
		$renderer->add_css( $sel . ' .jalpi-sp__related,' . $sel . ' .jalpi-sp__upsells{margin-top:' . $tab_sp . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__related h2,' . $sel . ' .jalpi-sp__upsells h2,'
			. $sel . ' .jalpi-sp__section-title{font-size:'
			. $px( $raw( 'sp_section_title_size', '22' ), '22px' ) . ';font-weight:700;margin:0 0 18px;'
			. ( $col( 'sp_section_title_color' ) ? 'color:' . $col( 'sp_section_title_color' ) . ';' : '' ) . '}' );

		// ── Empty / placeholder chrome ───────────────────────────────────────
		$renderer->add_css( $sel . ' .jalpi-sp__empty{text-align:center;padding:48px 16px;color:'
			. ( $col( 'sp_empty_color' ) ?: '#6b7280' ) . ';}' );
		$renderer->add_css( $sel . ' .jalpi-sp__empty-icon{font-size:34px;margin-bottom:10px;opacity:.7;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__empty-text{margin:0;font-size:15px;}' );
		$renderer->add_css( $sel . ' .jalpi-sp__placeholder-note{margin:0;font-size:13px;opacity:.6;}' );
	}
}

/**
 * Stand-in product used when WooCommerce is not active.
 *
 * Exposes only the read methods the single-product element calls.
 */
class Jalpi_Woo_Product_Placeholder {

	public function get_id(): int              { return 0; }
	public function get_name(): string         { return __( 'Sample Product', 'jalpi-page-builder' ); }
	public function get_permalink(): string    { return '#'; }
	public function get_sku(): string          { return 'DEMO-001'; }
	public function get_average_rating(): float { return 4.5; }
	public function get_review_count(): int    { return 12; }
	public function is_on_sale(): bool         { return true; }
	public function get_regular_price(): string { return '49.00'; }
	public function get_sale_price(): string   { return '39.00'; }
	public function get_type(): string         { return 'simple'; }
	public function is_purchasable(): bool     { return false; }
	public function is_in_stock(): bool        { return true; }
	public function is_sold_individually(): bool { return false; }
	public function get_image_id(): int        { return 0; }
	public function get_gallery_image_ids(): array { return []; }
	public function get_max_purchase_quantity(): int { return 0; }
	public function get_min_purchase_quantity(): int { return 1; }
	public function single_add_to_cart_text(): string { return __( 'Add to cart', 'jalpi-page-builder' ); }

	public function get_short_description(): string {
		return __( 'This is a sample product description shown while WooCommerce is unavailable. Connect WooCommerce and pick a product to see real data here.', 'jalpi-page-builder' );
	}

	public function get_price_html(): string {
		$symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '$';
		return '<del>' . esc_html( $symbol . '49.00' ) . '</del> <ins>' . esc_html( $symbol . '39.00' ) . '</ins>';
	}

	/** Inline SVG placeholder — no HTTP request, no missing-asset flash. */
	public function get_image_url(): string {
		$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">'
			. '<rect width="600" height="600" fill="#eef2f7"/>'
			. '<path d="M220 380h160l-32-112h-96z" fill="#c3ccd8"/>'
			. '<circle cx="300" cy="240" r="52" fill="#c3ccd8"/></svg>';
		return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
	}
}
