<?php

namespace RadiusTheme\RadiusBooking\Blocks;

use RadiusTheme\RadiusBooking\Assets\Vite;
use RadiusTheme\RadiusBooking\Controllers\ServiceController;
use RadiusTheme\RadiusBooking\Controllers\UserController;
use RadiusTheme\RadiusBooking\Helpers\SettingsHelper;

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

class BlockManager {

	public function __construct() {
		add_action( 'init', array( $this, 'register_blocks' ) );
		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_assets' ) );
		add_filter( 'block_categories_all', array( $this, 'register_block_category' ) );
	}

	/**
	 * Register custom block category.
	 *
	 * @param array $categories Existing block categories.
	 *
	 * @return array
	 */
	public function register_block_category( $categories ) {
		return array_merge(
			array(
				array(
					'slug'  => 'radius-booking',
					'title' => esc_html__( 'Radius Booking', 'radius-booking' ),
					'icon'  => 'calendar',
				),
			),
			$categories
		);
	}

	/**
	 * Register Gutenberg blocks.
	 *
	 * @return void
	 */
	public function register_blocks() {
		register_block_type(
			'radius-booking/appointment-form',
			array(
				'attributes'      => array(
					'modal'      => array(
						'type'    => 'boolean',
						'default' => false,
					),
					'buttonText' => array(
						'type'    => 'string',
						'default' => '',
					),
					'flow'       => array(
						'type'    => 'string',
						'default' => 'service_first',
					),
					'serviceId'  => array(
						'type'    => 'string',
						'default' => '',
					),
					'staffId'    => array(
						'type'    => 'string',
						'default' => '',
					),
					'btnAlign'           => array(
						'type'    => 'string',
						'default' => 'left',
					),
					'btnFullWidth'       => array(
						'type'    => 'boolean',
						'default' => false,
					),
					'btnTextColor'       => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
					'btnBgColor'         => array(
						'type'    => 'string',
						'default' => '#0040ff',
					),
					'btnTextColorHover'  => array(
						'type'    => 'string',
						'default' => '',
					),
					'btnBgColorHover'    => array(
						'type'    => 'string',
						'default' => '',
					),
					'btnBorderColor'     => array(
						'type'    => 'string',
						'default' => '',
					),
					'btnBorderColorHover' => array(
						'type'    => 'string',
						'default' => '',
					),
					'btnBorderWidth'     => array(
						'type'    => 'number',
						'default' => 0,
					),
					'btnRadius'          => array(
						'type'    => 'number',
						'default' => 8,
					),
					'btnFontSize'        => array(
						'type'    => 'number',
						'default' => 15,
					),
					'btnFontWeight'      => array(
						'type'    => 'string',
						'default' => '600',
					),
					'btnPaddingY'        => array(
						'type'    => 'number',
						'default' => 12,
					),
					'btnPaddingX'        => array(
						'type'    => 'number',
						'default' => 28,
					),
					'btnMarginTop'       => array(
						'type'    => 'number',
						'default' => 0,
					),
					'btnMarginBottom'    => array(
						'type'    => 'number',
						'default' => 0,
					),
				),
				'render_callback' => array( $this, 'render_appointment_form' ),
			)
		);

		register_block_type(
			'radius-booking/service-list',
			array(
				'attributes'      => array(
					'layout'             => array(
						'type'    => 'string',
						'default' => 'grid',
					),
					'columns'            => array(
						'type'    => 'number',
						'default' => 3,
					),
					'perPage'            => array(
						'type'    => 'number',
						'default' => 9,
					),
					'category'           => array(
						'type'    => 'string',
						'default' => '',
					),
					'showImage'          => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showPrice'          => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showDuration'       => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showCategory'       => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showDescription'    => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showCategoryFilter' => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showStaff'          => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'buttonText'         => array(
						'type'    => 'string',
						'default' => 'Book Now',
					),
					'primaryColor'       => array(
						'type'    => 'string',
						'default' => '#0040ff',
					),
					'cardBgColor'        => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
					'cardBorderColor'    => array(
						'type'    => 'string',
						'default' => '#e5e7eb',
					),
					'cardBorderRadius'   => array(
						'type'    => 'number',
						'default' => 12,
					),
					'cardGap'            => array(
						'type'    => 'number',
						'default' => 24,
					),
					'imageHeight'        => array(
						'type'    => 'number',
						'default' => 192,
					),
					'titleColor'         => array(
						'type'    => 'string',
						'default' => '#111827',
					),
					'titleFontSize'      => array(
						'type'    => 'number',
						'default' => 18,
					),
					'descColor'          => array(
						'type'    => 'string',
						'default' => '#6b7280',
					),
					'priceColor'         => array(
						'type'    => 'string',
						'default' => '',
					),
					'buttonBgColor'      => array(
						'type'    => 'string',
						'default' => '',
					),
					'buttonTextColor'    => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
					'buttonBorderRadius' => array(
						'type'    => 'number',
						'default' => 8,
					),
					'filterActiveBg'     => array(
						'type'    => 'string',
						'default' => '',
					),
					'filterActiveText'   => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
					'paginationActiveBg' => array(
						'type'    => 'string',
						'default' => '',
					),
					'paginationActiveText' => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
				),
				'render_callback' => array( $this, 'render_service_list' ),
			)
		);

		register_block_type(
			'radius-booking/employee-list',
			array(
				'attributes'      => array(
					'layout'                 => array(
						'type'    => 'string',
						'default' => 'grid',
					),
					'columns'                => array(
						'type'    => 'number',
						'default' => 3,
					),
					'perPage'                => array(
						'type'    => 'number',
						'default' => 9,
					),
					'service'                => array(
						'type'    => 'string',
						'default' => '',
					),
					'location'               => array(
						'type'    => 'string',
						'default' => '',
					),
					'showImage'              => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showServices'           => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showLocation'           => array(
						'type'    => 'boolean',
						'default' => false,
					),
					'showPhone'              => array(
						'type'    => 'boolean',
						'default' => false,
					),
					'showDescription'        => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'showButton'             => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'buttonText'             => array(
						'type'    => 'string',
						'default' => 'Book Now',
					),
					'primaryColor'           => array(
						'type'    => 'string',
						'default' => '#0040ff',
					),
					'cardBgColor'            => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
					'cardBorderColor'        => array(
						'type'    => 'string',
						'default' => '#e5e7eb',
					),
					'cardBorderRadius'       => array(
						'type'    => 'number',
						'default' => 12,
					),
					'cardGap'                => array(
						'type'    => 'number',
						'default' => 24,
					),
					'imageHeight'            => array(
						'type'    => 'number',
						'default' => 220,
					),
					'imageFit'               => array(
						'type'    => 'string',
						'default' => 'cover',
					),
					'titleColor'             => array(
						'type'    => 'string',
						'default' => '#111827',
					),
					'titleFontSize'          => array(
						'type'    => 'number',
						'default' => 18,
					),
					'metaColor'              => array(
						'type'    => 'string',
						'default' => '#6b7280',
					),
					'buttonBgColor'          => array(
						'type'    => 'string',
						'default' => '',
					),
					'buttonTextColor'        => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
					'buttonHoverBgColor'     => array(
						'type'    => 'string',
						'default' => '',
					),
					'buttonHoverTextColor'   => array(
						'type'    => 'string',
						'default' => '',
					),
					'buttonBorderRadius'     => array(
						'type'    => 'number',
						'default' => 8,
					),
					'paginationActiveBg'     => array(
						'type'    => 'string',
						'default' => '',
					),
					'paginationActiveText'   => array(
						'type'    => 'string',
						'default' => '#ffffff',
					),
				),
				'render_callback' => array( $this, 'render_employee_list' ),
			)
		);
	}

	/**
	 * Enqueue block editor scripts.
	 *
	 * @return void
	 */
	public function enqueue_editor_assets() {
		$assets_dir = RADIUS_BOOKING_DIR . '/build';
		$assets_url = RADIUS_BOOKING_BUILD;

		$vite = new Vite(
			$assets_dir,
			$assets_url,
			array(
				'preload-js-chunks'  => true,
				'preload-css-chunks' => true,
			)
		);

		$vite->enqueue(
			'src/blocks/index.js',
			array(
				'handle'              => 'radius-booking-blocks',
				'script-dependencies' => array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-server-side-render' ),
				'style-dependencies'  => array(),
			)
		);

		$form_data = $this->get_appointment_form_data();

		wp_localize_script(
			'radius-booking-blocks',
			'rtrbBlockData',
			array(
				'categories' => $this->get_categories_for_editor(),
				'services'   => $form_data['services'],
				'staff'      => $form_data['staff'],
				'staffMap'   => $form_data['staffMap'],
			)
		);
	}

	/**
	 * Build the service / staff option lists and the service → staff map for
	 * the Appointment Form block editor controls. Mirrors the data the
	 * Elementor Appointment Form widget exposes.
	 *
	 * @return array{services: array, staff: array, staffMap: array}
	 */
	private function get_appointment_form_data() {
		$services   = array(
			array(
				'value' => '',
				'label' => __( 'None', 'radius-booking' ),
			),
		);
		$staff      = array(
			array(
				'value' => '',
				'label' => __( 'None', 'radius-booking' ),
			),
		);
		$staff_seen = array();
		$map        = array();

		try {
			$categories = ( new ServiceController() )->fetchAllServices();

			if ( is_array( $categories ) ) {
				foreach ( $categories as $category ) {
					if ( empty( $category['services'] ) ) {
						continue;
					}
					$cat_name = $category['name'] ?? __( 'Uncategorized', 'radius-booking' );

					foreach ( $category['services'] as $service ) {
						$service_id              = (string) $service['id'];
						$services[]              = array(
							'value' => $service_id,
							'label' => $cat_name . ' — ' . $service['name'],
						);

						if ( empty( $service['staff'] ) ) {
							continue;
						}

						$service_staff = array();
						foreach ( $service['staff'] as $member ) {
							$member_id                = (string) $member['id'];
							$staff_seen[ $member_id ] = $member['name'] ?? __( 'Staff', 'radius-booking' );
							$service_staff[]          = $member_id;
						}
						$map[ $service_id ] = $service_staff;
					}
				}
			}
		} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			// Silently fail — controls will only show "None".
		}

		foreach ( $staff_seen as $member_id => $name ) {
			$staff[] = array(
				'value' => (string) $member_id,
				'label' => $name,
			);
		}

		return array(
			'services' => $services,
			'staff'    => $staff,
			'staffMap' => $map,
		);
	}

	/**
	 * Get service categories for the block editor.
	 *
	 * @return array
	 */
	private function get_categories_for_editor() {
		$categories = array(
			array(
				'value' => '',
				'label' => __( 'All Categories', 'radius-booking' ),
			),
		);

		try {
			$services = ( new ServiceController() )->fetchAllServices();
			if ( is_array( $services ) ) {
				foreach ( $services as $cat ) {
					if ( ! empty( $cat['id'] ) && ! empty( $cat['name'] ) ) {
						$categories[] = array(
							'value' => (string) $cat['id'],
							'label' => $cat['name'],
						);
					}
				}
			}
		} catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			// Silently fail.
		}

		return $categories;
	}

	/**
	 * Render the Appointment Form block on the frontend.
	 *
	 * @param array $attributes Block attributes.
	 *
	 * @return string
	 */
	public function render_appointment_form( $attributes = array() ) {
		$shortcode_atts = '';

		if ( ! empty( $attributes['modal'] ) ) {
			$shortcode_atts .= ' modal="true"';
		}

		if ( ! empty( $attributes['buttonText'] ) ) {
			$shortcode_atts .= ' button_text="' . esc_attr( $attributes['buttonText'] ) . '"';
		}

		$flow = isset( $attributes['flow'] ) && 'staff_first' === $attributes['flow'] ? 'staff_first' : 'service_first';
		if ( 'staff_first' === $flow ) {
			$shortcode_atts .= ' flow="staff_first"';
		}

		$service_id = ! empty( $attributes['serviceId'] ) ? absint( $attributes['serviceId'] ) : 0;
		$staff_id   = ! empty( $attributes['staffId'] ) ? absint( $attributes['staffId'] ) : 0;

		// Validate that the selected staff is actually assigned to the selected service.
		if ( $service_id && $staff_id ) {
			$data        = $this->get_appointment_form_data();
			$valid_staff = $data['staffMap'][ (string) $service_id ] ?? array();

			if ( ! in_array( (string) $staff_id, $valid_staff, true ) ) {
				$staff_id = 0;
			}
		}

		if ( $service_id ) {
			$shortcode_atts .= ' service_id="' . $service_id . '"';
		}

		if ( $staff_id ) {
			$shortcode_atts .= ' staff_id="' . $staff_id . '"';
		}

		$output = do_shortcode( '[radius_booking_form' . $shortcode_atts . ']' );

		// Modal button styling is only relevant when the modal trigger renders.
		if ( empty( $attributes['modal'] ) ) {
			return $output;
		}

		$scope = 'rtrb-af-' . wp_unique_id();
		$css   = $this->appointment_button_css( '.' . $scope, $attributes );

		return '<div class="' . esc_attr( $scope ) . '">' . $output . '</div><style>' . $css . '</style>';
	}

	/**
	 * Build scoped CSS for the modal "Book Now" trigger button.
	 *
	 * @param string $scope Scope selector (unique container class).
	 * @param array  $a     Block attributes.
	 *
	 * @return string
	 */
	private function appointment_button_css( $scope, $a ) {
		$btn   = $scope . ' .rtrb-shortcode-modal-trigger';
		$align = in_array( $a['btnAlign'] ?? 'left', array( 'left', 'center', 'right' ), true ) ? $a['btnAlign'] : 'left';

		$rules  = $scope . ' .rtrb-shortcode-modal-wrapper{text-align:' . $align . ';}';
		$rules .= $btn . '{';
		$rules .= 'cursor:pointer;';
		$rules .= 'display:' . ( ! empty( $a['btnFullWidth'] ) ? 'block' : 'inline-block' ) . ';';
		if ( ! empty( $a['btnFullWidth'] ) ) {
			$rules .= 'width:100%;';
		}
		$rules .= 'color:' . esc_attr( $a['btnTextColor'] ?? '#ffffff' ) . ';';
		$rules .= 'background-color:' . esc_attr( $a['btnBgColor'] ?? '#0040ff' ) . ';';
		$rules .= 'font-size:' . (int) ( $a['btnFontSize'] ?? 15 ) . 'px;';
		$rules .= 'font-weight:' . esc_attr( $a['btnFontWeight'] ?? '600' ) . ';';
		$rules .= 'padding:' . (int) ( $a['btnPaddingY'] ?? 12 ) . 'px ' . (int) ( $a['btnPaddingX'] ?? 28 ) . 'px;';
		$rules .= 'border-radius:' . (int) ( $a['btnRadius'] ?? 8 ) . 'px;';
		$rules .= 'border-style:solid;';
		$rules .= 'border-width:' . (int) ( $a['btnBorderWidth'] ?? 0 ) . 'px;';
		$rules .= 'border-color:' . esc_attr( ! empty( $a['btnBorderColor'] ) ? $a['btnBorderColor'] : 'transparent' ) . ';';
		$rules .= 'margin:' . (int) ( $a['btnMarginTop'] ?? 0 ) . 'px 0 ' . (int) ( $a['btnMarginBottom'] ?? 0 ) . 'px 0;';
		$rules .= 'transition:all .2s ease;';
		$rules .= '}';

		$hover = '';
		if ( ! empty( $a['btnTextColorHover'] ) ) {
			$hover .= 'color:' . esc_attr( $a['btnTextColorHover'] ) . ';';
		}
		if ( ! empty( $a['btnBgColorHover'] ) ) {
			$hover .= 'background-color:' . esc_attr( $a['btnBgColorHover'] ) . ';';
		}
		if ( ! empty( $a['btnBorderColorHover'] ) ) {
			$hover .= 'border-color:' . esc_attr( $a['btnBorderColorHover'] ) . ';';
		}
		if ( '' !== $hover ) {
			$rules .= $btn . ':hover,' . $btn . ':focus{' . $hover . '}';
		}

		return $rules;
	}

	/**
	 * Render the Employee List block on the frontend.
	 *
	 * @param array $attributes Block attributes.
	 *
	 * @return string
	 */
	public function render_employee_list( $attributes ) {
		// Editor preview — ServerSideRender requests run through the REST API
		// where the employee-list React bundle / localized data is absent, so
		// render a static HTML preview instead of an endless loader.
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return $this->render_employee_list_preview( $attributes );
		}

		$px = function ( $v, $fallback ) {
			$n = is_numeric( $v ) ? $v : $fallback;
			return $n . 'px';
		};

		$styles = array(
			'primaryColor'             => $attributes['primaryColor'] ?? '#0040ff',
			'cardBgColor'              => $attributes['cardBgColor'] ?? '#ffffff',
			'cardBorderColor'          => $attributes['cardBorderColor'] ?? '#e5e7eb',
			'cardBorderRadius'         => $px( $attributes['cardBorderRadius'] ?? 12, 12 ),
			'cardGap'                  => $px( $attributes['cardGap'] ?? 24, 24 ),
			'imageHeight'              => $px( $attributes['imageHeight'] ?? 220, 220 ),
			'imageFit'                 => ( ( $attributes['imageFit'] ?? 'cover' ) === 'contain' ) ? 'contain' : 'cover',
			'titleColor'               => $attributes['titleColor'] ?? '#111827',
			'titleFontSize'            => $px( $attributes['titleFontSize'] ?? 18, 18 ),
			'metaColor'                => $attributes['metaColor'] ?? '#6b7280',
			'buttonBgColor'            => $attributes['buttonBgColor'] ?? '',
			'buttonTextColor'          => $attributes['buttonTextColor'] ?? '#ffffff',
			'buttonHoverBgColor'       => $attributes['buttonHoverBgColor'] ?? '',
			'buttonHoverTextColor'     => $attributes['buttonHoverTextColor'] ?? '',
			'buttonBorderRadius'       => $px( $attributes['buttonBorderRadius'] ?? 8, 8 ),
			'paginationActiveBg'       => $attributes['paginationActiveBg'] ?? '',
			'paginationActiveText'     => $attributes['paginationActiveText'] ?? '#ffffff',
			'paginationInactiveBg'     => '#ffffff',
			'paginationInactiveText'   => '#4b5563',
			'paginationInactiveBorder' => '#e5e7eb',
			'paginationBorderRadius'   => '8px',
		);

		$display = array(
			'showImage'    => $attributes['showImage'] ?? true,
			'showServices' => $attributes['showServices'] ?? true,
			'showLocation' => $attributes['showLocation'] ?? false,
			'showPhone'    => $attributes['showPhone'] ?? false,
			'showDescription' => $attributes['showDescription'] ?? true,
			'showButton'   => $attributes['showButton'] ?? true,
			'buttonText'   => $attributes['buttonText'] ?? 'Book Now',
		);

		$atts = wp_json_encode(
			array(
				'layout'   => $attributes['layout'] ?? 'grid',
				'columns'  => (string) ( $attributes['columns'] ?? 3 ),
				'per_page' => (string) ( $attributes['perPage'] ?? 9 ),
				'service'  => $attributes['service'] ?? '',
				'location' => $attributes['location'] ?? '',
			)
		);

		$html  = '<div class="radius-booking">';
		$html .= '<div class="rtrb-root rtrb-employee-list" data-atts="' . esc_attr( $atts ) . '" data-elementor-styles="' . esc_attr( wp_json_encode( $styles ) ) . '" data-elementor-display="' . esc_attr( wp_json_encode( $display ) ) . '">';
		$html .= '<div class="rtrb-initial-loader" style="display:flex;align-items:center;justify-content:center;min-height:300px;">';
		$html .= '<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:rtrb-spin 1s linear infinite;color:#6B7280;"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>';
		$html .= '</div></div></div>';
		$html .= '<style>@keyframes rtrb-spin{to{transform:rotate(360deg)}}</style>';

		return $html;
	}

	/**
	 * Static HTML preview for the Employee List in the block editor.
	 *
	 * @param array $attributes Block attributes.
	 *
	 * @return string
	 */
	private function render_employee_list_preview( $attributes ) {
		$layout       = $attributes['layout'] ?? 'grid';
		$columns      = max( 1, (int) ( $attributes['columns'] ?? 3 ) );
		$per_page     = (int) ( $attributes['perPage'] ?? 9 );
		$service_flt  = (string) ( $attributes['service'] ?? '' );
		$location_flt = strtolower( trim( (string) ( $attributes['location'] ?? '' ) ) );
		$show_image   = $attributes['showImage'] ?? true;
		$show_serv    = $attributes['showServices'] ?? true;
		$show_loc     = $attributes['showLocation'] ?? false;
		$show_phone   = $attributes['showPhone'] ?? false;
		$show_desc    = $attributes['showDescription'] ?? true;
		$show_button  = $attributes['showButton'] ?? true;

		$primary  = $attributes['primaryColor'] ?? '#2f6bff';
		$card_bg  = $attributes['cardBgColor'] ?? '#ffffff';
		$card_bd  = $attributes['cardBorderColor'] ?? '#eef1f6';
		$card_rad = (int) ( $attributes['cardBorderRadius'] ?? 16 ) . 'px';
		$gap      = (int) ( $attributes['cardGap'] ?? 24 ) . 'px';
		$img_h    = (int) ( $attributes['imageHeight'] ?? 180 ) . 'px';
		$ttl_col  = $attributes['titleColor'] ?? '#14223f';
		$ttl_fs   = (int) ( $attributes['titleFontSize'] ?? 16 ) . 'px';
		$meta_col = $attributes['metaColor'] ?? '#6b7280';
		$btn_rad  = (int) ( $attributes['buttonBorderRadius'] ?? 10 ) . 'px';
		$btn_text = $attributes['buttonText'] ?? __( 'Book now', 'radius-booking' );

		try {
			$employees = ( new UserController() )->fetchAllEmployees();
		} catch ( \Exception $e ) {
			$employees = array();
		}

		if ( $service_flt ) {
			$employees = array_filter(
				$employees,
				function ( $e ) use ( $service_flt ) {
					foreach ( ( $e['services'] ?? array() ) as $s ) {
						if ( (string) $s['service_id'] === $service_flt ) {
							return true;
						}
					}
					return false;
				}
			);
		}
		if ( $location_flt ) {
			$employees = array_filter(
				$employees,
				fn( $e ) => false !== strpos( strtolower( (string) ( $e['location'] ?? '' ) ), $location_flt )
			);
		}

		$employees = array_slice( array_values( $employees ), 0, $per_page );

		if ( empty( $employees ) ) {
			return '<div style="text-align:center;padding:40px;color:#6b7280;">' . esc_html__( 'No staff found.', 'radius-booking' ) . '</div>';
		}

		$is_list  = 'list' === $layout;
		$grid_css = $is_list
			? 'display:flex;flex-direction:column;gap:' . esc_attr( $gap ) . ';'
			: 'display:grid;grid-template-columns:repeat(' . $columns . ',minmax(0,1fr));gap:' . esc_attr( $gap ) . ';';

		$html  = '<div style="font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;">';
		$html .= '<div style="' . $grid_css . '">';

		foreach ( $employees as $emp ) {
			$html .= $this->render_employee_list_preview_card(
				$emp,
				$is_list,
				$show_image,
				$show_serv,
				$show_loc,
				$show_phone,
				$show_desc,
				$show_button,
				array(
					'primary'   => $primary,
					'card_bg'   => $card_bg,
					'card_bd'   => $card_bd,
					'card_rad'  => $card_rad,
					'img_h'     => $img_h,
					'ttl_col'   => $ttl_col,
					'ttl_fs'    => $ttl_fs,
					'meta_col'  => $meta_col,
					'btn_rad'   => $btn_rad,
					'btn_text'  => $btn_text,
				)
			);
		}

		$html .= '</div></div>';

		return $html;
	}

	/**
	 * Render a single employee card for the static Gutenberg preview.
	 *
	 * Mirrors the React EmployeeCard markup so editor authors see (roughly)
	 * what the public site will render. Frontend rendering still comes from
	 * the React bundle via the mount div.
	 *
	 * @param array $emp         Employee data from fetchAllEmployees().
	 * @param bool  $is_list     True for the horizontal list layout.
	 * @param bool  $show_image  Show the avatar panel.
	 * @param bool  $show_serv   Show the service chips.
	 * @param bool  $show_loc    Show the location row.
	 * @param bool  $show_phone  Show the phone row.
	 * @param bool  $show_desc   Show the description.
	 * @param bool  $show_button Show the booking CTA.
	 * @param array $style       Style tokens (colors, sizes, button text).
	 *
	 * @return string
	 */
	private function render_employee_list_preview_card( $emp, $is_list, $show_image, $show_serv, $show_loc, $show_phone, $show_desc, $show_button, $style ) {
		$name             = ! empty( $emp['name'] ) ? $emp['name'] : __( 'Staff', 'radius-booking' );
		$role             = (string) ( $emp['role'] ?? '' );
		$status           = (string) ( $emp['today_status'] ?? 'available' );
		$today_index      = (int) ( $emp['today_day_index'] ?? 0 );
		$schedule         = is_array( $emp['schedule_days'] ?? null ) ? $emp['schedule_days'] : array();
		$hours_text       = (string) ( $emp['schedule_hours'] ?? '' );
		$schedule_groups  = is_array( $emp['schedule_groups'] ?? null ) ? $emp['schedule_groups'] : array();
		$emp_services     = is_array( $emp['services'] ?? null ) ? $emp['services'] : array();

		$palette = $this->preview_avatar_palette( $emp['id'] ?? $name );

		$badge_bg = '#16a34a';
		$badge_lb = __( 'Available', 'radius-booking' );
		if ( 'busy' === $status ) {
			$badge_bg = '#f59e0b';
			$badge_lb = __( 'Busy', 'radius-booking' );
		} elseif ( 'off' === $status ) {
			$badge_bg = '#ef4444';
			$badge_lb = __( 'Off today', 'radius-booking' );
		}

		$card_dir  = $is_list ? 'row' : 'column';
		$head_css  = $is_list
			? 'position:relative;width:200px;flex-shrink:0;'
			: 'position:relative;width:100%;';
		$avatar_h  = $is_list ? '100%' : esc_attr( $style['img_h'] );

		$html  = '<div style="position:relative;display:flex;flex-direction:' . $card_dir . ';background:' . esc_attr( $style['card_bg'] ) . ';border:1px solid ' . esc_attr( $style['card_bd'] ) . ';border-radius:' . esc_attr( $style['card_rad'] ) . ';overflow:hidden;box-shadow:0 1px 2px rgba(16,24,40,0.04);">';

		// Head (avatar + badge).
		$html .= '<div style="' . $head_css . '">';
		if ( $show_image ) {
			$html .= '<div style="position:relative;width:100%;height:' . $avatar_h . ';min-height:' . ( $is_list ? '200px' : esc_attr( $style['img_h'] ) ) . ';display:flex;align-items:center;justify-content:center;overflow:hidden;background:' . esc_attr( $palette['bg'] ) . ';color:' . esc_attr( $palette['fg'] ) . ';">';
			if ( ! empty( $emp['image_url'] ) ) {
				$html .= '<img src="' . esc_url( $emp['image_url'] ) . '" alt="' . esc_attr( $name ) . '" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center top;display:block;" />';
			} else {
				$initials = $this->preview_initials( $name );
				$html    .= '<span style="font-size:64px;font-weight:700;line-height:1;letter-spacing:-1px;">' . esc_html( $initials ) . '</span>';
			}
			$html .= '</div>';
		}
		$html .= '<span style="position:absolute;top:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:4px 10px 4px 8px;font-size:11.5px;font-weight:600;line-height:1.4;border-radius:9999px;color:#fff;background:' . esc_attr( $badge_bg ) . ';box-shadow:0 1px 2px rgba(16,24,40,0.18);">';
		$html .= '<span style="width:6px;height:6px;border-radius:9999px;background:currentColor;opacity:0.95;"></span>';
		$html .= esc_html( $badge_lb );
		$html .= '</span>';
		$html .= '</div>';

		// Body. In list layout the body is a horizontal row so the CTA can sit
		// on the right; in grid mode it stays a vertical column.
		$body_dir   = $is_list ? 'row' : 'column';
		$body_align = $is_list ? 'center' : 'stretch';
		$body_gap   = $is_list ? '20px' : '0';
		$html      .= '<div style="padding:16px 18px 18px;display:flex;flex-direction:' . $body_dir . ';align-items:' . $body_align . ';gap:' . $body_gap . ';flex:1;min-width:0;">';
		$html      .= '<div style="flex:1;min-width:0;">';
		$html .= '<div style="margin:0;color:' . esc_attr( $style['ttl_col'] ) . ';font-size:' . esc_attr( $style['ttl_fs'] ) . ';font-weight:600;line-height:1.35;word-break:break-word;">' . esc_html( $name ) . '</div>';
		if ( '' !== $role ) {
			$html .= '<div style="margin-top:2px;color:' . esc_attr( $style['meta_col'] ) . ';font-size:13px;line-height:1.4;">' . esc_html( $role ) . '</div>';
		}

		if ( $show_serv && ! empty( $emp_services ) ) {
			$chip_show = array_slice( $emp_services, 0, 2 );
			$chip_more = count( $emp_services ) - count( $chip_show );
			$html     .= '<div style="margin-top:10px;display:flex;flex-wrap:wrap;gap:6px;">';
			foreach ( $chip_show as $svc ) {
				$html .= '<span style="font-size:11.5px;line-height:1.6;padding:2px 10px;border-radius:9999px;background:color-mix(in srgb, ' . $style['primary'] . ' 10%, #fff);color:color-mix(in srgb, ' . $style['primary'] . ' 72%, #1f2937);white-space:nowrap;">' . esc_html( $svc['name'] ?? '' ) . '</span>';
			}
			if ( $chip_more > 0 ) {
				/* translators: %d: count of additional service chips. */
				$html .= '<span style="font-size:11.5px;line-height:1.6;padding:2px 8px;border-radius:9999px;background:#fff;color:#6b7280;border:1px solid #e5e7eb;white-space:nowrap;">' . esc_html( sprintf( __( '+%d', 'radius-booking' ), $chip_more ) ) . '</span>';
			}
			$html .= '</div>';
		}

		// Schedule section. When the week has mixed hours, the per-group rows
		// already encode which days belong to which range — rendering the
		// M T W T F S S pills on top of that would be redundant, so we only
		// show the pills when every working day shares one range.
		$html        .= '<div style="margin-top:14px;padding-top:12px;border-top:1px solid #eef1f6;">';
		$html        .= '<div style="font-size:10.5px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#6b7280;margin-bottom:8px;">' . esc_html__( 'Schedule', 'radius-booking' ) . '</div>';
		$has_mixed   = ( count( $schedule_groups ) > 1 );

		if ( $has_mixed ) {
			$html .= '<ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px;">';
			foreach ( $schedule_groups as $group ) {
				$g_label = (string) ( $group['days_label'] ?? '' );
				$g_hours = (string) ( $group['hours_text'] ?? '' );
				if ( '' === $g_label || '' === $g_hours ) {
					continue;
				}
				$html .= '<li style="display:flex;align-items:center;flex-wrap:wrap;gap:6px;font-size:13px;color:#374151;line-height:1.4;">';
				$html .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
				$html .= '<span style="font-weight:600;color:#111827;">' . esc_html( $g_label ) . '</span>';
				$html .= '<span style="color:#9ca3af;">·</span>';
				$html .= '<span>' . esc_html( $g_hours ) . '</span>';
				$html .= '</li>';
			}
			$html .= '</ul>';
		} else {
			$html        .= '<div style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;">';
			$day_letters = array( 1 => 'M', 2 => 'T', 3 => 'W', 4 => 'T', 5 => 'F', 6 => 'S', 7 => 'S' );
			foreach ( $day_letters as $idx => $letter ) {
				$enabled  = ! empty( $schedule[ $idx ] );
				$is_today = $idx === $today_index;
				if ( $enabled && $is_today ) {
					$pill_style = 'color:#fff;background:color-mix(in srgb, ' . $style['primary'] . ' 55%, #fff);box-shadow:0 0 0 2px color-mix(in srgb, ' . $style['primary'] . ' 20%, transparent);';
				} elseif ( $enabled ) {
					$pill_style = 'color:#fff;background:' . esc_attr( $style['primary'] ) . ';';
				} else {
					$pill_style = 'color:#9ca3af;background:#f3f4f6;';
				}
				$html .= '<span style="display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:9999px;font-size:10.5px;font-weight:600;line-height:1;' . $pill_style . '">' . esc_html( $letter ) . '</span>';
			}
			$html .= '</div>';

			if ( '' !== $hours_text ) {
				$html .= '<div style="margin-top:10px;display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#374151;">';
				$html .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
				$html .= '<span>' . esc_html( $hours_text ) . '</span>';
				$html .= '</div>';
			}
		}
		$html .= '</div>';

		// Contact rows.
		$has_contact = ( $show_loc && ! empty( $emp['location'] ) ) || ( $show_phone && ! empty( $emp['phone'] ) );
		if ( $has_contact ) {
			$html .= '<div style="margin-top:12px;display:flex;flex-direction:column;gap:4px;">';
			if ( $show_loc && ! empty( $emp['location'] ) ) {
				$html .= '<div style="display:flex;align-items:center;gap:8px;font-size:13px;color:#374151;">';
				$html .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>';
				$html .= '<span>' . esc_html( $emp['location'] ) . '</span>';
				$html .= '</div>';
			}
			if ( $show_phone && ! empty( $emp['phone'] ) ) {
				$html .= '<div style="display:flex;align-items:center;gap:8px;font-size:13px;color:#374151;">';
				$html .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.86 19.86 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.86 19.86 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92Z"/></svg>';
				$html .= '<span>' . esc_html( $emp['phone'] ) . '</span>';
				$html .= '</div>';
			}
			$html .= '</div>';
		}

		if ( $show_desc && ! empty( $emp['description'] ) ) {
			$html .= '<p style="margin:12px 0 0;color:#6b7280;font-size:12.5px;line-height:1.55;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;">' . esc_html( $emp['description'] ) . '</p>';
		}

		$html .= '</div>';

		if ( $show_button ) {
			$is_off    = ( 'off' === $status );
			$btn_op    = $is_off ? 'opacity:0.55;cursor:not-allowed;' : 'cursor:pointer;';
			$btn_sizing = $is_list
				? 'width:auto;flex-shrink:0;align-self:center;margin-top:0;padding:10px 26px;'
				: 'width:100%;margin-top:16px;padding:10px 18px;';
			$html      .= '<button type="button" style="display:inline-flex;align-items:center;justify-content:center;gap:8px;background:#fff;color:#111827;border:1px solid #e5e7eb;border-radius:' . esc_attr( $style['btn_rad'] ) . ';font-size:13.5px;font-weight:600;line-height:1.2;' . $btn_sizing . $btn_op . '">';
			$html      .= '<span>' . esc_html( $style['btn_text'] ) . '</span>';
			$html      .= '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>';
			$html      .= '</button>';
		}

		$html .= '</div></div>';

		return $html;
	}

	/**
	 * Pastel avatar palette (background + foreground) matching the React
	 * preview's deterministic colour assignment.
	 *
	 * @param string|int $seed Stable seed (employee id or name).
	 *
	 * @return array{bg:string,fg:string}
	 */
	private function preview_avatar_palette( $seed ) {
		$palette = array(
			array( 'bg' => '#dbeafe', 'fg' => '#1d4ed8' ),
			array( 'bg' => '#d1fae5', 'fg' => '#065f46' ),
			array( 'bg' => '#ede9fe', 'fg' => '#6d28d9' ),
			array( 'bg' => '#fce7f3', 'fg' => '#9d174d' ),
			array( 'bg' => '#fef3c7', 'fg' => '#92400e' ),
			array( 'bg' => '#cffafe', 'fg' => '#0e7490' ),
			array( 'bg' => '#fee2e2', 'fg' => '#991b1b' ),
			array( 'bg' => '#e0e7ff', 'fg' => '#3730a3' ),
		);

		$str  = (string) $seed;
		$hash = 0;
		$len  = strlen( $str );
		for ( $i = 0; $i < $len; $i++ ) {
			$hash = ( $hash * 31 + ord( $str[ $i ] ) ) & 0xFFFFFFFF;
		}

		return $palette[ $hash % count( $palette ) ];
	}

	/**
	 * Two-character uppercase initials from a full name (e.g. "Shahin Hasan"
	 * -> "SH"). Falls back to the first two characters of a single-word name.
	 *
	 * @param string $name Full name.
	 *
	 * @return string
	 */
	private function preview_initials( $name ) {
		$parts = preg_split( '/\s+/', trim( (string) $name ) );
		$parts = array_values( array_filter( $parts ) );
		if ( empty( $parts ) ) {
			return '?';
		}
		if ( 1 === count( $parts ) ) {
			return strtoupper( mb_substr( $parts[0], 0, 2 ) );
		}
		return strtoupper( mb_substr( $parts[0], 0, 1 ) . mb_substr( end( $parts ), 0, 1 ) );
	}

	/**
	 * Render the Service List block on the frontend.
	 *
	 * @param array $attributes Block attributes.
	 *
	 * @return string
	 */
	public function render_service_list( $attributes ) {
		// Editor preview — render static HTML preview.
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return $this->render_service_list_preview( $attributes );
		}

		$layout   = esc_attr( $attributes['layout'] ?? 'grid' );
		$columns  = esc_attr( $attributes['columns'] ?? 3 );
		$per_page = esc_attr( $attributes['perPage'] ?? 9 );
		$category = esc_attr( $attributes['category'] ?? '' );

		$shortcode = "[rtrb_service_list layout=\"{$layout}\" columns=\"{$columns}\" per_page=\"{$per_page}\" category=\"{$category}\"]";
		$html      = do_shortcode( $shortcode );

		// Inject block style/display data attributes into the mount div.
		$block_styles = wp_json_encode(
			array(
				'primaryColor'       => $attributes['primaryColor'] ?? '#0040ff',
				'cardBgColor'        => $attributes['cardBgColor'] ?? '#ffffff',
				'cardBorderColor'    => $attributes['cardBorderColor'] ?? '#e5e7eb',
				'cardBorderRadius'   => ( $attributes['cardBorderRadius'] ?? 12 ) . 'px',
				'cardGap'            => ( $attributes['cardGap'] ?? 24 ) . 'px',
				'imageHeight'        => ( $attributes['imageHeight'] ?? 192 ) . 'px',
				'titleColor'         => $attributes['titleColor'] ?? '#111827',
				'titleFontSize'      => ( $attributes['titleFontSize'] ?? 18 ) . 'px',
				'descColor'          => $attributes['descColor'] ?? '#6b7280',
				'priceColor'         => $attributes['priceColor'] ?? '',
				'buttonBgColor'      => $attributes['buttonBgColor'] ?? '',
				'buttonTextColor'    => $attributes['buttonTextColor'] ?? '#ffffff',
				'buttonBorderRadius' => ( $attributes['buttonBorderRadius'] ?? 8 ) . 'px',
				'filterActiveBg'     => $attributes['filterActiveBg'] ?? '',
				'filterActiveText'   => $attributes['filterActiveText'] ?? '#ffffff',
				'paginationActiveBg'   => $attributes['paginationActiveBg'] ?? '',
				'paginationActiveText' => $attributes['paginationActiveText'] ?? '#ffffff',
			)
		);

		$display_options = wp_json_encode(
			array(
				'showImage'          => $attributes['showImage'] ?? true,
				'showPrice'          => $attributes['showPrice'] ?? true,
				'showDuration'       => $attributes['showDuration'] ?? true,
				'showCategory'       => $attributes['showCategory'] ?? true,
				'showDescription'    => $attributes['showDescription'] ?? true,
				'showCategoryFilter' => $attributes['showCategoryFilter'] ?? true,
				'showStaff'          => $attributes['showStaff'] ?? true,
				'buttonText'         => $attributes['buttonText'] ?? 'Book Now',
			)
		);

		// Add style/display data attributes to the shortcode's mount div. The
		// template renders the wrapper as `class="rtrb-root rtrb-service-list"`,
		// so match the full class attribute (in either order) instead of relying
		// on a literal `class="rtrb-service-list"` which never matches.
		$injected = ' data-elementor-styles="' . esc_attr( $block_styles ) . '" data-elementor-display="' . esc_attr( $display_options ) . '"';
		$html     = preg_replace(
			'/(<div\b[^>]*\bclass="[^"]*\brtrb-service-list\b[^"]*")/',
			'$1' . $injected,
			$html,
			1
		);

		return $html;
	}
	/**
	 * Static HTML preview for the Service List in the block editor.
	 *
	 * @param array $attributes Block attributes.
	 *
	 * @return string
	 */
	private function render_service_list_preview( $attributes ) {
		$layout       = $attributes['layout'] ?? 'grid';
		$columns      = (int) ( $attributes['columns'] ?? 3 );
		$per_page     = (int) ( $attributes['perPage'] ?? 9 );
		$cat_filter   = $attributes['category'] ?? '';
		$show_image   = $attributes['showImage'] ?? true;
		$show_price   = $attributes['showPrice'] ?? true;
		$show_dur     = $attributes['showDuration'] ?? true;
		$show_cat     = $attributes['showCategory'] ?? true;
		$show_desc    = $attributes['showDescription'] ?? true;
		$btn_text     = $attributes['buttonText'] ?? 'Book Now';
		$primary      = $attributes['primaryColor'] ?? '#0040ff';
		$card_bg      = $attributes['cardBgColor'] ?? '#ffffff';
		$card_border  = $attributes['cardBorderColor'] ?? '#e5e7eb';
		$card_radius  = ( $attributes['cardBorderRadius'] ?? 12 ) . 'px';
		$gap          = ( $attributes['cardGap'] ?? 24 ) . 'px';
		$img_h        = ( $attributes['imageHeight'] ?? 192 ) . 'px';
		$title_color  = $attributes['titleColor'] ?? '#111827';
		$title_fs     = ( $attributes['titleFontSize'] ?? 18 ) . 'px';
		$desc_color   = $attributes['descColor'] ?? '#6b7280';
		$price_color  = $attributes['priceColor'] ?: $primary;
		$btn_bg       = $attributes['buttonBgColor'] ?: $primary;
		$btn_color    = $attributes['buttonTextColor'] ?? '#ffffff';
		$btn_radius   = ( $attributes['buttonBorderRadius'] ?? 8 ) . 'px';

		// Fetch real services.
		try {
			$all_services = ( new ServiceController() )->fetchAllServices();
		} catch ( \Exception $e ) {
			$all_services = array();
		}

		// Flatten services from categories.
		$services = array();
		if ( is_array( $all_services ) ) {
			foreach ( $all_services as $cat ) {
				if ( empty( $cat['services'] ) || ! is_array( $cat['services'] ) ) {
					continue;
				}
				foreach ( $cat['services'] as $svc ) {
					if ( isset( $svc['is_visible'] ) && '1' !== (string) $svc['is_visible'] ) {
						continue;
					}
					if ( $cat_filter && isset( $cat['id'] ) && (string) $cat['id'] !== (string) $cat_filter ) {
						continue;
					}
					$svc['category_name'] = $cat['name'] ?? '';
					$services[]           = $svc;
				}
			}
		}

		$services = array_slice( $services, 0, $per_page );

		if ( empty( $services ) ) {
			return '<div style="text-align:center;padding:40px;color:#6b7280;">' . esc_html__( 'No services found.', 'radius-booking' ) . '</div>';
		}

		$html = '<div style="font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;">';

		if ( 'list' === $layout ) {
			// List layout
			$html .= '<div style="display:flex;flex-direction:column;gap:' . esc_attr( $gap ) . ';">';
			foreach ( $services as $svc ) {
				$html .= '<div style="display:flex;border:1px solid ' . esc_attr( $card_border ) . ';border-radius:' . esc_attr( $card_radius ) . ';background:' . esc_attr( $card_bg ) . ';overflow:hidden;">';

				if ( $show_image ) {
					if ( ! empty( $svc['picture_full_path'] ) ) {
						$html .= '<div style="width:200px;flex-shrink:0;"><img src="' . esc_url( $svc['picture_full_path'] ) . '" alt="' . esc_attr( $svc['name'] ?? '' ) . '" style="width:100%;height:100%;object-fit:cover;" /></div>';
					} else {
						$html .= '<div style="width:200px;flex-shrink:0;background:#f3f4f6;display:flex;align-items:center;justify-content:center;">';
						$html .= '<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#c4cdd5" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>';
						$html .= '</div>';
					}
				}

				$html .= '<div style="flex:1;padding:16px;display:flex;align-items:center;justify-content:space-between;gap:16px;">';
				$html .= '<div>';
				if ( $show_cat && ! empty( $svc['category_name'] ) ) {
					$html .= '<span style="display:inline-block;font-size:11px;font-weight:500;padding:2px 8px;border-radius:99px;background:' . esc_attr( $primary ) . '1a;color:' . esc_attr( $primary ) . ';margin-bottom:6px;">' . esc_html( $svc['category_name'] ) . '</span>';
				}
				$html .= '<div style="font-weight:600;font-size:' . esc_attr( $title_fs ) . ';color:' . esc_attr( $title_color ) . ';margin-bottom:4px;">' . esc_html( $svc['name'] ?? '' ) . '</div>';
				if ( $show_desc && ! empty( $svc['description'] ) ) {
					$html .= '<div style="font-size:13px;color:' . esc_attr( $desc_color ) . ';margin-bottom:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px;">' . esc_html( wp_strip_all_tags( $svc['description'] ) ) . '</div>';
				}
				$html .= '<div style="display:flex;align-items:center;gap:10px;">';
				if ( $show_price && isset( $svc['price'] ) ) {
					$html .= '<span style="font-size:16px;font-weight:700;color:' . esc_attr( $price_color ) . ';">' . esc_html( get_rtrb_currency_symbol() . number_format( (float) $svc['price'], 2 ) ) . '</span>';
				}
				if ( $show_dur && ! empty( $svc['duration'] ) ) {
					$mins = (int) round( (int) $svc['duration'] / 60 );
					$dur  = $mins >= 60 ? floor( $mins / 60 ) . 'h' . ( $mins % 60 ? ' ' . ( $mins % 60 ) . 'min' : '' ) : $mins . ' min';
					$html .= '<span style="font-size:12px;color:#9ca3af;">' . esc_html( $dur ) . '</span>';
				}
				$html .= '</div>';
				$html .= '</div>';
				$html .= '<button style="flex-shrink:0;padding:10px 20px;font-size:13px;font-weight:600;border:none;border-radius:' . esc_attr( $btn_radius ) . ';background:' . esc_attr( $btn_bg ) . ';color:' . esc_attr( $btn_color ) . ';cursor:pointer;">' . esc_html( $btn_text ) . '</button>';
				$html .= '</div>';

				$html .= '</div>';
			}
			$html .= '</div>';
		} else {
			// Grid layout
			$col_template = '1fr';
			if ( $columns >= 2 ) {
				$col_template = 'repeat(' . $columns . ', 1fr)';
			}

			$html .= '<div style="display:grid;grid-template-columns:' . esc_attr( $col_template ) . ';gap:' . esc_attr( $gap ) . ';">';
			foreach ( $services as $svc ) {
				$html .= '<div style="border:1px solid ' . esc_attr( $card_border ) . ';border-radius:' . esc_attr( $card_radius ) . ';background:' . esc_attr( $card_bg ) . ';overflow:hidden;">';

				if ( $show_image ) {
					if ( ! empty( $svc['picture_full_path'] ) ) {
						$html .= '<div style="height:' . esc_attr( $img_h ) . ';overflow:hidden;"><img src="' . esc_url( $svc['picture_full_path'] ) . '" alt="' . esc_attr( $svc['name'] ?? '' ) . '" style="width:100%;height:100%;object-fit:cover;" /></div>';
					} else {
						$html .= '<div style="height:' . esc_attr( $img_h ) . ';background:#f3f4f6;display:flex;align-items:center;justify-content:center;">';
						$html .= '<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#c4cdd5" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>';
						$html .= '</div>';
					}
				}

				$html .= '<div style="padding:16px;">';
				if ( $show_cat && ! empty( $svc['category_name'] ) ) {
					$html .= '<span style="display:inline-block;font-size:11px;font-weight:500;padding:2px 8px;border-radius:99px;background:' . esc_attr( $primary ) . '1a;color:' . esc_attr( $primary ) . ';margin-bottom:6px;">' . esc_html( $svc['category_name'] ) . '</span>';
				}
				$html .= '<div style="font-weight:600;font-size:' . esc_attr( $title_fs ) . ';color:' . esc_attr( $title_color ) . ';margin-bottom:4px;">' . esc_html( $svc['name'] ?? '' ) . '</div>';
				if ( $show_desc && ! empty( $svc['description'] ) ) {
					$html .= '<div style="font-size:13px;color:' . esc_attr( $desc_color ) . ';margin-bottom:8px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">' . esc_html( wp_strip_all_tags( $svc['description'] ) ) . '</div>';
				}
				$html .= '<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">';
				if ( $show_price && isset( $svc['price'] ) ) {
					$html .= '<span style="font-size:16px;font-weight:700;color:' . esc_attr( $price_color ) . ';">' . esc_html( get_rtrb_currency_symbol() . number_format( (float) $svc['price'], 2 ) ) . '</span>';
				}
				if ( $show_dur && ! empty( $svc['duration'] ) ) {
					$mins = (int) round( (int) $svc['duration'] / 60 );
					$dur  = $mins >= 60 ? floor( $mins / 60 ) . 'h' . ( $mins % 60 ? ' ' . ( $mins % 60 ) . 'min' : '' ) : $mins . ' min';
					$html .= '<span style="font-size:12px;color:#9ca3af;">' . esc_html( $dur ) . '</span>';
				}
				$html .= '</div>';
				$html .= '<button style="width:100%;padding:10px;font-size:13px;font-weight:600;border:none;border-radius:' . esc_attr( $btn_radius ) . ';background:' . esc_attr( $btn_bg ) . ';color:' . esc_attr( $btn_color ) . ';cursor:pointer;">' . esc_html( $btn_text ) . '</button>';
				$html .= '</div>';

				$html .= '</div>';
			}
			$html .= '</div>';
		}

		$html .= '</div>';

		return $html;
	}
}
